feat(core): add QUIC TLS admission transport

This commit is contained in:
sechmachine
2026-08-12 19:08:32 +07:00
parent 7947ebcc75
commit 111092becb
11 changed files with 2304 additions and 38 deletions
+147 -17
View File
@@ -1,5 +1,5 @@
use serde::de::DeserializeOwned;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use crate::error::{CoreError, Result};
@@ -203,6 +203,8 @@ fn timestamp(value: &str, exact_seconds: bool) -> Option<Timestamp> {
fn valid_dns_name(value: &str) -> bool {
bounded(value, 1, 253)
&& value.parse::<std::net::IpAddr>().is_err()
&& !uuid_shaped(value)
&& value.split('.').all(|label| {
bounded(label, 1, 63)
&& !label.starts_with('-')
@@ -213,7 +215,15 @@ fn valid_dns_name(value: &str) -> bool {
})
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
fn uuid_shaped(value: &str) -> bool {
value.len() == 36
&& value.bytes().enumerate().all(|(index, byte)| match index {
8 | 13 | 18 | 23 => byte == b'-',
_ => byte.is_ascii_hexdigit(),
})
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CapabilityProfile {
transport: String,
@@ -432,6 +442,35 @@ impl ConnectionManifest {
}
Ok(())
}
pub(crate) fn addresses(&self) -> &[String] {
&self.gateway.addresses
}
pub(crate) fn public_identity(&self) -> &str {
&self.gateway.public_identity
}
pub(crate) fn admission(
&self,
client_nonce: String,
device_signature: String,
capabilities: CapabilityProfile,
) -> Result<TunnelAdmissionRequest> {
let request = TunnelAdmissionRequest {
version: "1".to_owned(),
session_id: self.session_id.clone(),
gateway_id: self.gateway.id.clone(),
audience: self.grant.audience.clone(),
grant: self.grant.opaque_value.clone(),
reconnect_sequence: self.reconnect_sequence,
client_nonce,
device_signature,
capabilities,
};
request.validate()?;
Ok(request)
}
}
#[derive(Clone, Debug, Deserialize)]
@@ -477,9 +516,17 @@ impl NativeTunnelCredential {
}
Ok(())
}
pub(crate) fn certificate_chain_pem(&self) -> &str {
&self.certificate_chain_pem
}
pub(crate) fn trust_bundle_pem(&self) -> &str {
&self.trust_bundle_pem
}
}
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TunnelAdmissionRequest {
version: String,
@@ -501,23 +548,25 @@ impl TunnelAdmissionRequest {
/// Returns `invalid_argument` for malformed, unknown, or out-of-bound data.
pub fn decode(bytes: &[u8]) -> Result<Self> {
let request: Self = decode_strict(bytes, MAX_ADMISSION_JSON_BYTES)?;
if request.version != "1"
|| !bounded(&request.session_id, 1, 128)
|| !bounded(&request.gateway_id, 1, 128)
|| !bounded(&request.audience, 1, 256)
|| !bounded(&request.grant, 43, 256)
|| !bounded(&request.client_nonce, 16, 128)
|| request.device_signature.len() != 86
|| !matches!(
raw_base64url_decoded_len(&request.client_nonce),
Some(12..=96)
)
|| raw_base64url_decoded_len(&request.device_signature) != Some(64)
request.validate()?;
Ok(request)
}
fn validate(&self) -> Result<()> {
if self.version != "1"
|| !bounded(&self.session_id, 1, 128)
|| !bounded(&self.gateway_id, 1, 128)
|| !bounded(&self.audience, 1, 256)
|| !bounded(&self.grant, 43, 256)
|| !bounded(&self.client_nonce, 16, 128)
|| self.device_signature.len() != 86
|| !matches!(raw_base64url_decoded_len(&self.client_nonce), Some(12..=96))
|| raw_base64url_decoded_len(&self.device_signature) != Some(64)
{
return Err(CoreError::InvalidArgument);
}
request.capabilities.validate()?;
Ok(request)
self.capabilities.validate()?;
Ok(())
}
#[must_use]
@@ -561,6 +610,20 @@ pub struct ClientSessionAuthority {
capabilities: CapabilityProfile,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct StableError {
version: String,
code: String,
message: String,
retryable: bool,
}
pub(crate) struct DecodedStableError {
pub(crate) error: CoreError,
pub(crate) retryable: bool,
}
impl ClientSessionAuthority {
/// Strictly decodes and validates a provider-free RC5 client authority.
///
@@ -608,4 +671,71 @@ impl ClientSessionAuthority {
}
Ok(())
}
#[must_use]
pub fn session_id(&self) -> &str {
&self.session_id
}
}
pub(crate) fn decode_stable_error(bytes: &[u8]) -> Result<DecodedStableError> {
let stable: StableError = decode_strict(bytes, MAX_ADMISSION_JSON_BYTES)?;
if stable.version != "1" || !bounded(&stable.code, 1, 128) || !bounded(&stable.message, 1, 512)
{
return Err(CoreError::Protocol);
}
let error = match stable.code.as_str() {
"expired_grant" => CoreError::Expired,
"admission_rejected"
| "gateway_draining"
| "invalid_authority"
| "no_capability_overlap"
| "wrong_gateway"
| "provider_work_unavailable"
| "clipboard_audit_unavailable" => CoreError::AuthorityRejected,
"provider_identity_rejected"
| "provider_malformed"
| "provider_timeout"
| "provider_unavailable"
| "provider_state_unavailable" => CoreError::Transport,
"invalid_hello" => CoreError::Protocol,
_ => return Err(CoreError::Protocol),
};
Ok(DecodedStableError {
error,
retryable: stable.retryable,
})
}
#[cfg(test)]
mod stable_error_tests {
use super::decode_stable_error;
use crate::error::CoreError;
#[test]
fn stable_error_uses_exact_rc5_bounds_and_preserves_retryability() {
let message = "m".repeat(512);
let bytes = serde_json::to_vec(&serde_json::json!({
"version": "1",
"code": "gateway_draining",
"message": message,
"retryable": true,
}))
.expect("encode stable error");
let decoded = decode_stable_error(&bytes).expect("RC5 stable error");
assert_eq!(decoded.error, CoreError::AuthorityRejected);
assert!(decoded.retryable);
for invalid in [
serde_json::json!({"version":"1","code":"gateway_draining","message":"","retryable":true}),
serde_json::json!({"version":"1","code":"c".repeat(129),"message":"m","retryable":true}),
serde_json::json!({"version":"1","code":"gateway_draining","message":"m".repeat(513),"retryable":true}),
serde_json::json!({"version":"1","code":"unknown","message":"m","retryable":true}),
] {
assert_eq!(
decode_stable_error(&serde_json::to_vec(&invalid).expect("encode invalid")).err(),
Some(CoreError::Protocol)
);
}
}
}