fix(transport): harden QUIC admission boundaries
This commit is contained in:
+56
-5
@@ -1,6 +1,6 @@
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use rustls::client::ResolvesClientCert;
|
||||
use rustls::pki_types::CertificateDer;
|
||||
@@ -13,7 +13,7 @@ use crate::wire::NativeTunnelCredential;
|
||||
pub(crate) type SignCallback = dyn Fn(&[u8]) -> Result<[u8; 64]> + Send + Sync;
|
||||
|
||||
pub(crate) struct CallbackSigningKey {
|
||||
callback: Arc<SignCallback>,
|
||||
callback: Arc<Mutex<Option<Arc<SignCallback>>>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for CallbackSigningKey {
|
||||
@@ -24,7 +24,9 @@ impl fmt::Debug for CallbackSigningKey {
|
||||
|
||||
impl CallbackSigningKey {
|
||||
pub(crate) fn new(callback: Arc<SignCallback>) -> Self {
|
||||
Self { callback }
|
||||
Self {
|
||||
callback: Arc::new(Mutex::new(Some(callback))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +42,7 @@ impl SigningKey for CallbackSigningKey {
|
||||
}
|
||||
}
|
||||
|
||||
struct CallbackSigner(Arc<SignCallback>);
|
||||
struct CallbackSigner(Arc<Mutex<Option<Arc<SignCallback>>>>);
|
||||
|
||||
impl fmt::Debug for CallbackSigner {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
@@ -50,7 +52,13 @@ impl fmt::Debug for CallbackSigner {
|
||||
|
||||
impl Signer for CallbackSigner {
|
||||
fn sign(&self, message: &[u8]) -> std::result::Result<Vec<u8>, rustls::Error> {
|
||||
(self.0)(message)
|
||||
let callback = self
|
||||
.0
|
||||
.lock()
|
||||
.map_err(|_| rustls::Error::General("client signing state failed".to_owned()))?
|
||||
.take()
|
||||
.ok_or_else(|| rustls::Error::General("client signer already used".to_owned()))?;
|
||||
callback(message)
|
||||
.map(|signature| signature.to_vec())
|
||||
.map_err(|_| rustls::Error::General("client signing failed".to_owned()))
|
||||
}
|
||||
@@ -112,3 +120,46 @@ pub(crate) fn client_config(
|
||||
config.enable_early_data = false;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use super::*;
|
||||
|
||||
struct DropMarker(Arc<AtomicBool>);
|
||||
|
||||
impl Drop for DropMarker {
|
||||
fn drop(&mut self) {
|
||||
self.0.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls_callback_is_consumed_and_released_after_one_signature() {
|
||||
let dropped = Arc::new(AtomicBool::new(false));
|
||||
let marker = DropMarker(Arc::clone(&dropped));
|
||||
let callback: Arc<SignCallback> = Arc::new(move |_| {
|
||||
let _ = ▮
|
||||
Ok([7; 64])
|
||||
});
|
||||
let key = CallbackSigningKey::new(callback);
|
||||
let signer = key
|
||||
.choose_scheme(&[SignatureScheme::ED25519])
|
||||
.expect("ED25519 signer");
|
||||
let second_signer = key
|
||||
.choose_scheme(&[SignatureScheme::ED25519])
|
||||
.expect("second ED25519 signer");
|
||||
drop(key);
|
||||
|
||||
assert_eq!(
|
||||
signer.sign(b"handshake").expect("first signature"),
|
||||
vec![7; 64]
|
||||
);
|
||||
assert!(dropped.load(Ordering::SeqCst), "callback remained retained");
|
||||
assert!(
|
||||
second_signer.sign(b"second request").is_err(),
|
||||
"signer was reusable"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+87
-17
@@ -1,4 +1,5 @@
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
@@ -157,15 +158,6 @@ pub async fn connect_with_cancellation(
|
||||
}
|
||||
Ok(signature)
|
||||
});
|
||||
let rustls = client_config(credential, checked_tls_signer)?;
|
||||
let crypto = QuicClientConfig::try_from(rustls).map_err(|_| CoreError::Tls)?;
|
||||
let mut quinn_config = quinn::ClientConfig::new(Arc::new(crypto));
|
||||
let mut transport = TransportConfig::default();
|
||||
transport.max_concurrent_bidi_streams(VarInt::from_u32(1));
|
||||
transport.max_concurrent_uni_streams(VarInt::from_u32(0));
|
||||
transport.datagram_receive_buffer_size(Some(64 * 1024));
|
||||
quinn_config.transport_config(Arc::new(transport));
|
||||
|
||||
let remaining = deadline
|
||||
.checked_sub(started.elapsed())
|
||||
.ok_or(CoreError::Cancelled)?;
|
||||
@@ -173,16 +165,25 @@ pub async fn connect_with_cancellation(
|
||||
let address_count = manifest.addresses().len();
|
||||
for (address_index, address) in manifest.addresses().iter().enumerate() {
|
||||
cancellation.check()?;
|
||||
let Ok(resolved) = tokio::net::lookup_host(address).await else {
|
||||
continue;
|
||||
};
|
||||
let resolved = resolved.take(8).collect::<Vec<_>>();
|
||||
let address_budget = deadline.saturating_sub(started.elapsed())
|
||||
/ u32::try_from(address_count.saturating_sub(address_index).max(1)).unwrap_or(1);
|
||||
let address_deadline = Instant::now() + address_budget;
|
||||
let resolved = match bounded_lookup(
|
||||
address_budget,
|
||||
cancellation,
|
||||
tokio::net::lookup_host(address),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(resolved) => resolved,
|
||||
Err(CoreError::Transport) => continue,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let remote_count = resolved.len();
|
||||
for (remote_index, remote) in resolved.into_iter().enumerate() {
|
||||
cancellation.check()?;
|
||||
let quinn_config =
|
||||
client_transport_config(credential, Arc::clone(&checked_tls_signer))?;
|
||||
let remaining_remotes = remote_count.saturating_sub(remote_index).max(1);
|
||||
let attempt_budget = address_deadline.saturating_duration_since(Instant::now())
|
||||
/ u32::try_from(remaining_remotes).unwrap_or(1);
|
||||
@@ -222,6 +223,21 @@ pub async fn connect_with_cancellation(
|
||||
}
|
||||
}
|
||||
|
||||
fn client_transport_config(
|
||||
credential: &NativeTunnelCredential,
|
||||
callback: Arc<SignCallback>,
|
||||
) -> Result<quinn::ClientConfig> {
|
||||
let rustls = client_config(credential, callback)?;
|
||||
let crypto = QuicClientConfig::try_from(rustls).map_err(|_| CoreError::Tls)?;
|
||||
let mut config = quinn::ClientConfig::new(Arc::new(crypto));
|
||||
let mut transport = TransportConfig::default();
|
||||
transport.max_concurrent_bidi_streams(VarInt::from_u32(1));
|
||||
transport.max_concurrent_uni_streams(VarInt::from_u32(0));
|
||||
transport.datagram_receive_buffer_size(Some(64 * 1024));
|
||||
config.transport_config(Arc::new(transport));
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
struct AttemptContext<'a> {
|
||||
manifest: &'a ConnectionManifest,
|
||||
offered: &'a CapabilityProfile,
|
||||
@@ -253,6 +269,20 @@ async fn cancellable_timeout<T>(
|
||||
}
|
||||
}
|
||||
|
||||
async fn bounded_lookup<T>(
|
||||
duration: Duration,
|
||||
cancellation: &Cancellation,
|
||||
future: impl std::future::Future<Output = io::Result<T>>,
|
||||
) -> Result<Vec<SocketAddr>>
|
||||
where
|
||||
T: Iterator<Item = SocketAddr>,
|
||||
{
|
||||
let resolved = cancellable_timeout(duration, cancellation, future)
|
||||
.await?
|
||||
.map_err(|_| CoreError::Transport)?;
|
||||
Ok(resolved.take(8).collect())
|
||||
}
|
||||
|
||||
async fn dial(
|
||||
remote: SocketAddr,
|
||||
config: quinn::ClientConfig,
|
||||
@@ -270,7 +300,7 @@ async fn dial(
|
||||
let negotiated = connecting
|
||||
.handshake_data()
|
||||
.await
|
||||
.map_err(|_| AttemptError::Terminal(CoreError::Tls))?
|
||||
.map_err(|_| AttemptError::Terminal(tls_error(context)))?
|
||||
.downcast::<quinn::crypto::rustls::HandshakeData>()
|
||||
.ok()
|
||||
.and_then(|data| data.protocol.clone());
|
||||
@@ -279,7 +309,7 @@ async fn dial(
|
||||
}
|
||||
let connection = connecting
|
||||
.await
|
||||
.map_err(|_| AttemptError::Terminal(CoreError::Tls))?;
|
||||
.map_err(|_| AttemptError::Terminal(tls_error(context)))?;
|
||||
let payload = admission_payload(
|
||||
context.manifest,
|
||||
context.offered,
|
||||
@@ -302,7 +332,7 @@ async fn dial(
|
||||
.map_err(|_| AttemptError::Terminal(connection_error(&connection)))?;
|
||||
let Ok(authority) = ClientSessionAuthority::decode(&response) else {
|
||||
let stable = decode_stable_error(&response)?;
|
||||
return if stable.retryable {
|
||||
return if stable.retryable && stable.code == "gateway_draining" {
|
||||
Err(AttemptError::Retry)
|
||||
} else {
|
||||
Err(AttemptError::Terminal(stable.error))
|
||||
@@ -318,6 +348,14 @@ async fn dial(
|
||||
})
|
||||
}
|
||||
|
||||
fn tls_error(context: &AttemptContext<'_>) -> CoreError {
|
||||
if context.cancellation.check().is_err() || context.started.elapsed() >= context.deadline {
|
||||
CoreError::Cancelled
|
||||
} else {
|
||||
CoreError::Tls
|
||||
}
|
||||
}
|
||||
|
||||
fn admission_payload(
|
||||
manifest: &ConnectionManifest,
|
||||
offered: &CapabilityProfile,
|
||||
@@ -410,7 +448,11 @@ fn hello_length(length: usize) -> Result<u32> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{hello_length, HELLO_LIMIT};
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::{bounded_lookup, hello_length, Cancellation, HELLO_LIMIT};
|
||||
use crate::error::CoreError;
|
||||
|
||||
#[test]
|
||||
@@ -419,4 +461,32 @@ mod tests {
|
||||
assert_eq!(hello_length(0), Err(CoreError::Protocol));
|
||||
assert_eq!(hello_length(HELLO_LIMIT + 1), Err(CoreError::Protocol));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dns_lookup_is_bounded_by_address_share() {
|
||||
let started = Instant::now();
|
||||
let result = tokio::runtime::Runtime::new()
|
||||
.expect("runtime")
|
||||
.block_on(bounded_lookup(
|
||||
Duration::from_millis(20),
|
||||
&Cancellation::new(),
|
||||
std::future::pending::<io::Result<std::vec::IntoIter<SocketAddr>>>(),
|
||||
));
|
||||
assert_eq!(result.err(), Some(CoreError::Transport));
|
||||
assert!(started.elapsed() < Duration::from_secs(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancellation_interrupts_dns_lookup() {
|
||||
let cancellation = Cancellation::new();
|
||||
cancellation.cancel();
|
||||
let result = tokio::runtime::Runtime::new()
|
||||
.expect("runtime")
|
||||
.block_on(bounded_lookup(
|
||||
Duration::from_secs(1),
|
||||
&cancellation,
|
||||
std::future::pending::<io::Result<std::vec::IntoIter<SocketAddr>>>(),
|
||||
));
|
||||
assert_eq!(result.err(), Some(CoreError::Cancelled));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,6 +388,7 @@ impl ConnectionManifest {
|
||||
.iter()
|
||||
.any(|address| !bounded(address, 1, 256))
|
||||
|| !valid_dns_name(&self.gateway.public_identity)
|
||||
|| self.gateway.public_identity == self.gateway.id
|
||||
|| !(1..=4).contains(&self.tunnel.versions.len())
|
||||
|| self
|
||||
.tunnel
|
||||
@@ -621,6 +622,7 @@ struct StableError {
|
||||
|
||||
pub(crate) struct DecodedStableError {
|
||||
pub(crate) error: CoreError,
|
||||
pub(crate) code: String,
|
||||
pub(crate) retryable: bool,
|
||||
}
|
||||
|
||||
@@ -703,6 +705,7 @@ pub(crate) fn decode_stable_error(bytes: &[u8]) -> Result<DecodedStableError> {
|
||||
};
|
||||
Ok(DecodedStableError {
|
||||
error,
|
||||
code: stable.code,
|
||||
retryable: stable.retryable,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ type admission struct {
|
||||
lastTranscript []byte
|
||||
reusable bool
|
||||
failWorkOnce bool
|
||||
releaseFails bool
|
||||
authority protocol.SessionAuthority
|
||||
work protocol.ProviderSessionWork
|
||||
public ed25519.PublicKey
|
||||
@@ -67,11 +68,15 @@ func (a *admission) ProviderWork(context.Context, protocol.SessionAuthority) (pr
|
||||
if a.failWorkOnce { a.failWorkOnce = false; return protocol.ProviderSessionWork{}, context.DeadlineExceeded }
|
||||
return a.work, nil
|
||||
}
|
||||
func (a *admission) Release(context.Context, protocol.SessionAuthority) error { return nil }
|
||||
func (a *admission) Release(context.Context, protocol.SessionAuthority) error {
|
||||
if a.releaseFails { return errors.New("release failed") }
|
||||
return nil
|
||||
}
|
||||
|
||||
type provider struct{}
|
||||
type provider struct { failStart bool }
|
||||
type session struct { state protocol.ProviderState; video chan gateway.ProviderMedia; audio chan gateway.ProviderMedia; events chan gateway.ProviderEvent }
|
||||
func (provider) Start(_ context.Context, request gateway.LaunchRequest) (gateway.ProviderSession, error) {
|
||||
func (p provider) Start(_ context.Context, request gateway.LaunchRequest) (gateway.ProviderSession, error) {
|
||||
if p.failStart { return nil, context.DeadlineExceeded }
|
||||
return &session{state: protocol.ProviderState{Version:"1", SessionID:request.SessionID, State:gateway.ProviderStateReady, Channels:[]string{"video","audio","input","feedback"}}, video:make(chan gateway.ProviderMedia), audio:make(chan gateway.ProviderMedia), events:make(chan gateway.ProviderEvent)}, nil
|
||||
}
|
||||
func (s *session) Ready(context.Context) error { return nil }
|
||||
@@ -125,9 +130,9 @@ func main() {
|
||||
_, firstReplayErr := replayGuard.Admit(context.Background(),replayRequest)
|
||||
_, secondReplayErr := replayGuard.Admit(context.Background(),replayRequest)
|
||||
replayRejected := firstReplayErr == nil && errors.Is(secondReplayErr,gateway.ErrAdmissionRejected)
|
||||
admissionService := &admission{authority:authority,work:work,public:admissionKey.Public().(ed25519.PublicKey),reusable:mode == "reusable",failWorkOnce:mode == "post-retryable"}
|
||||
admissionService := &admission{authority:authority,work:work,public:admissionKey.Public().(ed25519.PublicKey),reusable:mode == "reusable",failWorkOnce:mode == "post-retryable",releaseFails:mode == "cleanup-release-failure"}
|
||||
newService := func() *gateway.Server {
|
||||
service, err := gateway.NewServer(gateway.ServerConfig{ListenAddress:"127.0.0.1:0",TLSConfig:server,GatewayID:authority.GatewayID,Admission:admissionService,Provider:provider{}}); if err != nil { panic(err) }
|
||||
service, err := gateway.NewServer(gateway.ServerConfig{ListenAddress:"127.0.0.1:0",TLSConfig:server,GatewayID:authority.GatewayID,Admission:admissionService,Provider:provider{failStart:mode == "provider-start-lost-response" || mode == "cleanup-release-failure"}}); if err != nil { panic(err) }
|
||||
return service
|
||||
}
|
||||
service := newService()
|
||||
@@ -139,7 +144,7 @@ func main() {
|
||||
services = []*gateway.Server{draining, service}
|
||||
addresses = []string{draining.Addr().String(), service.Addr().String()}
|
||||
}
|
||||
if mode == "post-retryable" {
|
||||
if mode == "post-retryable" || mode == "provider-start-lost-response" || mode == "cleanup-release-failure" {
|
||||
second := newService()
|
||||
services = []*gateway.Server{service, second}
|
||||
addresses = []string{service.Addr().String(), second.Addr().String()}
|
||||
@@ -303,7 +308,7 @@ fn callback_ed25519_signer_completes_tls13_quic_admission_without_private_key_in
|
||||
|
||||
assert_eq!(session.authority().session_id(), "session-1");
|
||||
assert_eq!(admission_calls.load(Ordering::SeqCst), 1);
|
||||
assert!(tls_calls.load(Ordering::SeqCst) >= 1);
|
||||
assert_eq!(tls_calls.load(Ordering::SeqCst), 1);
|
||||
assert!(admission_inputs
|
||||
.lock()
|
||||
.expect("admission transcript")
|
||||
@@ -598,6 +603,41 @@ fn overall_deadline_includes_synchronous_admission_signing() {
|
||||
assert!(started.elapsed() < Duration::from_secs(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls_signer_finite_deadline_overrun_is_cancelled_not_tls() {
|
||||
let oracle = Oracle::start("");
|
||||
let manifest = ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let admission_key = test_key(&oracle.ready.admission_key);
|
||||
let tls_key = test_key(&oracle.ready.client_key);
|
||||
let cancellation = Cancellation::new();
|
||||
let trigger = cancellation.clone();
|
||||
let canceller = thread::spawn(move || {
|
||||
thread::sleep(Duration::from_millis(25));
|
||||
trigger.cancel();
|
||||
});
|
||||
let result =
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("runtime")
|
||||
.block_on(connect_with_cancellation(
|
||||
&manifest,
|
||||
&credential,
|
||||
Signers::new(
|
||||
AdmissionSigner::new(signer(admission_key)),
|
||||
TlsEd25519Signer::new(move |message| {
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
signer(Arc::clone(&tls_key))(message)
|
||||
}),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
&cancellation,
|
||||
));
|
||||
canceller.join().expect("canceller");
|
||||
assert_eq!(result.err(), Some(CoreError::Cancelled));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_cancellation_interrupts_network_wait() {
|
||||
let oracle = Oracle::start("");
|
||||
@@ -733,29 +773,6 @@ fn invalid_first_manifest_address_does_not_block_live_second_address() {
|
||||
fn retryable_stable_error_advances_to_live_manifest_address() {
|
||||
let oracle = Oracle::start("retryable");
|
||||
let manifest = ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.expect("retryable draining response advanced to live address");
|
||||
runtime.block_on(session.close());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_admission_retry_uses_a_fresh_signed_nonce() {
|
||||
let oracle = Oracle::start("post-retryable");
|
||||
let manifest = ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let transcripts = Arc::new(Mutex::new(Vec::new()));
|
||||
@@ -772,13 +789,48 @@ fn post_admission_retry_uses_a_fresh_signed_nonce() {
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.expect("post-admission retry used a fresh request");
|
||||
.expect("retryable draining response advanced to live address");
|
||||
runtime.block_on(session.close());
|
||||
let transcripts = transcripts.lock().expect("transcripts");
|
||||
assert_eq!(transcripts.len(), 2);
|
||||
assert_ne!(transcripts[0], transcripts[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_admission_failure_is_terminal_and_does_not_mutate_twice() {
|
||||
for mode in [
|
||||
"post-retryable",
|
||||
"provider-start-lost-response",
|
||||
"cleanup-release-failure",
|
||||
] {
|
||||
let oracle = Oracle::start(mode);
|
||||
let manifest =
|
||||
ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let transcripts = Arc::new(Mutex::new(Vec::new()));
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let result = runtime.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::clone(&transcripts),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
));
|
||||
assert!(result.is_err(), "{mode} was retried to success");
|
||||
let transcripts = transcripts.lock().expect("transcripts");
|
||||
assert_eq!(
|
||||
transcripts.len(),
|
||||
1,
|
||||
"{mode} attempted admission mutation twice"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_production_gateway_admissions_remain_bounded() {
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
|
||||
@@ -145,6 +145,14 @@ fn manifest_public_identity_requires_dns_sni_not_ip_or_uuid() {
|
||||
"non-DNS SNI accepted: {invalid_identity}"
|
||||
);
|
||||
}
|
||||
|
||||
let identity_equals_dns_shaped_gateway_id = String::from_utf8(valid_manifest().to_vec())
|
||||
.expect("fixture is UTF-8")
|
||||
.replace("\"id\":\"gateway\"", "\"id\":\"gateway.test\"");
|
||||
assert!(
|
||||
ConnectionManifest::decode(identity_equals_dns_shaped_gateway_id.as_bytes()).is_err(),
|
||||
"public SNI identity matched the logical gateway id"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user