Skip to content

Commit

Permalink
Add HTTPS proxy as a transport
Browse files Browse the repository at this point in the history
  • Loading branch information
akonradi-signal authored Dec 18, 2024
1 parent 33b8e9c commit 74b524c
Show file tree
Hide file tree
Showing 16 changed files with 896 additions and 63 deletions.
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ subtle = "2.6"
syn = "2.0"
syn-mid = "0.6"
test-case = "3.3"
test-log = "0.2.16"
testing_logger = "0.1.1"
thiserror = "1.0.57"
tokio = "1"
Expand Down
24 changes: 18 additions & 6 deletions rust/bridge/shared/types/src/net/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use libsignal_net::chat::{
use libsignal_net::infra::route::{ConnectionProxyConfig, DirectOrProxyProvider};
use libsignal_net::infra::tcp_ssl::InvalidProxyConfig;
use libsignal_protocol::Timestamp;
use static_assertions::assert_impl_all;
use tokio::sync::{mpsc, oneshot};

use crate::net::{ConnectionManager, TokioAsyncContext};
Expand Down Expand Up @@ -248,18 +249,23 @@ impl RefUnwindSafe for AuthenticatedChatConnection {}

enum MaybeChatConnection {
Running(ChatConnection),
WaitingForListener(tokio::runtime::Handle, chat::PendingChatConnection),
WaitingForListener(
tokio::runtime::Handle,
std::sync::Mutex<chat::PendingChatConnection>,
),
TemporarilyEvicted,
}

assert_impl_all!(MaybeChatConnection: Send, Sync);

impl UnauthenticatedChatConnection {
pub async fn connect(connection_manager: &ConnectionManager) -> Result<Self, ChatServiceError> {
let inner = establish_chat_connection("unauthenticated", connection_manager, None).await?;
log::info!("connected unauthenticated chat");
Ok(Self {
inner: MaybeChatConnection::WaitingForListener(
tokio::runtime::Handle::current(),
inner,
inner.into(),
)
.into(),
})
Expand All @@ -283,7 +289,7 @@ impl AuthenticatedChatConnection {
Ok(Self {
inner: MaybeChatConnection::WaitingForListener(
tokio::runtime::Handle::current(),
inner,
inner.into(),
)
.into(),
})
Expand Down Expand Up @@ -344,9 +350,15 @@ impl<C: AsRef<tokio::sync::RwLock<MaybeChatConnection>> + Sync> BridgeChatConnec
fn info(&self) -> ConnectionInfo {
let guard = self.as_ref().blocking_read();
let connection_info = match &*guard {
MaybeChatConnection::Running(chat_connection) => chat_connection.connection_info(),
MaybeChatConnection::Running(chat_connection) => {
chat_connection.connection_info().clone()
}
MaybeChatConnection::WaitingForListener(_, pending_chat_connection) => {
pending_chat_connection.connection_info()
pending_chat_connection
.lock()
.expect("not poisoned")
.connection_info()
.clone()
}
MaybeChatConnection::TemporarilyEvicted => unreachable!("unobservable state"),
};
Expand All @@ -370,7 +382,7 @@ fn init_listener(connection: &mut MaybeChatConnection, listener: Box<dyn ChatLis

*connection = MaybeChatConnection::Running(ChatConnection::finish_connect(
tokio_runtime,
pending,
pending.into_inner().expect("not poisoned"),
listener.into_event_listener(),
))
}
Expand Down
2 changes: 1 addition & 1 deletion rust/net/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ proptest = { workspace = true }
proptest-state-machine = "0.1.0"
snow = { workspace = true, features = ["default-resolver"] }
test-case = { workspace = true }
test-log = "0.2.16"
test-log = { workspace = true }
tokio = { workspace = true, features = ["test-util", "io-std", "rt-multi-thread"] }
warp = { version = "0.3.6", features = ["tls"] }

Expand Down
163 changes: 163 additions & 0 deletions rust/net/examples/https_proxy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//

//! Connects to a provided proxy host and then shuffles bytes to/from stdout/stdin.
//!
//! This makes an HTTP request through an HTTPS proxy:
//! ```text
//! #!/bin/bash
//! # This example uses https://tinyproxy.github.io/ with the following config:
//! # > Port 8888
//! # > Listen 127.0.0.1
//! # > Allow 127.0.0.1
//! PROXY_URL=http://127.0.0.1:8888;
//! # Send an HTTP 1.1 request, then hold STDIN open so the example doesn't exit.
//! bash -c 'echo -en "GET / HTTP/1.1\r\nHost: signal.org\r\n\r\n"; cat' | \
//! # Run the example, pointing it at an HTTP(S) proxy that supports CONNECT.
//! cargo run -p libsignal-net --example https_proxy -- $PROXY_URL signal.org:80
//! ```
use std::net::SocketAddr;
use std::num::NonZeroU16;
use std::str::FromStr;
use std::sync::Arc;

use clap::Parser;
use either::Either;
use futures_util::stream::FuturesUnordered;
use futures_util::StreamExt;
use libsignal_net::infra::certs::RootCertificates;
use libsignal_net::infra::dns::DnsResolver;
use libsignal_net::infra::host::Host;
use libsignal_net_infra::route::{
ConnectorExt as _, HttpProxyAuth, HttpProxyRouteFragment, HttpsProxyRoute, ProxyTarget,
TcpRoute, TlsRoute, TlsRouteFragment, UnresolvedHost,
};
use libsignal_net_infra::Alpn;
use tokio::time::Duration;
use url::Url;

#[derive(Clone, Debug, Parser)]
struct Args {
proxy_url: Url,
#[arg(value_parser = parse_target)]
target: Target,

#[arg(default_value_t = false, long)]
resolve_hostname_locally: bool,
}

#[derive(Clone, Debug)]
struct Target(Host<Arc<str>>, NonZeroU16);

fn parse_target(target: &str) -> Result<Target, &'static str> {
if let Ok(target) = SocketAddr::from_str(target) {
let port = NonZeroU16::new(target.port()).ok_or("expected nonzero port")?;
return Ok(Target(Host::Ip(target.ip()), port));
}

let (domain, port) = target.split_once(':').ok_or("expected host:port")?;
let port = NonZeroU16::from_str(port).map_err(|_| "expected valid port")?;
Ok(Target(Host::Domain(domain.into()), port))
}

#[tokio::main]
async fn main() {
env_logger::init();

let Args {
proxy_url,
target,
resolve_hostname_locally,
} = Args::parse();

let proxy_host = Host::<Arc<str>>::parse_as_ip_or_domain(
proxy_url.host_str().expect("proxy host was not provided"),
);
let proxy_port = proxy_url
.port()
.expect("proxy port was not provided")
.try_into()
.expect("proxy port was zero");

let root_certs = RootCertificates::Native;
let tcp_to_proxy = TcpRoute {
address: proxy_host.clone().map_domain(UnresolvedHost::from),
port: proxy_port,
};
let inner = match proxy_url.scheme() {
"http" => Either::Right(tcp_to_proxy),
"https" => Either::Left(TlsRoute {
inner: tcp_to_proxy,
fragment: TlsRouteFragment {
root_certs,
sni: proxy_host.clone(),
alpn: Some(Alpn::Http1_1),
},
}),
scheme => panic!("unsupported protocol {scheme}"),
};
let username = (!proxy_url.username().is_empty()).then_some(proxy_url.username());
let authorization = match (username, proxy_url.password()) {
(Some(username), Some(password)) => Some(HttpProxyAuth {
username: username.to_owned(),
password: password.to_owned(),
}),
(None, None) => None,
_ => panic!("only one of username or password was provided"),
};

let dns_resolver = DnsResolver::new(&Default::default());

let Target(target_host, target_port) = target;
let target_host = match (resolve_hostname_locally, target_host) {
(true, host) => ProxyTarget::ResolvedLocally(host.map_domain(UnresolvedHost::from)),
(false, Host::Ip(ip)) => ProxyTarget::ResolvedLocally(Host::Ip(ip)),
(false, Host::Domain(domain)) => ProxyTarget::ResolvedRemotely { name: domain },
};
let unresolved_route = HttpsProxyRoute {
inner,
fragment: HttpProxyRouteFragment {
target_host,
target_port,
authorization,
},
};
log::info!("unresolved: {unresolved_route:?}");
let resolved = libsignal_net::infra::route::resolve_route(&dns_resolver, unresolved_route)
.await
.expect("failed to resolve");
let connector = libsignal_net::infra::tcp_ssl::proxy::StatelessProxied;

const START_NEXT_DELAY: Duration = Duration::from_secs(5);
let connect_attempts = FuturesUnordered::from_iter(resolved.zip(0..).map(|(route, i)| {
let connector = &connector;
async move {
tokio::time::sleep(START_NEXT_DELAY * i).await;
log::info!("connecting via: {route:?}");
connector.connect(route).await
}
}));
let mut connection = connect_attempts
.filter_map(|r| {
std::future::ready(match r {
Ok(c) => Some(c),
Err(e) => {
log::info!("connect failure: {e}");
None
}
})
})
.next()
.await
.expect("failed to connect");

eprintln!("connected to proxy, reading from stdin");
let mut stdinout = tokio::io::join(tokio::io::stdin(), tokio::io::stdout());

tokio::io::copy_bidirectional(&mut stdinout, &mut connection)
.await
.expect("proxying failed");
}
8 changes: 4 additions & 4 deletions rust/net/examples/socks_proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use libsignal_net::infra::tcp_ssl::proxy::socks::{Protocol, SocksConnector};
use libsignal_net::infra::{Alpn, StreamAndInfo, TransportConnectionParams, TransportConnector};
use libsignal_net_infra::errors::TransportConnectError;
use libsignal_net_infra::route::{
ConnectorExt as _, SocksRoute, SocksTarget, TcpRoute, TlsRoute, TlsRouteFragment,
ConnectorExt as _, ProxyTarget, SocksRoute, TcpRoute, TlsRoute, TlsRouteFragment,
UnresolvedHost,
};
use tokio::time::Duration;
Expand Down Expand Up @@ -97,9 +97,9 @@ async fn main() {
let Target(target_host, target_port) = target;
let host_name = target_host.to_string().into();
let target_host = match (resolve_hostname_locally, target_host) {
(true, host) => SocksTarget::ResolvedLocally(host.map_domain(UnresolvedHost::from)),
(false, Host::Ip(ip)) => SocksTarget::ResolvedLocally(Host::Ip(ip)),
(false, Host::Domain(domain)) => SocksTarget::ResolvedRemotely { name: domain },
(true, host) => ProxyTarget::ResolvedLocally(host.map_domain(UnresolvedHost::from)),
(false, Host::Ip(ip)) => ProxyTarget::ResolvedLocally(Host::Ip(ip)),
(false, Host::Domain(domain)) => ProxyTarget::ResolvedRemotely { name: domain },
};
let unresolved_route = TlsRoute {
fragment: TlsRouteFragment {
Expand Down
4 changes: 3 additions & 1 deletion rust/net/infra/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ either = "1.10.0"
futures-util = { workspace = true }
http = { workspace = true }
http-body-util = "0.1.1"
hyper = { version = "1.3.1", features = ["http2", "client"] }
hyper = { version = "1.3.1", features = ["http1", "http2", "client"] }
hyper-util = { version = "0.1.3", features = ["tokio"] }
indexmap = { workspace = true }
itertools = { workspace = true }
Expand Down Expand Up @@ -58,13 +58,15 @@ warp = { version = "0.3.6", features = ["tls"], optional = true }
assert_matches = { workspace = true }
env_logger = { workspace = true }
hickory-proto = "0.24.1"
hyper = { version = "1.3.1", features = ["http1", "server"] }
lazy_static = { workspace = true }
pretty_assertions = { workspace = true }
proptest = { workspace = true }
rcgen = "0.13.0"
snow = { workspace = true, features = ["default-resolver"] }
socks5-server = "0.10.1"
test-case = { workspace = true }
test-log = { workspace = true }
tls-parser = "0.11.0"
tokio = { workspace = true, features = ["test-util", "io-std", "rt-multi-thread"] }
warp = { version = "0.3.6", features = ["tls"] }
2 changes: 1 addition & 1 deletion rust/net/infra/src/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ mod test {
address: Host::Domain(UnresolvedHost("socks-proxy".into())),
port: PROXY_PORT,
},
target_addr: SocksTarget::ResolvedRemotely {
target_addr: ProxyTarget::ResolvedRemotely {
name: "direct-target".into(),
},
target_port: TARGET_PORT,
Expand Down
37 changes: 27 additions & 10 deletions rust/net/infra/src/route/describe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ use crate::dns::dns_utils::log_safe_domain;
use crate::errors::LogSafeDisplay;
use crate::host::Host;
use crate::route::{
ConnectionProxyKind, ConnectionProxyRoute, Connector, DirectOrProxyRoute, HttpsTlsRoute,
ResolveHostnames, ResolvedRoute, RouteDelayPolicy, SocksRoute, SocksTarget, TcpRoute, TlsRoute,
UnresolvedHost, UnresolvedWebsocketServiceRoute, DEFAULT_HTTPS_PORT,
ConnectionProxyKind, ConnectionProxyRoute, Connector, DirectOrProxyRoute,
HttpProxyRouteFragment, HttpsProxyRoute, HttpsTlsRoute, ProxyTarget, ResolveHostnames,
ResolvedRoute, RouteDelayPolicy, SocksRoute, TcpRoute, TlsRoute, UnresolvedHost,
UnresolvedWebsocketServiceRoute, DEFAULT_HTTPS_PORT,
};

/// A type that is not itself loggable but can produce a [`LogSafeDisplay`]
Expand Down Expand Up @@ -174,13 +175,16 @@ impl DescribeForLog for UnresolvedWebsocketServiceRoute {
target_addr,
target_port,
..
}) => (
match target_addr {
SocksTarget::ResolvedLocally(host) => host.clone().map_domain(Arc::from),
SocksTarget::ResolvedRemotely { name } => Host::Domain(name.clone()),
},
*target_port,
),
}) => (target_addr.as_informational_host(), *target_port),
ConnectionProxyRoute::Https(HttpsProxyRoute {
fragment:
HttpProxyRouteFragment {
target_host,
target_port,
..
},
inner: _,
}) => (target_host.as_informational_host(), *target_port),
},
};

Expand All @@ -197,3 +201,16 @@ impl DescribeForLog for UnresolvedWebsocketServiceRoute {
}
}
}

impl ProxyTarget<Host<UnresolvedHost>> {
/// Returns a [`Host`] suitable for informational purposes.
///
/// The returned type doesn't carry the locally-/remotely-resolved
/// distinction, so use with caution!
fn as_informational_host(&self) -> Host<Arc<str>> {
match self {
ProxyTarget::ResolvedLocally(host) => host.clone().map_domain(Arc::from),
ProxyTarget::ResolvedRemotely { name } => Host::Domain(name.clone()),
}
}
}
Loading

0 comments on commit 74b524c

Please sign in to comment.