From af6d9080358de74bc3675b567eacbfe9b18111f4 Mon Sep 17 00:00:00 2001 From: metah3m Date: Thu, 30 Jul 2026 17:52:43 +0800 Subject: [PATCH 1/8] build: track h3x reliability branch --- Cargo.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1acd3a3..64f4c5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,9 +52,12 @@ ddns = { package = "dyns", version = "0.7.0-beta.2", features = [ "mdns", "dquic-network", ] } -h3x = { version = "0.6.0-beta.4", features = [ +h3x = { git = "https://github.com/genmeta/h3x.git", branch = "fix/ha-http3-reliability", version = "0.6.0-beta.4", features = [ "dquic", ] } dhttp = { path = "dhttp", version = "0.6.0-beta.5" } dhttp-access = { path = "access", version = "0.4.0-beta.2" } dhttp-log = { path = "log", version = "0.1.0-beta.1" } + +[patch.crates-io] +h3x = { git = "https://github.com/genmeta/h3x.git", branch = "fix/ha-http3-reliability" } From 1b263394527251d0b13b1d181055e6345bff26d3 Mon Sep 17 00:00:00 2001 From: metah3m Date: Thu, 30 Jul 2026 17:53:02 +0800 Subject: [PATCH 2/8] fix(dns): select stun resolver by server --- dhttp/src/ddns.rs | 78 +++++++++++++++++++++++++++++++++++++++++-- dhttp/src/endpoint.rs | 44 ++++-------------------- dhttp/src/network.rs | 38 +++++++++++++++++++++ 3 files changed, 120 insertions(+), 40 deletions(-) diff --git a/dhttp/src/ddns.rs b/dhttp/src/ddns.rs index 6c64720..d038adc 100644 --- a/dhttp/src/ddns.rs +++ b/dhttp/src/ddns.rs @@ -22,6 +22,8 @@ pub type ArcResolver = Arc; /// Publisher trait object used by DHTTP DNS construction. pub type ArcPublisher = Arc; +const DHTTP_DNS_SUFFIX: &str = "dhttp.net"; + #[derive(Clone)] enum DhttpDnsOp { Dns(resolvers::DnsScheme), @@ -188,8 +190,15 @@ where let deferred_stun_resolver = Arc::new(DeferredStunResolver::new()); let stun_resolver: ArcResolver = deferred_stun_resolver.clone(); let network = builder(stun_resolver); - let final_resolver = - network_stun_resolver_from_plan(dns_plan, network.clone(), bind, h3_dns_server).await?; + let stun_server = network.quic().stun_server(); + let final_resolver = network_stun_resolver_from_plan( + dns_plan, + network.clone(), + bind, + h3_dns_server, + stun_server.as_deref(), + ) + .await?; DhttpNetwork::from_deferred_stun_resolver(network, deferred_stun_resolver, final_resolver) .context(build_dhttp_network_with_dns_error::DeferredStunResolverSnafu) @@ -223,9 +232,11 @@ async fn network_stun_resolver_from_plan( network: Arc, bind: Arc>, h3_dns_server: Arc, + stun_server: Option<&str>, ) -> Result { let operations = dns_plan.effective_ops(); - let h3_resolver = if uses_h3(&operations) { + let use_h3 = uses_h3(&operations) && stun_server.is_some_and(uses_h3_dns); + let h3_resolver = if use_h3 { let h3_underlay = network_h3_underlay(&operations, network.clone(), bind.clone()).await?; let h3_quic = dedicated_network_h3_client_quic(network.clone(), bind.clone(), h3_underlay).await; @@ -263,6 +274,14 @@ async fn network_stun_resolver_from_plan( } } + if uses_h3(&operations) + && !use_h3 + && !has_custom_resolver(&operations) + && !has_system_dns(&operations) + { + builder = builder.system(); + } + network_resolver_chain(builder.build()) } @@ -510,6 +529,33 @@ fn uses_h3(operations: &[DhttpDnsOp]) -> bool { .any(|operation| matches!(operation, DhttpDnsOp::Dns(resolvers::DnsScheme::H3))) } +fn uses_h3_dns(name: &str) -> bool { + let host = match name.rsplit_once(':') { + Some((host, digits)) + if !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit()) => + { + host + } + _ => name, + }; + let host = host.strip_suffix('.').unwrap_or(host); + + if rustls::pki_types::DnsName::try_from(host).is_err() { + return false; + } + + if host.eq_ignore_ascii_case(DHTTP_DNS_SUFFIX) { + return true; + } + + let Some(suffix_start) = host.len().checked_sub(DHTTP_DNS_SUFFIX.len()) else { + return false; + }; + suffix_start > 0 + && host.as_bytes().get(suffix_start - 1) == Some(&b'.') + && host.as_bytes()[suffix_start..].eq_ignore_ascii_case(DHTTP_DNS_SUFFIX.as_bytes()) +} + fn has_custom_resolver(operations: &[DhttpDnsOp]) -> bool { operations .iter() @@ -578,6 +624,32 @@ mod tests { } } + #[test] + fn h3_dns_is_limited_to_dhttp_names() { + for name in [ + "dhttp.net", + "DHTTP.NET.", + "node.dhttp.net", + "deep.node.dhttp.net:443", + "deep.node.dhttp.net.:7", + ] { + assert!(uses_h3_dns(name), "expected H3 DNS for {name}"); + } + + for name in [ + "nat.genmeta.net:20004", + "ddns.genmeta.net:443", + "notdhttp.net", + "dhttp.net.example", + "127.0.0.1:443", + "[::1]:443", + "dhttp.net:service", + "bad..name.dhttp.net", + ] { + assert!(!uses_h3_dns(name), "unexpected H3 DNS for {name}"); + } + } + #[test] fn dhttp_dns_plan_defaults_only_when_empty() { let empty = DhttpDnsPlan::new(); diff --git a/dhttp/src/endpoint.rs b/dhttp/src/endpoint.rs index f7fd9a4..df22c0d 100644 --- a/dhttp/src/endpoint.rs +++ b/dhttp/src/endpoint.rs @@ -1115,8 +1115,13 @@ mod tests { } #[tokio::test] - async fn owned_default_network_stun_resolver_keeps_h3_resolver_alive_through_weak_edge() { - let endpoint = Endpoint::builder().build().await.unwrap(); + async fn dhttp_stun_server_keeps_h3_resolver_alive_through_weak_edge() { + let network = DhttpNetwork::builder() + .stun_server(Some(Arc::from("node.dhttp.net"))) + .build() + .await + .unwrap(); + let endpoint = Endpoint::builder().network(network).build().await.unwrap(); let stun_resolver = endpoint.network().network().quic().stun_resolver(); let deferred_any: &dyn Any = stun_resolver.as_ref(); let deferred = deferred_any @@ -1135,41 +1140,6 @@ mod tests { })); } - #[tokio::test] - async fn h3_only_endpoint_stun_resolver_uses_h3_final_resolver_not_system_final_resolver() { - let endpoint = Endpoint::builder() - .dns(DnsScheme::H3) - .build() - .await - .unwrap(); - let stun_resolver = endpoint.network().network().quic().stun_resolver(); - let deferred_any: &dyn Any = stun_resolver.as_ref(); - let deferred = deferred_any - .downcast_ref::() - .expect("h3-only endpoint-owned network uses deferred STUN resolver"); - let weak_resolver = deferred - .get() - .expect("deferred STUN resolver is initialized"); - let actual = weak_resolver - .upgrade() - .expect("DhttpNetwork keeps the STUN resolver target alive"); - let resolver_names = actual - .iter() - .map(|resolver| resolver.to_string()) - .collect::>(); - - assert!( - resolver_names - .iter() - .any(|name| name.starts_with("H3 DNS Resolver(")) - ); - assert!( - !resolver_names - .iter() - .any(|name| name == "System DNS Resolver") - ); - } - #[tokio::test] async fn endpoint_name_returns_dhttp_identity_name() { let identity = valid_dhttp_identity("client.example.com.dhttp.net"); diff --git a/dhttp/src/network.rs b/dhttp/src/network.rs index 9c628b2..5f12c8b 100644 --- a/dhttp/src/network.rs +++ b/dhttp/src/network.rs @@ -315,6 +315,7 @@ mod tests { async fn h3_only_network_stun_resolver_uses_h3_without_system_final_resolver() { let dhttp_network = DhttpNetwork::builder() .dns(DnsScheme::H3) + .stun_server(Some(Arc::from("node.dhttp.net"))) .build() .await .expect("h3-only network should build"); @@ -347,6 +348,43 @@ mod tests { ); } + #[tokio::test] + async fn h3_only_external_stun_server_uses_system_final_resolver() { + let dhttp_network = DhttpNetwork::builder() + .dns(DnsScheme::H3) + .stun_server(Some(Arc::from("nat.genmeta.net:20004"))) + .build() + .await + .expect("external STUN server should build with system DNS fallback"); + + let stun_resolver = dhttp_network.network().quic().stun_resolver(); + let any: &dyn std::any::Any = stun_resolver.as_ref(); + let deferred = any + .downcast_ref::() + .expect("h3-only network stun resolver is deferred"); + let weak_resolver = deferred + .get() + .expect("deferred STUN resolver is initialized"); + let actual = weak_resolver + .upgrade() + .expect("DhttpNetwork keeps the STUN resolver target alive"); + let resolver_names = actual + .iter() + .map(|resolver| resolver.to_string()) + .collect::>(); + + assert!( + !resolver_names + .iter() + .any(|name| name.starts_with("H3 DNS Resolver(")) + ); + assert!( + resolver_names + .iter() + .any(|name| name == "System DNS Resolver") + ); + } + #[tokio::test] async fn explicit_custom_network_stun_resolver_is_not_augmented_with_system() { let calls = Arc::new(AtomicUsize::new(0)); From ea97448637ac11dfa6f7de3cb41a5d815f33a1d9 Mon Sep 17 00:00:00 2001 From: metah3m Date: Thu, 30 Jul 2026 19:49:36 +0800 Subject: [PATCH 3/8] fix(dns): route stun lookups by name --- dhttp/src/ddns.rs | 114 +++++++++++++++++++++++++++--------------- dhttp/src/endpoint.rs | 11 ++-- dhttp/src/network.rs | 50 +----------------- 3 files changed, 80 insertions(+), 95 deletions(-) diff --git a/dhttp/src/ddns.rs b/dhttp/src/ddns.rs index d038adc..e957d40 100644 --- a/dhttp/src/ddns.rs +++ b/dhttp/src/ddns.rs @@ -1,6 +1,6 @@ //! Re-export of the ddns crate APIs used by DHTTP. -use std::{future::Future, sync::Arc}; +use std::{fmt, future::Future, sync::Arc}; use snafu::ResultExt; @@ -112,6 +112,29 @@ impl DhttpDnsPlan { type DeferredEndpointResolver = resolvers::deferred::DeferredResolver; type EndpointH3Client = Arc>; +#[derive(Debug)] +struct StunResolverRouter { + dhttp: ArcResolvers, + external: ArcResolvers, +} + +impl fmt::Display for StunResolverRouter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("STUN DNS Router") + } +} + +impl Resolve for StunResolverRouter { + fn lookup<'a>(&'a self, name: &'a str) -> crate::dquic::resolver::ResolveFuture<'a> { + let resolvers = if uses_h3_dns(name) { + &self.dhttp + } else { + &self.external + }; + Resolve::lookup(resolvers.as_ref(), name) + } +} + #[derive(Clone)] struct EndpointH3Clients { resolver: EndpointH3Client, @@ -190,15 +213,8 @@ where let deferred_stun_resolver = Arc::new(DeferredStunResolver::new()); let stun_resolver: ArcResolver = deferred_stun_resolver.clone(); let network = builder(stun_resolver); - let stun_server = network.quic().stun_server(); - let final_resolver = network_stun_resolver_from_plan( - dns_plan, - network.clone(), - bind, - h3_dns_server, - stun_server.as_deref(), - ) - .await?; + let final_resolver = + network_stun_resolver_from_plan(dns_plan, network.clone(), bind, h3_dns_server).await?; DhttpNetwork::from_deferred_stun_resolver(network, deferred_stun_resolver, final_resolver) .context(build_dhttp_network_with_dns_error::DeferredStunResolverSnafu) @@ -232,20 +248,22 @@ async fn network_stun_resolver_from_plan( network: Arc, bind: Arc>, h3_dns_server: Arc, - stun_server: Option<&str>, ) -> Result { let operations = dns_plan.effective_ops(); - let use_h3 = uses_h3(&operations) && stun_server.is_some_and(uses_h3_dns); - let h3_resolver = if use_h3 { + let (h3_resolver, external_resolvers) = if uses_h3(&operations) { let h3_underlay = network_h3_underlay(&operations, network.clone(), bind.clone()).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, - )?)) + dedicated_network_h3_client_quic(network.clone(), bind.clone(), h3_underlay.clone()) + .await; + ( + Some(Arc::new(h3_resolver_for_network( + h3_dns_server.as_ref(), + h3_quic, + )?)), + Some(h3_underlay), + ) } else { - None + (None, None) }; let mut builder = resolvers::Resolvers::builder(); @@ -274,15 +292,16 @@ async fn network_stun_resolver_from_plan( } } - if uses_h3(&operations) - && !use_h3 - && !has_custom_resolver(&operations) - && !has_system_dns(&operations) - { - builder = builder.system(); - } + let dhttp_resolvers = network_resolver_chain(builder.build())?; + let Some(external_resolvers) = external_resolvers else { + return Ok(dhttp_resolvers); + }; - network_resolver_chain(builder.build()) + let router: ArcResolver = Arc::new(StunResolverRouter { + dhttp: dhttp_resolvers, + external: external_resolvers, + }); + network_resolver_chain(resolvers::Resolvers::new().with(router)) } async fn endpoint_dns_from_quic( @@ -426,10 +445,10 @@ async fn network_h3_underlay( operations: &[DhttpDnsOp], network: Arc, bind: Arc>, -) -> Result { +) -> Result { let resolvers = non_h3_resolvers(operations, network, bind).await; - network_arc_resolver_chain(resolvers) + network_resolver_chain(resolvers) } async fn non_h3_resolvers( @@ -513,16 +532,6 @@ fn network_resolver_chain( } } -fn network_arc_resolver_chain( - resolvers: resolvers::Resolvers, -) -> Result { - if resolvers.iter().next().is_none() { - build_dhttp_network_with_dns_error::EmptyResolverSnafu.fail() - } else { - Ok(Arc::new(resolvers)) - } -} - fn uses_h3(operations: &[DhttpDnsOp]) -> bool { operations .iter() @@ -650,6 +659,33 @@ mod tests { } } + #[tokio::test] + async fn stun_resolver_router_selects_branch_from_lookup_name() { + let dhttp_calls = Arc::new(AtomicUsize::new(0)); + let external_calls = Arc::new(AtomicUsize::new(0)); + let dhttp = Arc::new(resolvers::Resolvers::new().with(Arc::new(CountingResolver { + calls: dhttp_calls.clone(), + }))); + let external = Arc::new(resolvers::Resolvers::new().with(Arc::new(CountingResolver { + calls: external_calls.clone(), + }))); + let router = StunResolverRouter { dhttp, external }; + + let _dhttp_records = router + .lookup("node.dhttp.net:443") + .await + .expect("dhttp STUN name should use dhttp resolvers"); + assert_eq!(dhttp_calls.load(Ordering::SeqCst), 1); + assert_eq!(external_calls.load(Ordering::SeqCst), 0); + + let _external_records = router + .lookup("nat.genmeta.net:20004") + .await + .expect("external STUN name should use external resolvers"); + assert_eq!(dhttp_calls.load(Ordering::SeqCst), 1); + assert_eq!(external_calls.load(Ordering::SeqCst), 1); + } + #[test] fn dhttp_dns_plan_defaults_only_when_empty() { let empty = DhttpDnsPlan::new(); diff --git a/dhttp/src/endpoint.rs b/dhttp/src/endpoint.rs index df22c0d..16dd3e3 100644 --- a/dhttp/src/endpoint.rs +++ b/dhttp/src/endpoint.rs @@ -588,7 +588,7 @@ impl crate::h3x::quic::Connect for Endpoint { mod tests { use super::*; use crate::{ - ddns::resolvers::{DnsScheme, H3Resolver, Resolvers}, + ddns::resolvers::{DnsScheme, Resolvers}, dquic::Network, network::DeferredStunResolver, }; @@ -1115,7 +1115,7 @@ mod tests { } #[tokio::test] - async fn dhttp_stun_server_keeps_h3_resolver_alive_through_weak_edge() { + async fn owned_network_keeps_stun_resolver_alive_through_weak_edge() { let network = DhttpNetwork::builder() .stun_server(Some(Arc::from("node.dhttp.net"))) .build() @@ -1130,14 +1130,9 @@ mod tests { let weak_resolver = deferred .get() .expect("deferred STUN resolver is initialized"); - let actual = weak_resolver + let _actual = weak_resolver .upgrade() .expect("DhttpNetwork keeps the STUN resolver target alive"); - - assert!(actual.iter().any(|resolver| { - let resolver_any: &dyn Any = resolver.as_ref(); - resolver_any.is::>() - })); } #[tokio::test] diff --git a/dhttp/src/network.rs b/dhttp/src/network.rs index 5f12c8b..1da587b 100644 --- a/dhttp/src/network.rs +++ b/dhttp/src/network.rs @@ -312,7 +312,7 @@ mod tests { } #[tokio::test] - async fn h3_only_network_stun_resolver_uses_h3_without_system_final_resolver() { + async fn h3_only_network_installs_routed_stun_resolver() { let dhttp_network = DhttpNetwork::builder() .dns(DnsScheme::H3) .stun_server(Some(Arc::from("node.dhttp.net"))) @@ -336,53 +336,7 @@ mod tests { .map(|resolver| resolver.to_string()) .collect::>(); - assert!( - resolver_names - .iter() - .any(|name| name.starts_with("H3 DNS Resolver(")) - ); - assert!( - !resolver_names - .iter() - .any(|name| name == "System DNS Resolver") - ); - } - - #[tokio::test] - async fn h3_only_external_stun_server_uses_system_final_resolver() { - let dhttp_network = DhttpNetwork::builder() - .dns(DnsScheme::H3) - .stun_server(Some(Arc::from("nat.genmeta.net:20004"))) - .build() - .await - .expect("external STUN server should build with system DNS fallback"); - - let stun_resolver = dhttp_network.network().quic().stun_resolver(); - let any: &dyn std::any::Any = stun_resolver.as_ref(); - let deferred = any - .downcast_ref::() - .expect("h3-only network stun resolver is deferred"); - let weak_resolver = deferred - .get() - .expect("deferred STUN resolver is initialized"); - let actual = weak_resolver - .upgrade() - .expect("DhttpNetwork keeps the STUN resolver target alive"); - let resolver_names = actual - .iter() - .map(|resolver| resolver.to_string()) - .collect::>(); - - assert!( - !resolver_names - .iter() - .any(|name| name.starts_with("H3 DNS Resolver(")) - ); - assert!( - resolver_names - .iter() - .any(|name| name == "System DNS Resolver") - ); + assert_eq!(resolver_names, vec!["STUN DNS Router"]); } #[tokio::test] From 8a892345aa9c8600c3c7e95b3156f84d35cc904c Mon Sep 17 00:00:00 2001 From: metah3m Date: Thu, 30 Jul 2026 22:56:43 +0800 Subject: [PATCH 4/8] feat(bootstrap): use production endpoint --- .cargo/config.toml | 6 +++++ dhttp/Cargo.toml | 3 +++ dhttp/build.rs | 61 ++++++++++++++++++++++++++++++++++++------- dhttp/src/ddns.rs | 2 +- dhttp/src/endpoint.rs | 24 ++++++++--------- dhttp/src/network.rs | 6 ++--- 6 files changed, 77 insertions(+), 25 deletions(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..4d28253 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,6 @@ +[env] +DHTTP_BOOTSTRAP_URL = "https://bootstrap.genmeta.net:20002" +DHTTP_H3_DNS_SERVER = "https://ddns.genmeta.net:4433" +DHTTP_MDNS_SERVICE = "_dhttp.local" +# Compatibility for the dyns version currently pinned in Cargo.lock. +DHTTP_HTTP_DNS_SERVER = "https://bootstrap.genmeta.net:20002" diff --git a/dhttp/Cargo.toml b/dhttp/Cargo.toml index 0ffbe94..8e8e21c 100644 --- a/dhttp/Cargo.toml +++ b/dhttp/Cargo.toml @@ -38,6 +38,9 @@ dquic = { workspace = true } ddns = { workspace = true } h3x = { workspace = true } +[build-dependencies] +url = "2" + [dev-dependencies] dhttp-access = { workspace = true, features = ["http", "orm"] } tokio = { workspace = true, features = ["macros", "rt"] } diff --git a/dhttp/build.rs b/dhttp/build.rs index 88a2131..fb46e3a 100644 --- a/dhttp/build.rs +++ b/dhttp/build.rs @@ -1,9 +1,9 @@ use std::{env, fs, path::PathBuf}; const ROOT_CA_ENV: &str = "DHTTP_ROOT_CA"; -const STUN_SERVER_ENV: &str = "DHTTP_STUN_SERVER"; +const BOOTSTRAP_URL_ENV: &str = "DHTTP_BOOTSTRAP_URL"; -const DEFAULT_STUN_SERVER: &str = "stun.dhttp.example.net"; +const DEFAULT_BOOTSTRAP_URL: &str = "https://bootstrap.genmeta.net:20002"; const DEFAULT_ROOT_CA_PEM: &str = "\ -----BEGIN CERTIFICATE-----\n\ MIIDKTCCAhGgAwIBAgIUHNScq6R2U5QYUzxkEkNDaOJt4yMwDQYJKoZIhvcNAQEL\n\ @@ -37,17 +37,21 @@ fn main() { ) }); - let stun_server = env_or_default(STUN_SERVER_ENV, DEFAULT_STUN_SERVER); + let bootstrap_url = env_or_default(BOOTSTRAP_URL_ENV, DEFAULT_BOOTSTRAP_URL); + let bootstrap_authority = bootstrap_authority(&bootstrap_url).unwrap_or_else(|error| { + panic!("invalid {BOOTSTRAP_URL_ENV} value {bootstrap_url:?}: {error}") + }); let bootstrap = format!( "// @generated by build.rs; do not edit.\n\ - pub const DHTTP_STUN_SERVER: &str = {stun_server:?};\n\ + pub const DHTTP_BOOTSTRAP_URL: &str = {bootstrap_url:?};\n\ + pub const DHTTP_BOOTSTRAP_AUTHORITY: &str = {bootstrap_authority:?};\n\ pub const DHTTP_ROOT_CA: &[u8] = &{root_ca:?};\n" ); fs::write(out_dir.join("bootstrap.rs"), bootstrap) .expect("failed to write generated DHTTP bootstrap constants"); println!("cargo::rerun-if-env-changed={ROOT_CA_ENV}"); - println!("cargo::rerun-if-env-changed={STUN_SERVER_ENV}"); + println!("cargo::rerun-if-env-changed={BOOTSTRAP_URL_ENV}"); if let Some(root_ca) = optional_env_path(ROOT_CA_ENV) { println!("cargo::rerun-if-changed={}", root_ca.display()); } @@ -57,6 +61,31 @@ fn env_or_default(name: &str, default: &str) -> String { env::var(name).unwrap_or_else(|_| default.to_owned()) } +fn bootstrap_authority(value: &str) -> Result { + let url = url::Url::parse(value).map_err(|error| error.to_string())?; + if url.scheme() != "https" { + return Err("scheme must be https".to_owned()); + } + if url.username() != "" || url.password().is_some() { + return Err("credentials are not allowed".to_owned()); + } + if url.path() != "/" || url.query().is_some() || url.fragment().is_some() { + return Err("path, query, and fragment are not allowed".to_owned()); + } + + let host = url + .host_str() + .ok_or_else(|| "host is required".to_owned())?; + let port = url + .port() + .ok_or_else(|| "an explicit port is required".to_owned())?; + if matches!(url.host(), Some(url::Host::Ipv6(_))) { + Ok(format!("[{host}]:{port}")) + } else { + Ok(format!("{host}:{port}")) + } +} + fn optional_env_path(name: &str) -> Option { env::var_os(name).map(PathBuf::from) } @@ -79,15 +108,29 @@ mod tests { use super::*; #[test] - fn missing_stun_env_uses_dhttp_example_net_placeholder() { - let name = format!("__DHTTP_MISSING_STUN_{}", std::process::id()); + fn missing_bootstrap_env_uses_genmeta_production_default() { + let name = format!("__DHTTP_MISSING_BOOTSTRAP_{}", std::process::id()); assert_eq!( - env_or_default(&name, DEFAULT_STUN_SERVER), - "stun.dhttp.example.net" + env_or_default(&name, DEFAULT_BOOTSTRAP_URL), + "https://bootstrap.genmeta.net:20002" ); } + #[test] + fn bootstrap_url_produces_stun_authority() { + assert_eq!( + bootstrap_authority("https://bootstrap.genmeta.net:20002").as_deref(), + Ok("bootstrap.genmeta.net:20002") + ); + } + + #[test] + fn bootstrap_url_requires_https_and_explicit_port() { + assert!(bootstrap_authority("http://bootstrap.genmeta.net:20002").is_err()); + assert!(bootstrap_authority("https://bootstrap.genmeta.net").is_err()); + } + #[test] fn placeholder_root_ca_is_pem_certificate() { assert!(DEFAULT_ROOT_CA_PEM.starts_with("-----BEGIN CERTIFICATE-----")); diff --git a/dhttp/src/ddns.rs b/dhttp/src/ddns.rs index e957d40..bf03773 100644 --- a/dhttp/src/ddns.rs +++ b/dhttp/src/ddns.rs @@ -341,7 +341,7 @@ async fn endpoint_dns_from_quic( } DhttpDnsOp::Dns(resolvers::DnsScheme::Http) => { let http = Arc::new( - resolvers::HttpResolver::new(resolvers::DHTTP_HTTP_DNS_SERVER) + resolvers::HttpResolver::new(crate::endpoint::BOOTSTRAP_URL) .expect("BUG: DHTTP HTTP DNS server is a valid URL"), ); resolver_builder = resolver_builder.candidate_resolver(http.clone()); diff --git a/dhttp/src/endpoint.rs b/dhttp/src/endpoint.rs index 16dd3e3..c2c7759 100644 --- a/dhttp/src/endpoint.rs +++ b/dhttp/src/endpoint.rs @@ -88,12 +88,11 @@ pub enum CreateEndpointPublicationLoopError { AnonymousEndpoint, } -/// Default STUN bootstrap name for NAT traversal. -/// -/// 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; +/// Default bootstrap service URL. +pub const BOOTSTRAP_URL: &str = crate::bootstrap::DHTTP_BOOTSTRAP_URL; + +/// Bootstrap authority passed to the NAT traversal layer. +pub(crate) const BOOTSTRAP_AUTHORITY: &str = crate::bootstrap::DHTTP_BOOTSTRAP_AUTHORITY; fn normalize_bind(bind: Arc>) -> Arc> { if bind.is_empty() { @@ -616,16 +615,17 @@ mod tests { } #[test] - fn stun_server_comes_from_compile_time_environment() { - if let Some(expected) = option_env!("DHTTP_STUN_SERVER") { - assert_eq!(STUN_SERVER, expected); + fn bootstrap_url_comes_from_compile_time_environment() { + if let Some(expected) = option_env!("DHTTP_BOOTSTRAP_URL") { + assert_eq!(BOOTSTRAP_URL, expected); } } #[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"); + fn bootstrap_production_default_is_used_when_compile_time_env_is_absent() { + if option_env!("DHTTP_BOOTSTRAP_URL").is_none() { + assert_eq!(BOOTSTRAP_URL, "https://bootstrap.genmeta.net:20002"); + assert_eq!(BOOTSTRAP_AUTHORITY, "bootstrap.genmeta.net:20002"); } } diff --git a/dhttp/src/network.rs b/dhttp/src/network.rs index 1da587b..659da06 100644 --- a/dhttp/src/network.rs +++ b/dhttp/src/network.rs @@ -108,8 +108,8 @@ impl DhttpNetwork { #[builder(default = Arc::new(QuicRouter::new()))] quic_router: 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))); + let stun_server = stun_server + .unwrap_or_else(|| Some(Arc::::from(crate::endpoint::BOOTSTRAP_AUTHORITY))); if let Some(stun_resolver) = stun_resolver { let network = Network::builder() @@ -226,7 +226,7 @@ mod tests { assert_eq!( dhttp_network.network().quic().stun_server().as_deref(), - Some(crate::endpoint::STUN_SERVER) + Some(crate::endpoint::BOOTSTRAP_AUTHORITY) ); } From 7716e541e26e29c3de2a308980073d96f02628af Mon Sep 17 00:00:00 2001 From: metah3m Date: Fri, 31 Jul 2026 20:49:05 +0800 Subject: [PATCH 5/8] build: track bootstrap branches --- Cargo.toml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 64f4c5f..22497a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,8 +43,8 @@ tracing-subscriber = { version = "0.3", default-features = false, features = ["f # identity types with h3x/ddns in the formal release graph. dhttp-identity = "0.3.0-beta.1" dhttp-home = { path = "home", version = "0.5.0-beta.1" } -dquic = { version = "0.7.0-beta.4", default-features = false } -ddns = { package = "dyns", version = "0.7.0-beta.2", features = [ +dquic = { git = "https://github.com/genmeta/dquic.git", branch = "main", default-features = false } +ddns = { package = "dyns", git = "https://github.com/genmeta/ddns.git", branch = "fix/ddns-bootstrap", features = [ "resolvers", "publishers", "h3", @@ -52,12 +52,9 @@ ddns = { package = "dyns", version = "0.7.0-beta.2", features = [ "mdns", "dquic-network", ] } -h3x = { git = "https://github.com/genmeta/h3x.git", branch = "fix/ha-http3-reliability", version = "0.6.0-beta.4", features = [ +h3x = { git = "https://github.com/genmeta/h3x.git", branch = "fix/h3x-reliability", features = [ "dquic", ] } dhttp = { path = "dhttp", version = "0.6.0-beta.5" } dhttp-access = { path = "access", version = "0.4.0-beta.2" } dhttp-log = { path = "log", version = "0.1.0-beta.1" } - -[patch.crates-io] -h3x = { git = "https://github.com/genmeta/h3x.git", branch = "fix/ha-http3-reliability" } From a032b67e8357920542bf7c58bd656bdc10c42fec Mon Sep 17 00:00:00 2001 From: metah3m Date: Sun, 2 Aug 2026 23:58:37 +0800 Subject: [PATCH 6/8] fix(config): align production defaults --- .cargo/config.toml | 2 -- .env.example | 24 ++++-------------------- Cargo.toml | 8 ++++---- api/js/index.d.ts | 2 +- api/src/certificate.rs | 2 +- dhttp/build.rs | 32 ++++++++++++++------------------ dhttp/src/endpoint.rs | 2 +- log/tests/certificate.rs | 8 +++++--- 8 files changed, 30 insertions(+), 50 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 4d28253..5507cea 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,5 +2,3 @@ DHTTP_BOOTSTRAP_URL = "https://bootstrap.genmeta.net:20002" DHTTP_H3_DNS_SERVER = "https://ddns.genmeta.net:4433" DHTTP_MDNS_SERVICE = "_dhttp.local" -# Compatibility for the dyns version currently pinned in Cargo.lock. -DHTTP_HTTP_DNS_SERVER = "https://bootstrap.genmeta.net:20002" diff --git a/.env.example b/.env.example index 5713e04..32b28bb 100644 --- a/.env.example +++ b/.env.example @@ -1,21 +1,5 @@ -# Copy this file to .env and load it in your shell or CI before building. -# Pishoo does not load .env files automatically. - -DHTTP_BOOTSTRAP_URL=https://bootstrap.genmeta.net +# Optional shell/CI overrides. Cargo defaults to the same production values. +DHTTP_BOOTSTRAP_URL=https://bootstrap.genmeta.net:20002 +DHTTP_CERT_SERVER_URL=https://api.genmeta.net:4433 DHTTP_H3_DNS_SERVER=https://ddns.genmeta.net:4433 -DHTTP_MDNS_SERVICE="_dhttp.local" -DHTTP_ROOT_CA_PEM="-----BEGIN CERTIFICATE----- -MIICVTCCAdqgAwIBAgIUcBI2Xq2ZS+SYgLrPjqN0eqCf5a8wCgYIKoZIzj0EAwMw -WTELMAkGA1UEBhMCQ04xETAPBgNVBAgMCEhvbmdLb25nMRwwGgYDVQQKDBNHZW5t -ZXRhIEVDQyBSb290IENBMRkwFwYDVQQDDBByb290Lmdlbm1ldGEubmV0MB4XDTI1 -MDQyMTA1MDEwNFoXDTQ1MDQxNjA1MDEwNFowWTELMAkGA1UEBhMCQ04xETAPBgNV -BAgMCEhvbmdLb25nMRwwGgYDVQQKDBNHZW5tZXRhIEVDQyBSb290IENBMRkwFwYD -VQQDDBByb290Lmdlbm1ldGEubmV0MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEA81Y -m6nFz+c58mDquLk2KhZufUejy6js+Ru9AsrRyFIHCHbTQSOlkK+QXZMqRBkBSA+n -Gy0mnkf0zeXj9NFqTYveIrgDtXv/WoD3eadAyD8CAu5O0/XsyQGaP/bS+sEeo2Mw -YTAdBgNVHQ4EFgQUXgbKWqQlEhGkIdqW+slZ0wTtCS8wHwYDVR0jBBgwFoAUXgbK -WqQlEhGkIdqW+slZ0wTtCS8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AQYwCgYIKoZIzj0EAwMDaQAwZgIxALCdmweGeSRn+B5IblH1emNjT2kw3CKO8l1g -jYBag7kqGK8ZGjwx9xpC5QMn3hw8qQIxAKKnzCktBfssKJ5HMEHuWCg0rw/FHmpu -PrFCruzsQiLcBa+GAP2O7Qbl+XAlK+MNiA== ------END CERTIFICATE-----" +DHTTP_MDNS_SERVICE=_dhttp.local diff --git a/Cargo.toml b/Cargo.toml index 22497a4..d7555f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,10 +41,10 @@ tracing-subscriber = { version = "0.3", default-features = false, features = ["f # Keep same-repository workspace members as path dependencies, except staged # dhttp-identity releases, which resolve through crates.io to avoid duplicate # identity types with h3x/ddns in the formal release graph. -dhttp-identity = "0.3.0-beta.1" +dhttp-identity = "0.3.0-beta.2" dhttp-home = { path = "home", version = "0.5.0-beta.1" } -dquic = { git = "https://github.com/genmeta/dquic.git", branch = "main", default-features = false } -ddns = { package = "dyns", git = "https://github.com/genmeta/ddns.git", branch = "fix/ddns-bootstrap", features = [ +dquic = { version = "0.7.0-beta.6", default-features = false } +ddns = { package = "dyns", version = "0.7.0-beta.2", features = [ "resolvers", "publishers", "h3", @@ -52,7 +52,7 @@ ddns = { package = "dyns", git = "https://github.com/genmeta/ddns.git", branch = "mdns", "dquic-network", ] } -h3x = { git = "https://github.com/genmeta/h3x.git", branch = "fix/h3x-reliability", features = [ +h3x = { version = "0.6.0-beta.4", features = [ "dquic", ] } dhttp = { path = "dhttp", version = "0.6.0-beta.5" } diff --git a/api/js/index.d.ts b/api/js/index.d.ts index 9ba268f..7575c79 100644 --- a/api/js/index.d.ts +++ b/api/js/index.d.ts @@ -12,7 +12,7 @@ export type FetchHandler = (request: DhttpRequest) => Response | Promise void | Promise; -export type CertificateChainKind = "primary" | "secondary"; +export type CertificateChainKind = "client" | "client and server"; export interface CertificateChainKey { sequence: number; diff --git a/api/src/certificate.rs b/api/src/certificate.rs index 18a4b4a..fbcb34e 100644 --- a/api/src/certificate.rs +++ b/api/src/certificate.rs @@ -17,7 +17,7 @@ impl From for DhttpSubjectKeyIden value: value.to_string(), chain: CertificateChainKey { sequence: value.chain().sequence().get(), - kind: value.chain().kind().as_str().to_owned(), + kind: value.chain().usage().as_str().to_owned(), }, owner_hash: value.owner_hash().as_str().to_owned(), } diff --git a/dhttp/build.rs b/dhttp/build.rs index fb46e3a..4fdcd85 100644 --- a/dhttp/build.rs +++ b/dhttp/build.rs @@ -6,23 +6,19 @@ const BOOTSTRAP_URL_ENV: &str = "DHTTP_BOOTSTRAP_URL"; const DEFAULT_BOOTSTRAP_URL: &str = "https://bootstrap.genmeta.net:20002"; const DEFAULT_ROOT_CA_PEM: &str = "\ -----BEGIN CERTIFICATE-----\n\ -MIIDKTCCAhGgAwIBAgIUHNScq6R2U5QYUzxkEkNDaOJt4yMwDQYJKoZIhvcNAQEL\n\ -BQAwHDEaMBgGA1UEAwwRZGh0dHAuZXhhbXBsZS5uZXQwHhcNMjYwNjA0MTE0NjI1\n\ -WhcNMzYwNjAxMTE0NjI1WjAcMRowGAYDVQQDDBFkaHR0cC5leGFtcGxlLm5ldDCC\n\ -ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANCHw/okfV4RDIN5ws0IMGi1\n\ -c1TTQAklaBynw8neIM+ZniGLOluRLvYATY4xla/ayjYdzXmel+4hdaG5pN6Ggdgm\n\ -PhIj9hpsNKOjjkBHtdnDg/Dfc/L+ElRDPMNosvVN2L/G4yDBso5SGJUlFE7Fww3x\n\ -izrTCCbR4lofwZAnUZAtvnX3KGgvgRtg0VdgvpWJ1JOnwQYm1qU6ljFG+72418SW\n\ -Htn2A8SRGSO1Im1W1QMY4OJtAVwfi/XwwN9ifWWPwcl4OGV/MXBLUQRF4gzgFC+1\n\ -8EG5jKCZtoJcY3FY+LF1DMkdbmE6TOddycev0HWNBPQtSMsxTo9aYrApKnOk5ekC\n\ -AwEAAaNjMGEwHQYDVR0OBBYEFDe4aYWivTEYa1hkBy7WaZOVY/S6MB8GA1UdIwQY\n\ -MBaAFDe4aYWivTEYa1hkBy7WaZOVY/S6MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P\n\ -AQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4IBAQAEpkndNISW40WGLJa9QVYcBAyP\n\ -Ou+vdoRuch+/jeIyM4Lau2yBYu1nyneLmVpJGTCFHxcTrYCmUqJKnkWbHPXoDY27\n\ -MPGih8GkiIW057JUbcyktKZqzzmsAoCDz6kLp76BpJiLViiSctufWKeHt8Rm/n66\n\ -dI2XmJcJxJ9Xs+QkziinZbiUKQxiDBTgkTT4kxhMESlb4iV9YNpIvW4m8fqCaxtl\n\ -ECyJBmqcGIp3bEYchL4hs8I3jbT3VXykjCK/FU/wLWSCIMzhsV4U7JxI0xKGziPU\n\ -p83DC7/YpVF7avt05Mwb0n3RhKSmDMLnfWs61zTDQu0T/IYBbr+PvVMsw+W4\n\ +MIICVzCCAd2gAwIBAgIUe8kwBACY6f+MAzdCBVPmq4p+CiswCgYIKoZIzj0EAwMw\n\ +WTELMAkGA1UEBhMCQ04xETAPBgNVBAgMCEhvbmdLb25nMRwwGgYDVQQKDBNHZW5t\n\ +ZXRhIEVDQyBSb290IENBMRkwFwYDVQQDDBByb290Lmdlbm1ldGEubmV0MB4XDTI2\n\ +MDcxMzEzMDQyOFoXDTQ2MDcxMzEzMDQyOFowWTELMAkGA1UEBhMCQ04xETAPBgNV\n\ +BAgMCEhvbmdLb25nMRwwGgYDVQQKDBNHZW5tZXRhIEVDQyBSb290IENBMRkwFwYD\n\ +VQQDDBByb290Lmdlbm1ldGEubmV0MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEO+zm\n\ +ZYL0LaqTKf7mW4tnRWeNop1p8f2ZsexhAl23GHkHwLjCihhQzBCZ8VMRPAdVcEIS\n\ +XcGY/U6+Z1IAYCRG0tdsUCXHXxzvDY0I9FZqZw1Xo94gkHnNe7mTu/jCQg3Xo2Yw\n\ +ZDAdBgNVHQ4EFgQUq1SsSWDnp0G5v5/hWi9CC7eWDTwwHwYDVR0jBBgwFoAUq1Ss\n\ +SWDnp0G5v5/hWi9CC7eWDTwwEgYDVR0TAQH/BAgwBgEB/wIBATAOBgNVHQ8BAf8E\n\ +BAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIwK9GqxdmRHJw7iB0z/b/WgzBv2jb7OmFS\n\ +uVPA+6ZNApjYXCZUOVQFC60KUUV7yW53AjEA5lLrdXxdGNSIuLe1h/A+v/vRrYtt\n\ +132Jzh+LkKBHdC1wcvDKjk2ZQG5WySly6VMp\n\ -----END CERTIFICATE-----\n"; fn main() { @@ -132,7 +128,7 @@ mod tests { } #[test] - fn placeholder_root_ca_is_pem_certificate() { + fn default_root_ca_is_pem_certificate() { assert!(DEFAULT_ROOT_CA_PEM.starts_with("-----BEGIN CERTIFICATE-----")); assert!(DEFAULT_ROOT_CA_PEM.ends_with("-----END CERTIFICATE-----\n")); } diff --git a/dhttp/src/endpoint.rs b/dhttp/src/endpoint.rs index c2c7759..498d2cf 100644 --- a/dhttp/src/endpoint.rs +++ b/dhttp/src/endpoint.rs @@ -92,7 +92,7 @@ pub enum CreateEndpointPublicationLoopError { pub const BOOTSTRAP_URL: &str = crate::bootstrap::DHTTP_BOOTSTRAP_URL; /// Bootstrap authority passed to the NAT traversal layer. -pub(crate) const BOOTSTRAP_AUTHORITY: &str = crate::bootstrap::DHTTP_BOOTSTRAP_AUTHORITY; +pub const BOOTSTRAP_AUTHORITY: &str = crate::bootstrap::DHTTP_BOOTSTRAP_AUTHORITY; fn normalize_bind(bind: Arc>) -> Arc> { if bind.is_empty() { diff --git a/log/tests/certificate.rs b/log/tests/certificate.rs index f75fd91..bc92465 100644 --- a/log/tests/certificate.rs +++ b/log/tests/certificate.rs @@ -1,5 +1,7 @@ use chrono::{FixedOffset, TimeZone}; -use dhttp_identity::certificate::{CertificateChainKey, CertificateChainKind, CertificateSequence}; +use dhttp_identity::certificate::{ + CertificateChainKey, CertificateSequence, CertificateUsage as IdentityCertificateUsage, +}; use dhttp_log::cert::{ CertificateAction, CertificateIssuer, CertificateLogRecord, CertificateUsage, DefaultCertificateFormatter, Sha256Fingerprint, @@ -26,7 +28,7 @@ fn timestamp( fn chain() -> CertificateChainKey { CertificateChainKey::new( CertificateSequence::try_from(0_u32).unwrap(), - CertificateChainKind::Primary, + IdentityCertificateUsage::ClientAndServer, ) } @@ -86,7 +88,7 @@ fn default_certificate_line_formats_sha256_fingerprint_and_existing_chain_key() assert_eq!( line.as_bytes(), - b"[13/Jul/2026:08:20:31 +0000] APPLY \"Genmeta Tech Limited\" \"client only\" primary:0 [13/Jul/2027:08:20:30 +0000] \"sha256:0000000000000000000000000000000000000000000000000000000000000000\"\n" + b"[13/Jul/2026:08:20:31 +0000] APPLY \"Genmeta Tech Limited\" \"client only\" client and server:0 [13/Jul/2027:08:20:30 +0000] \"sha256:0000000000000000000000000000000000000000000000000000000000000000\"\n" ); } From e06d8192bacb169ae60c6face736245bc79e2e4d Mon Sep 17 00:00:00 2001 From: metah3m Date: Tue, 4 Aug 2026 10:58:17 +0800 Subject: [PATCH 7/8] chore(release): prepare dhttp 0.6.0 --- Cargo.toml | 18 +++++++++--------- access/Cargo.toml | 2 +- api/package-lock.json | 4 ++-- api/package.json | 2 +- home/Cargo.toml | 2 +- identity/Cargo.toml | 2 +- identity/src/certificate.rs | 18 ++++++++++++------ identity/src/identity.rs | 2 +- log/Cargo.toml | 2 +- 9 files changed, 29 insertions(+), 23 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d7555f0..11e02f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "3" members = ["dhttp", "identity", "home", "api", "access", "log"] [workspace.package] -version = "0.6.0-beta.5" +version = "0.6.0" edition = "2024" license = "Apache-2.0" repository = "https://github.com/genmeta/dhttp" @@ -41,10 +41,10 @@ tracing-subscriber = { version = "0.3", default-features = false, features = ["f # Keep same-repository workspace members as path dependencies, except staged # dhttp-identity releases, which resolve through crates.io to avoid duplicate # identity types with h3x/ddns in the formal release graph. -dhttp-identity = "0.3.0-beta.2" -dhttp-home = { path = "home", version = "0.5.0-beta.1" } -dquic = { version = "0.7.0-beta.6", default-features = false } -ddns = { package = "dyns", version = "0.7.0-beta.2", features = [ +dhttp-identity = "0.3.0" +dhttp-home = { path = "home", version = "0.5.0" } +dquic = { version = "0.7.0", default-features = false } +ddns = { package = "dyns", version = "0.7.0", features = [ "resolvers", "publishers", "h3", @@ -52,9 +52,9 @@ ddns = { package = "dyns", version = "0.7.0-beta.2", features = [ "mdns", "dquic-network", ] } -h3x = { version = "0.6.0-beta.4", features = [ +h3x = { version = "0.6.0", features = [ "dquic", ] } -dhttp = { path = "dhttp", version = "0.6.0-beta.5" } -dhttp-access = { path = "access", version = "0.4.0-beta.2" } -dhttp-log = { path = "log", version = "0.1.0-beta.1" } +dhttp = { path = "dhttp", version = "0.6.0" } +dhttp-access = { path = "access", version = "0.4.0" } +dhttp-log = { path = "log", version = "0.1.0" } diff --git a/access/Cargo.toml b/access/Cargo.toml index b7ce8ef..f15ca3e 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 = "0.4.0-beta.2" +version = "0.4.0" edition.workspace = true license.workspace = true repository.workspace = true diff --git a/api/package-lock.json b/api/package-lock.json index 3be1460..01f49aa 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -1,12 +1,12 @@ { "name": "@genmeta/dhttp", - "version": "0.6.0-beta.5", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@genmeta/dhttp", - "version": "0.6.0-beta.5", + "version": "0.6.0", "devDependencies": { "@napi-rs/cli": "^3.3.5" } diff --git a/api/package.json b/api/package.json index 10c6b1a..a8bf8a3 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "@genmeta/dhttp", - "version": "0.6.0-beta.5", + "version": "0.6.0", "description": "The True Internet", "license": "Apache-2.0", "homepage": "https://dhttp.net/", diff --git a/home/Cargo.toml b/home/Cargo.toml index cc41830..5762afe 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.5.0-beta.1" +version = "0.5.0" edition.workspace = true license.workspace = true repository.workspace = true diff --git a/identity/Cargo.toml b/identity/Cargo.toml index 0e8c190..fdea346 100644 --- a/identity/Cargo.toml +++ b/identity/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "dhttp-identity" description = "Identity primitives for DHttp" -version = "0.3.0-beta.2" +version = "0.3.0" edition.workspace = true license.workspace = true repository.workspace = true diff --git a/identity/src/certificate.rs b/identity/src/certificate.rs index 3b380f9..1ce0a25 100644 --- a/identity/src/certificate.rs +++ b/identity/src/certificate.rs @@ -93,8 +93,8 @@ impl CertificateUsage { pub fn kind_flag(self) -> &'static str { match self { - Self::ClientOnly => "0", - Self::ClientAndServer => "1", + Self::ClientOnly => "1", + Self::ClientAndServer => "0", } } } @@ -227,8 +227,8 @@ impl FromStr for DhttpSubjectKeyIdentifier { let sequence = CertificateSequence::try_from(sequence) .context(invalid_dhttp_subject_key_identifier::SequenceRangeSnafu)?; let usage = match usage { - "0" => CertificateUsage::ClientOnly, - "1" => CertificateUsage::ClientAndServer, + "0" => CertificateUsage::ClientAndServer, + "1" => CertificateUsage::ClientOnly, _ => return invalid_dhttp_subject_key_identifier::KindFlagSnafu.fail(), }; let owner_hash = OwnerHash::try_from(owner_hash) @@ -309,6 +309,12 @@ mod tests { assert_eq!(secondary.to_string(), "client:2"); } + #[test] + fn certificate_usage_preserves_certserver_kind_flags() { + assert_eq!(CertificateUsage::ClientAndServer.kind_flag(), "0"); + assert_eq!(CertificateUsage::ClientOnly.kind_flag(), "1"); + } + #[test] fn rejects_out_of_range_subject_key_identifier_sequence() { let error = format!("{}:0:{OWNER_HASH}", i32::MAX as u64 + 1) @@ -324,14 +330,14 @@ mod tests { #[test] fn parses_canonical_dhttp_subject_key_identifier() { let ski = DhttpSubjectKeyIdentifier::try_from_subject_key_identifier_bytes( - format!("7:1:{OWNER_HASH}").as_bytes(), + format!("7:0:{OWNER_HASH}").as_bytes(), ) .unwrap(); assert_eq!(ski.chain().sequence().get(), 7); assert_eq!(ski.chain().usage(), CertificateUsage::ClientAndServer); assert_eq!(ski.owner_hash().as_str(), OWNER_HASH); - assert_eq!(ski.to_string(), format!("7:1:{OWNER_HASH}")); + assert_eq!(ski.to_string(), format!("7:0:{OWNER_HASH}")); } #[test] diff --git a/identity/src/identity.rs b/identity/src/identity.rs index 861daf0..be40d09 100644 --- a/identity/src/identity.rs +++ b/identity/src/identity.rs @@ -572,7 +572,7 @@ mod tests { let dhttp = identity .dhttp_subject_key_identifier() .expect("extract dhttp ski"); - assert_eq!(dhttp.chain().usage(), CertificateUsage::ClientOnly); + assert_eq!(dhttp.chain().usage(), CertificateUsage::ClientAndServer); assert_eq!(dhttp.chain().sequence().get(), 0); } diff --git a/log/Cargo.toml b/log/Cargo.toml index 9ec0922..03fe045 100644 --- a/log/Cargo.toml +++ b/log/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "dhttp-log" description = "Typed DHTTP domain log records and formatting" -version = "0.1.0-beta.1" +version = "0.1.0" edition = "2024" license.workspace = true repository.workspace = true From 6f5bebae952567880f2cc0ecb2034e0616775c2b Mon Sep 17 00:00:00 2001 From: metah3m Date: Tue, 4 Aug 2026 12:12:29 +0800 Subject: [PATCH 8/8] feat(log): support identity-based access records --- log/src/access/formatter.rs | 28 +++++++++++++++++++++++++++- log/tests/access.rs | 21 +++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/log/src/access/formatter.rs b/log/src/access/formatter.rs index ec3b0f5..a5fb43c 100644 --- a/log/src/access/formatter.rs +++ b/log/src/access/formatter.rs @@ -15,9 +15,30 @@ pub struct DefaultAccessFormatter; impl DefaultAccessFormatter { /// Formats one access record using the built-in compact V1 representation. pub fn format(record: &AccessLogRecord) -> Result { + Self::format_with_client(record, FormattedClient::Address) + } + + /// Formats one access record using the authenticated client identity as the + /// remote host and omitting the transport address. + pub fn format_with_client_identity( + record: &AccessLogRecord, + client_identity: Option<&str>, + ) -> Result { + Self::format_with_client(record, FormattedClient::Identity(client_identity)) + } + + fn format_with_client( + record: &AccessLogRecord, + client: FormattedClient<'_>, + ) -> Result { let convention = CompactConvention::default(); let mut builder = RecordBuilder::new(); - builder.element(&convention, &record.client)?; + match client { + FormattedClient::Address => builder.element(&convention, &record.client)?, + FormattedClient::Identity(identity) => { + builder.element(&convention, &Optional(identity.map(Text)))?; + } + } builder.literal(b" ")?; builder.element(&convention, &MissingField)?; builder.literal(b" ")?; @@ -38,6 +59,11 @@ impl DefaultAccessFormatter { } } +enum FormattedClient<'a> { + Address, + Identity(Option<&'a str>), +} + struct MissingField; impl FormatElement for MissingField { diff --git a/log/tests/access.rs b/log/tests/access.rs index 6803a8f..ee2a172 100644 --- a/log/tests/access.rs +++ b/log/tests/access.rs @@ -53,6 +53,27 @@ fn default_access_format_is_combined_without_reserved_columns() { assert!(!line.as_bytes().windows(5).any(|bytes| bytes == b"token")); } +#[test] +fn authenticated_client_identity_replaces_the_transport_address() { + let line = + DefaultAccessFormatter::format_with_client_identity(&access_fixture(), Some("reimu.pilot")) + .unwrap(); + + assert_eq!( + line.as_bytes(), + b"reimu.pilot - - [13/Jul/2026:16:20:31 +0800] \"GET /assets/a.css HTTP/3\" 200 1432 \"https://example.test/\" \"ExampleAgent/1.0\"\n" + ); +} + +#[test] +fn identity_format_omits_the_transport_address_for_anonymous_clients() { + let line = + DefaultAccessFormatter::format_with_client_identity(&access_fixture(), None).unwrap(); + + assert!(line.as_bytes().starts_with(b"- - - [")); + assert!(!line.as_bytes().starts_with(b"192.0.2.10")); +} + #[test] fn request_header_values_are_built_from_the_named_allowlist_only() { let mut headers = HeaderMap::new();