Skip to content
10 changes: 8 additions & 2 deletions .github/workflows/publish-crates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ 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

Expand Down Expand Up @@ -126,10 +132,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

Expand Down
10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -39,17 +39,17 @@ 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" }
ddns = { package = "dyns", version = "0.4.0", features = [
dhttp-home = { path = "home", version = "0.3.0" }
ddns = { package = "dyns", version = "0.5.0", features = [
"resolvers",
"publishers",
"h3",
"http",
"mdns",
"dquic-network",
] }
h3x = { version = "0.4.0", 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" }
2 changes: 1 addition & 1 deletion access/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion access/src/db/identity.rs
Original file line number Diff line number Diff line change
@@ -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};
4 changes: 2 additions & 2 deletions access/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))]
Expand All @@ -52,7 +52,7 @@ pub enum AccessDbError {
}

pub fn load_dhttp_home() -> Result<DhttpHome, AccessDbError> {
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 {
Expand Down
4 changes: 2 additions & 2 deletions api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
@@ -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/",
Expand Down
2 changes: 1 addition & 1 deletion api/src/home.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub struct IdentityProfile(dhttp::home::identity::IdentityProfile);

impl DhttpHome {
pub fn load() -> Result<Self> {
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))
}
Expand Down
4 changes: 2 additions & 2 deletions dhttp/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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\
Expand Down Expand Up @@ -84,7 +84,7 @@ mod tests {

assert_eq!(
env_or_default(&name, DEFAULT_STUN_SERVER),
"dhttp.example.net:1"
"stun.dhttp.example.net"
);
}

Expand Down
199 changes: 183 additions & 16 deletions dhttp/src/ddns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,46 @@ impl DhttpDnsPlan {
}

type DeferredEndpointResolver = resolvers::deferred::DeferredResolver<resolvers::Resolvers>;
type EndpointH3Client = Arc<H3Endpoint<EndpointH3Connector, crate::dquic::connection::Connection>>;

#[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<Arc<Self::Connection>, 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<Option<Self::LocalAuthority>, 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))]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
};
Expand Down Expand Up @@ -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) => {
Expand All @@ -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<EndpointH3Clients, BuildQuicEndpointWithDnsError> {
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<Network>,
bind: Arc<Vec<BindPattern>>,
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,
Expand Down Expand Up @@ -369,8 +455,8 @@ fn h3_resolver_for_network(

fn h3_resolver_for_endpoint(
h3_dns_server: &str,
h3: Arc<H3Endpoint<QuicEndpoint, crate::dquic::connection::Connection>>,
) -> Result<resolvers::H3Resolver<QuicEndpoint>, BuildQuicEndpointWithDnsError> {
h3: EndpointH3Client,
) -> Result<resolvers::H3Resolver<EndpointH3Connector>, BuildQuicEndpointWithDnsError> {
resolvers::H3Resolver::from_endpoint(h3_dns_server, h3)
.context(build_quic_endpoint_with_dns_error::InvalidH3DnsServerSnafu)
}
Expand Down Expand Up @@ -435,6 +521,7 @@ fn has_system_dns(operations: &[DhttpDnsOp]) -> bool {

#[cfg(test)]
mod tests {
use std::str::FromStr;
use std::{
fmt,
sync::{
Expand Down Expand Up @@ -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()
);
}
}
Loading
Loading