0
0
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:
Alex Konradi 2024-04-02 07:48:34 -04:00 committed by GitHub
parent a829c8f2e3
commit 5d30eaa9c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 436 additions and 66 deletions

126
Cargo.lock generated
View File

@ -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",
]

View File

@ -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"

View File

@ -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>,

View File

@ -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;
}
}