Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions .github/workflows/publish-crates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,20 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Install Rust stable toolchain
- name: Install Rust nightly toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: nightly
components: rustfmt, clippy

- name: Check formatting
run: cargo +nightly fmt -- --check

- name: Run clippy
run: cargo +nightly clippy --all-targets --all-features -- -D warnings

- name: Test crate
run: cargo test --all-features --all-targets
run: cargo +nightly test --all-features --all-targets

- name: Authenticate to crates.io
if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v')
Expand All @@ -47,7 +56,7 @@ jobs:
fi

package_name=dyns
package_version="$(cargo metadata --no-deps --format-version 1 | python3 -c 'import json, sys; print(json.load(sys.stdin)["packages"][0]["version"])')"
package_version="$(cargo +nightly metadata --no-deps --format-version 1 | python3 -c 'import json, sys; print(json.load(sys.stdin)["packages"][0]["version"])')"

crate_state="$(
python3 - <<'PY' "$package_name" "$package_version"
Expand Down Expand Up @@ -97,9 +106,9 @@ jobs:

if [[ "$mode" == "dry-run" ]]; then
echo "dry-run $package_name $package_version"
cargo publish --dry-run --locked
cargo +nightly publish --dry-run
exit 0
fi

echo "publish $package_name $package_version"
cargo publish --locked
cargo +nightly publish
8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "dyns"
description = "DNS discovery and resolver support for DHTTP applications"
version = "0.4.0"
version = "0.5.0"
edition = "2024"
license = "Apache-2.0"
repository = "https://github.com/genmeta/ddns"
Expand All @@ -20,7 +20,7 @@ bitfield-struct = "0.13"
bytes = "1"
dashmap = { version = "6", optional = true }
dhttp-identity = "0.2.0"
dquic = "0.5.1"
dquic = "0.6.0"
flume = { version = "0.12", optional = true }
futures = "0.3"
libc = { version = "0.2", optional = true }
Expand All @@ -47,7 +47,7 @@ tokio = { version = "1", features = [
tracing = "0.1"
x509-parser = { version = "0.18", features = ["verify"] }

h3x = { version = "0.4.0", default-features = false, optional = true }
h3x = { version = "0.5.0", default-features = false, optional = true }
http = { version = "1", optional = true }
http-body = { version = "1", optional = true }
http-body-util = { version = "0.1", optional = true }
Expand Down Expand Up @@ -80,7 +80,7 @@ mdns = ["dep:dashmap", "dep:flume", "dep:libc", "dep:socket2"]

[dev-dependencies]
clap = { version = "4", features = ["derive"] }
h3x = { version = "0.4.0", default-features = false, features = ["dquic"] }
h3x = { version = "0.5.0", default-features = false, features = ["dquic"] }
shellexpand = "3"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

Expand Down
81 changes: 76 additions & 5 deletions src/core/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@
/// +-----------+ | u32 BE | ... | u32 BE | ... |
/// +-----------+------+-----------+------+
/// ```
use std::borrow::Borrow;

use bytes::BufMut;
use dhttp_identity::certificate::CertificateChainKey;
use nom::{IResult, bytes::streaming::take, number::streaming::be_u32};
use rustls::pki_types::CertificateDer;

use crate::core::signature::SignatureFields;

Expand Down Expand Up @@ -48,6 +52,17 @@ impl ResponseRecord {
let digest = digest(&SHA256, &self.cert);
Some(digest.as_ref().iter().map(|b| format!("{b:02x}")).collect())
}

pub fn publisher_certificate_chain_key(&self) -> Option<CertificateChainKey> {
if self.cert.is_empty() {
return None;
}

let cert = CertificateDer::from(self.cert.clone());
dhttp_identity::identity::extract_dhttp_subject_key_identifier(std::slice::from_ref(&cert))
.ok()
.map(|ski| ski.chain().clone())
}
}

/// HTTP response body carrying zero or more DNS records.
Expand Down Expand Up @@ -86,6 +101,26 @@ impl MultiResponse {
buf.put_multi_response(self);
buf
}

pub fn encode_records<I, R>(records: I) -> Vec<u8>
where
I: IntoIterator<Item = R>,
R: Borrow<ResponseRecord>,
{
let mut buf = Vec::new();
buf.put_u32(0);

let mut count = 0u32;
for record in records {
count = count
.checked_add(1)
.expect("multi response record count exceeds u32 range");
put_response_record(&mut buf, record.borrow());
}

buf[..4].copy_from_slice(&count.to_be_bytes());
buf
}
}

pub trait WriteMultiResponse {
Expand All @@ -96,15 +131,19 @@ impl<B: BufMut> WriteMultiResponse for B {
fn put_multi_response(&mut self, response: &MultiResponse) {
self.put_u32(response.records.len() as u32);
for record in &response.records {
put_field(self, &record.signature_fields.content_digest);
put_field(self, &record.signature_fields.signature_input);
put_field(self, &record.signature_fields.signature);
put_field(self, &record.dns);
put_field(self, &record.cert);
put_response_record(self, record);
}
}
}

fn put_response_record<B: BufMut>(buf: &mut B, record: &ResponseRecord) {
put_field(buf, &record.signature_fields.content_digest);
put_field(buf, &record.signature_fields.signature_input);
put_field(buf, &record.signature_fields.signature);
put_field(buf, &record.dns);
put_field(buf, &record.cert);
}

fn put_field<B: BufMut>(buf: &mut B, value: &[u8]) {
buf.put_u32(value.len() as u32);
buf.put_slice(value);
Expand Down Expand Up @@ -164,4 +203,36 @@ mod tests {
assert!(remain.is_empty());
assert_eq!(decoded, response);
}

#[test]
fn encode_records_matches_multi_response_encoding_for_owned_records() {
let records = [
ResponseRecord::unsigned(vec![1, 2, 3], vec![4, 5]),
ResponseRecord::new(
SignatureFields {
content_digest: b"sha-256=:abc:".to_vec(),
signature_input: b"dns=(\"content-digest\")".to_vec(),
signature: b"dns=:sig:".to_vec(),
},
vec![6, 7],
Vec::new(),
),
];
let response = MultiResponse::new(records.clone());

assert_eq!(MultiResponse::encode_records(records), response.encode());
}

#[test]
fn encode_records_matches_multi_response_encoding_for_borrowed_records() {
let response = MultiResponse::new([
ResponseRecord::unsigned(vec![1, 2, 3], vec![4, 5]),
ResponseRecord::unsigned(vec![6, 7], Vec::new()),
]);

assert_eq!(
MultiResponse::encode_records(response.records.iter()),
response.encode()
);
}
}
2 changes: 1 addition & 1 deletion src/h3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ mod tests {
));
let resolver = H3Resolver::from_endpoint(DHTTP_H3_DNS_SERVER, endpoint).unwrap();
resolver.cache.insert_positive(
"nat.genmeta.net",
"nat.genmeta.net:20004",
vec![EndpointAddr::direct("192.0.2.10:21000".parse().unwrap())],
);

Expand Down
16 changes: 16 additions & 0 deletions src/h3/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,20 @@ mod tests {

assert!(cache.negative_hit("missing.dhttp.net"));
}

#[test]
fn positive_cache_hit_keeps_selector_entries_separate() {
let cache = LookupCache::default();
cache.insert_positive("demo.dhttp.net", vec![endpoint("192.0.2.10:4433")]);
cache.insert_positive("demo.dhttp.net:1", vec![endpoint("192.0.2.11:4433")]);

assert_eq!(
cache.positive_hit("demo.dhttp.net").unwrap(),
vec![endpoint("192.0.2.10:4433")]
);
assert_eq!(
cache.positive_hit("demo.dhttp.net:1").unwrap(),
vec![endpoint("192.0.2.11:4433")]
);
}
}
Loading
Loading