feat(core): add QUIC TLS admission transport
This commit is contained in:
+147
-17
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user