mirror of
https://github.com/signalapp/libsignal.git
synced 2024-09-20 12:02:18 +02:00
Add a TransportConnector impl via proxy
This commit is contained in:
parent
a829c8f2e3
commit
5d30eaa9c5
126
Cargo.lock
generated
126
Cargo.lock
generated
@ -565,7 +565,7 @@ dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"num-traits 0.2.18",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-targets 0.52.4",
|
||||
@ -759,7 +759,7 @@ dependencies = [
|
||||
"criterion-plot",
|
||||
"is-terminal",
|
||||
"itertools 0.10.5",
|
||||
"num-traits",
|
||||
"num-traits 0.2.18",
|
||||
"once_cell",
|
||||
"oorandom",
|
||||
"plotters",
|
||||
@ -1093,6 +1093,15 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum_primitive"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180"
|
||||
dependencies = [
|
||||
"num-traits 0.1.43",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.0"
|
||||
@ -2029,6 +2038,7 @@ dependencies = [
|
||||
"snow",
|
||||
"strum",
|
||||
"thiserror",
|
||||
"tls-parser",
|
||||
"tokio",
|
||||
"tokio-boring",
|
||||
"tokio-stream",
|
||||
@ -2232,7 +2242,7 @@ dependencies = [
|
||||
"encoding_rs",
|
||||
"memmap2",
|
||||
"minidump-common",
|
||||
"num-traits",
|
||||
"num-traits 0.2.18",
|
||||
"procfs-core",
|
||||
"range-map",
|
||||
"scroll",
|
||||
@ -2251,7 +2261,7 @@ dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"debugid",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"num-traits 0.2.18",
|
||||
"range-map",
|
||||
"scroll",
|
||||
"smart-default",
|
||||
@ -2408,6 +2418,28 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom-derive"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ff943d68b88d0b87a6e0d58615e8fa07f9fd5a1319fa0a72efc1f62275c79a7"
|
||||
dependencies = [
|
||||
"nom",
|
||||
"nom-derive-impl",
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom-derive-impl"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd0b9a93a84b0d3ec3e70e02d332dc33ac6dfac9cde63e17fcb77172dededa62"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nonzero_ext"
|
||||
version = "0.3.0"
|
||||
@ -2437,7 +2469,16 @@ version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"num-traits 0.2.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.1.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
|
||||
dependencies = [
|
||||
"num-traits 0.2.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2606,6 +2647,44 @@ dependencies = [
|
||||
"indexmap 2.2.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.5"
|
||||
@ -2650,7 +2729,7 @@ version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"num-traits 0.2.18",
|
||||
"plotters-backend",
|
||||
"plotters-svg",
|
||||
"wasm-bindgen",
|
||||
@ -2841,7 +2920,7 @@ dependencies = [
|
||||
"bit-vec",
|
||||
"bitflags 2.4.2",
|
||||
"lazy_static",
|
||||
"num-traits",
|
||||
"num-traits 0.2.18",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"rand_xorshift",
|
||||
@ -3036,7 +3115,7 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12a5a2d6c7039059af621472a4389be1215a816df61aa4d531cfe85264aee95f"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"num-traits 0.2.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3146,6 +3225,15 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusticata-macros"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.31"
|
||||
@ -3479,6 +3567,12 @@ dependencies = [
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.9"
|
||||
@ -3776,6 +3870,20 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tls-parser"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "409206e2de64edbf7ea99a44ac31680daf9ef1a57895fb3c5bd738a903691be0"
|
||||
dependencies = [
|
||||
"enum_primitive",
|
||||
"nom",
|
||||
"nom-derive",
|
||||
"phf",
|
||||
"phf_codegen",
|
||||
"rusticata-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.36.0"
|
||||
@ -4244,7 +4352,7 @@ dependencies = [
|
||||
"log",
|
||||
"mediasan-common",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"num-traits 0.2.18",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
|
@ -64,3 +64,4 @@ tokio = { version = "1", features = ["test-util", "rt-multi-thread"] }
|
||||
tokio-stream = "0.1.14"
|
||||
url = "2.4.1"
|
||||
warp = { version = "0.3.6", features = ["tls"] }
|
||||
tls-parser = "0.11.0"
|
||||
|
@ -60,6 +60,17 @@ impl LookupResult {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl LookupResult {
|
||||
pub(crate) fn localhost() -> Self {
|
||||
Self::new(
|
||||
crate::infra::DnsSource::Static,
|
||||
vec![Ipv4Addr::LOCALHOST],
|
||||
vec![Ipv6Addr::LOCALHOST],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DnsResolver {
|
||||
static_map: HashMap<&'static str, LookupResult>,
|
||||
|
@ -9,8 +9,9 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use boring::ssl::{SslConnector, SslConnectorBuilder, SslMethod};
|
||||
use boring::ssl::{ConnectConfiguration, SslConnector, SslMethod};
|
||||
use futures_util::TryFutureExt;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_boring::SslStream;
|
||||
|
||||
@ -44,12 +45,7 @@ impl TransportConnector for TcpSslTransportConnector {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let ssl_config = Self::builder(connection_params.certs, alpn)?
|
||||
.build()
|
||||
.configure()?;
|
||||
|
||||
let ssl_stream =
|
||||
tokio_boring::connect(ssl_config, &connection_params.sni, tcp_stream).await?;
|
||||
let ssl_stream = connect_tls(tcp_stream, connection_params, alpn).await?;
|
||||
|
||||
Ok(StreamAndInfo(ssl_stream, remote_address))
|
||||
}
|
||||
@ -61,19 +57,81 @@ impl TcpSslTransportConnector {
|
||||
dns_resolver: Arc::new(resolver),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn builder(
|
||||
certs: RootCertificates,
|
||||
#[derive(Clone)]
|
||||
pub struct TcpSslProxyConnector {
|
||||
dns_resolver: Arc<DnsResolver>,
|
||||
proxy_host: Arc<str>,
|
||||
proxy_port: NonZeroU16,
|
||||
proxy_certs: RootCertificates,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TransportConnector for TcpSslProxyConnector {
|
||||
type Stream = SslStream<SslStream<TcpStream>>;
|
||||
|
||||
async fn connect(
|
||||
&self,
|
||||
connection_params: &ConnectionParams,
|
||||
alpn: Alpn,
|
||||
) -> Result<SslConnectorBuilder, TransportConnectError> {
|
||||
let mut ssl = SslConnector::builder(SslMethod::tls_client())?;
|
||||
ssl.set_verify_cert_store(certs.try_into()?)?;
|
||||
ssl.set_alpn_protos(alpn.as_ref())?;
|
||||
Ok(ssl)
|
||||
) -> Result<StreamAndInfo<Self::Stream>, TransportConnectError> {
|
||||
let StreamAndInfo(tcp_stream, remote_address) = connect_tcp(
|
||||
&self.dns_resolver,
|
||||
connection_params.route_type,
|
||||
&self.proxy_host,
|
||||
self.proxy_port,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let ssl_config = ssl_config(self.proxy_certs, None)?;
|
||||
|
||||
let outer_ssl = tokio_boring::connect(ssl_config, &self.proxy_host, tcp_stream).await?;
|
||||
|
||||
let tls_stream = connect_tls(outer_ssl, connection_params, alpn).await?;
|
||||
|
||||
Ok(StreamAndInfo(tls_stream, remote_address))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn connect_tcp(
|
||||
impl TcpSslProxyConnector {
|
||||
pub fn new(resolver: DnsResolver, (proxy_host, proxy_port): (&str, NonZeroU16)) -> Self {
|
||||
Self {
|
||||
dns_resolver: Arc::new(resolver),
|
||||
proxy_host: proxy_host.into(),
|
||||
proxy_port,
|
||||
// We don't bundle roots of trust for all the SSL proxies, just the
|
||||
// Signal servers. It's fine to use the system SSL trust roots;
|
||||
// even if the outer connection is not secure, the inner connection
|
||||
// is also TLS-encrypted.
|
||||
proxy_certs: RootCertificates::Native,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ssl_config(
|
||||
certs: RootCertificates,
|
||||
alpn: Option<Alpn>,
|
||||
) -> Result<ConnectConfiguration, TransportConnectError> {
|
||||
let mut ssl = SslConnector::builder(SslMethod::tls_client())?;
|
||||
ssl.set_verify_cert_store(certs.try_into()?)?;
|
||||
if let Some(alpn) = alpn {
|
||||
ssl.set_alpn_protos(alpn.as_ref())?;
|
||||
}
|
||||
Ok(ssl.build().configure()?)
|
||||
}
|
||||
|
||||
async fn connect_tls<S: AsyncRead + AsyncWrite + Unpin>(
|
||||
transport: S,
|
||||
connection_params: &ConnectionParams,
|
||||
alpn: Alpn,
|
||||
) -> Result<SslStream<S>, TransportConnectError> {
|
||||
let ssl_config = ssl_config(connection_params.certs, Some(alpn))?;
|
||||
|
||||
Ok(tokio_boring::connect(ssl_config, &connection_params.sni, transport).await?)
|
||||
}
|
||||
|
||||
async fn connect_tcp(
|
||||
dns_resolver: &DnsResolver,
|
||||
route_type: &'static str,
|
||||
host: &str,
|
||||
@ -135,62 +193,51 @@ fn ip_addr_to_host(ip: IpAddr) -> url::Host {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::net::Ipv6Addr;
|
||||
mod testutil {
|
||||
use std::future::Future;
|
||||
use std::net::{Ipv6Addr, SocketAddr};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use boring::pkey::PKey;
|
||||
use boring::ssl::{SslAcceptor, SslMethod};
|
||||
use boring::x509::X509;
|
||||
use lazy_static::lazy_static;
|
||||
use rcgen::CertifiedKey;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tls_parser::{ClientHello, TlsExtension, TlsMessage, TlsMessageHandshake, TlsPlaintext};
|
||||
use tokio::io::{
|
||||
AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufStream,
|
||||
};
|
||||
use warp::Filter;
|
||||
|
||||
use crate::infra::HttpRequestDecoratorSeq;
|
||||
|
||||
use super::*;
|
||||
|
||||
const TEST_SNI: &str = "localhost";
|
||||
pub(super) const SERVER_HOSTNAME: &str = "test-server.signal.org.local";
|
||||
|
||||
lazy_static! {
|
||||
static ref CERTIFICATE: CertifiedKey =
|
||||
rcgen::generate_simple_self_signed([TEST_SNI.to_string()]).expect("can generate");
|
||||
pub(super) static ref SERVER_CERTIFICATE: CertifiedKey =
|
||||
rcgen::generate_simple_self_signed([SERVER_HOSTNAME.to_string()])
|
||||
.expect("can generate");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn connect_to_server() {
|
||||
const FAKE_RESPONSE: &str = "Hello there";
|
||||
|
||||
const FAKE_RESPONSE: &str = "Hello there";
|
||||
/// Starts an HTTP server listening on `::1` that responds with 200 and
|
||||
/// [`FAKE_RESPONSE`].
|
||||
///
|
||||
/// Returns the address of the server and a [`Future`] that runs it.
|
||||
pub(super) fn localhost_http_server() -> (SocketAddr, impl Future<Output = ()>) {
|
||||
let filter = warp::any().map(|| FAKE_RESPONSE);
|
||||
let server = warp::serve(filter)
|
||||
.tls()
|
||||
.cert(CERTIFICATE.cert.pem())
|
||||
.key(CERTIFICATE.key_pair.serialize_pem());
|
||||
.cert(SERVER_CERTIFICATE.cert.pem())
|
||||
.key(SERVER_CERTIFICATE.key_pair.serialize_pem());
|
||||
|
||||
let (addr, server) = server.bind_ephemeral((Ipv6Addr::LOCALHOST, 0));
|
||||
let _server_handle = tokio::spawn(server);
|
||||
|
||||
let connector = TcpSslTransportConnector::new(DnsResolver::default());
|
||||
let connection_params = ConnectionParams {
|
||||
route_type: "test",
|
||||
sni: TEST_SNI.into(),
|
||||
host: addr.ip().to_string().into(),
|
||||
port: addr.port().try_into().expect("bound port"),
|
||||
http_request_decorator: HttpRequestDecoratorSeq::default(),
|
||||
certs: RootCertificates::FromDer(CERTIFICATE.cert.der()),
|
||||
};
|
||||
|
||||
let StreamAndInfo(mut stream, info) = connector
|
||||
.connect(&connection_params, Alpn::Http1_1)
|
||||
.await
|
||||
.expect("can connect");
|
||||
|
||||
assert_eq!(
|
||||
info,
|
||||
ConnectionInfo {
|
||||
address: url::Host::Ipv6(Ipv6Addr::LOCALHOST),
|
||||
dns_source: crate::infra::DnsSource::Lookup,
|
||||
route_type: "test"
|
||||
}
|
||||
);
|
||||
server.bind_ephemeral((Ipv6Addr::LOCALHOST, 0))
|
||||
}
|
||||
|
||||
/// Makes an HTTP request on the provided stream and asserts on the response.
|
||||
///
|
||||
/// Asserts that the server returns 200 and [`FAKE_RESPONSE`].
|
||||
pub(super) async fn make_http_request_response_over(
|
||||
mut stream: impl AsyncRead + AsyncWrite + Unpin,
|
||||
) {
|
||||
stream
|
||||
.write_all(b"GET /index HTTP/1.1\r\nConnection: close\r\n\r\n")
|
||||
.await
|
||||
@ -209,4 +256,207 @@ mod test {
|
||||
assert_eq!(lines.first(), Some("HTTP/1.1 200 OK").as_ref(), "{lines:?}");
|
||||
assert_eq!(lines.last(), Some(FAKE_RESPONSE).as_ref(), "{lines:?}");
|
||||
}
|
||||
|
||||
pub(super) const PROXY_HOSTNAME: &str = "test-proxy.signal.org.local";
|
||||
|
||||
lazy_static! {
|
||||
pub(super) static ref PROXY_CERTIFICATE: CertifiedKey =
|
||||
rcgen::generate_simple_self_signed([PROXY_HOSTNAME.to_string()]).expect("can generate");
|
||||
}
|
||||
|
||||
/// Starts a TLS server that proxies TLS connections to an upstream server.
|
||||
///
|
||||
/// Proxies TLS connections with `upstream_sni` to `upstream_addr`.
|
||||
pub(super) fn localhost_tls_proxy(
|
||||
upstream_sni: &'static str,
|
||||
upstream_addr: SocketAddr,
|
||||
) -> (SocketAddr, impl Future<Output = ()>) {
|
||||
// TODO(https://github.com/rust-lang/rust/issues/31436): use a `try`
|
||||
// block instead of immediately-invoked closure.
|
||||
let ssl_acceptor = (|| {
|
||||
let mut builder = SslAcceptor::mozilla_intermediate_v5(SslMethod::tls_server())?;
|
||||
builder.set_certificate(X509::from_der(PROXY_CERTIFICATE.cert.der())?.as_ref())?;
|
||||
builder.set_private_key(
|
||||
PKey::private_key_from_der(PROXY_CERTIFICATE.key_pair.serialized_der())?.as_ref(),
|
||||
)?;
|
||||
// If the cert can be loaded, build the thing.
|
||||
builder.check_private_key().map(|()| builder.build())
|
||||
})()
|
||||
.expect("can configure acceptor");
|
||||
|
||||
let listener = std::net::TcpListener::bind((Ipv6Addr::LOCALHOST, 0)).expect("can bind");
|
||||
let listen_addr = listener.local_addr().expect("is bound to local addr");
|
||||
let tcp_listener = tokio::net::TcpListener::from_std(listener).expect("can use std socket");
|
||||
let proxy = async move {
|
||||
loop {
|
||||
let (tcp_stream, _remote_addr) =
|
||||
tcp_listener.accept().await.expect("incoming connection");
|
||||
let ssl_stream = tokio_boring::accept(&ssl_acceptor, tcp_stream)
|
||||
.await
|
||||
.expect("handshake successful");
|
||||
|
||||
let (sni_names, mut ssl_stream) = parse_sni_from_stream(ssl_stream).await;
|
||||
assert_eq!(sni_names, &[upstream_sni]);
|
||||
|
||||
// Now connect to the upstream and then proxy for the life of the connection.
|
||||
let mut upstream_stream = tokio::net::TcpStream::connect(upstream_addr)
|
||||
.await
|
||||
.expect("can connect to upstream");
|
||||
tokio::io::copy_bidirectional(&mut ssl_stream, &mut upstream_stream)
|
||||
.await
|
||||
.expect("can proxy");
|
||||
}
|
||||
};
|
||||
|
||||
(listen_addr, proxy)
|
||||
}
|
||||
|
||||
/// Read SNI names from TCP handshake on a stream.
|
||||
///
|
||||
/// Consumes the stream and returns a new one with the same contents.
|
||||
pub(super) async fn parse_sni_from_stream<S: AsyncRead + AsyncWrite + Unpin>(
|
||||
stream: S,
|
||||
) -> (Vec<String>, BufStream<S>) {
|
||||
/// Minimum acceptable size for a TCP segment.
|
||||
///
|
||||
/// The first TLS frame sent by the client should fit within this.
|
||||
const TCP_MIN_MSS: usize = 576;
|
||||
|
||||
let mut stream = tokio::io::BufStream::with_capacity(TCP_MIN_MSS, TCP_MIN_MSS, stream);
|
||||
|
||||
let first_record = loop {
|
||||
// We're intentionally reading from the buffer without marking the
|
||||
// bytes as consumed so that when the stream is passed back to the
|
||||
// caller they can read them too.
|
||||
let buffer = stream.fill_buf().await.expect("can read");
|
||||
match tls_parser::parse_tls_plaintext(buffer) {
|
||||
Ok((_, record)) => break record,
|
||||
Err(tls_parser::Err::Incomplete(_)) => continue,
|
||||
Err(e) => panic!("failed to parse TLS: {e}"),
|
||||
}
|
||||
};
|
||||
|
||||
let TlsPlaintext { hdr: _, msg } = first_record;
|
||||
let msg = msg.first().expect("nonempty messages");
|
||||
let client_hello = assert_matches!(
|
||||
msg,
|
||||
TlsMessage::Handshake(TlsMessageHandshake::ClientHello(hello)) => hello
|
||||
);
|
||||
let (_, client_hello_extensions) = tls_parser::parse_tls_client_hello_extensions(
|
||||
client_hello.ext().expect("has extensions"),
|
||||
)
|
||||
.expect("can parse extensions");
|
||||
let sni = client_hello_extensions
|
||||
.into_iter()
|
||||
.find_map(|ex| match ex {
|
||||
TlsExtension::SNI(sni) => Some(sni),
|
||||
_ => None,
|
||||
})
|
||||
.expect("has SNI extension");
|
||||
let names = sni
|
||||
.into_iter()
|
||||
.map(|(_sni_type, name)| String::from_utf8(Vec::from(name)).expect("SNI name is UTF-8"))
|
||||
.collect();
|
||||
|
||||
(names, stream)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashMap;
|
||||
use std::net::Ipv6Addr;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
use crate::infra::dns::LookupResult;
|
||||
use crate::infra::HttpRequestDecoratorSeq;
|
||||
|
||||
use super::testutil::*;
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn connect_to_server() {
|
||||
let (addr, server) = localhost_http_server();
|
||||
let _server_handle = tokio::spawn(server);
|
||||
|
||||
let connector =
|
||||
TcpSslTransportConnector::new(DnsResolver::new_with_static_fallback(HashMap::from([
|
||||
(SERVER_HOSTNAME, LookupResult::localhost()),
|
||||
])));
|
||||
let connection_params = ConnectionParams {
|
||||
route_type: "test",
|
||||
sni: SERVER_HOSTNAME.into(),
|
||||
host: addr.ip().to_string().into(),
|
||||
port: addr.port().try_into().expect("bound port"),
|
||||
http_request_decorator: HttpRequestDecoratorSeq::default(),
|
||||
certs: RootCertificates::FromDer(SERVER_CERTIFICATE.cert.der()),
|
||||
};
|
||||
|
||||
let StreamAndInfo(stream, info) = connector
|
||||
.connect(&connection_params, Alpn::Http1_1)
|
||||
.await
|
||||
.expect("can connect");
|
||||
|
||||
assert_eq!(
|
||||
info,
|
||||
ConnectionInfo {
|
||||
address: url::Host::Ipv6(Ipv6Addr::LOCALHOST),
|
||||
dns_source: crate::infra::DnsSource::Static,
|
||||
route_type: "test"
|
||||
}
|
||||
);
|
||||
|
||||
make_http_request_response_over(stream).await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn connect_through_proxy() {
|
||||
let (addr, server) = localhost_http_server();
|
||||
let _server_handle = tokio::spawn(server);
|
||||
|
||||
let (proxy_addr, proxy) = localhost_tls_proxy(SERVER_HOSTNAME, addr);
|
||||
let _proxy_handle = tokio::spawn(proxy);
|
||||
|
||||
// Ensure that the proxy is doing the right thing
|
||||
let mut connector = TcpSslProxyConnector::new(
|
||||
DnsResolver::new_with_static_fallback(HashMap::from([(
|
||||
PROXY_HOSTNAME,
|
||||
LookupResult::localhost(),
|
||||
)])),
|
||||
(PROXY_HOSTNAME, proxy_addr.port().try_into().unwrap()),
|
||||
);
|
||||
// Override the SSL certificate for the proxy; since it's self-signed,
|
||||
// it won't work with the default config.
|
||||
let default_root_cert = std::mem::replace(
|
||||
&mut connector.proxy_certs,
|
||||
RootCertificates::FromDer(PROXY_CERTIFICATE.cert.der()),
|
||||
);
|
||||
assert_matches!(default_root_cert, RootCertificates::Native);
|
||||
|
||||
let connection_params = ConnectionParams {
|
||||
route_type: "test",
|
||||
sni: SERVER_HOSTNAME.into(),
|
||||
host: "localhost".to_string().into(),
|
||||
port: addr.port().try_into().expect("bound port"),
|
||||
http_request_decorator: HttpRequestDecoratorSeq::default(),
|
||||
certs: RootCertificates::FromDer(SERVER_CERTIFICATE.cert.der()),
|
||||
};
|
||||
|
||||
let StreamAndInfo(stream, info) = connector
|
||||
.connect(&connection_params, Alpn::Http1_1)
|
||||
.await
|
||||
.expect("can connect");
|
||||
|
||||
assert_eq!(
|
||||
info,
|
||||
ConnectionInfo {
|
||||
address: url::Host::Ipv6(Ipv6Addr::LOCALHOST),
|
||||
dns_source: crate::infra::DnsSource::Static,
|
||||
route_type: "test"
|
||||
}
|
||||
);
|
||||
|
||||
make_http_request_response_over(stream).await;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user