mirror of
https://github.com/signalapp/libsignal.git
synced 2024-09-20 03:52:17 +02:00
net: "Poison" the TCP connector if an invalid proxy is set
This ensures that if there's an error setting a proxy, the previous settings won't continue to be used for new connections. This only applies to the Java, Swift, and TypeScript layers; the Rust layer's set_proxy isn't a fallible API in the first place today. The Java API now explicitly throws a checked IOException instead of IllegalArgumentException.
This commit is contained in:
parent
62d88b1198
commit
a09eb567f0
@ -5,6 +5,8 @@
|
||||
|
||||
package org.signal.libsignal.net;
|
||||
|
||||
import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Consumer;
|
||||
@ -46,8 +48,11 @@ public class Network {
|
||||
*
|
||||
* <p>Sets a domain name and port to be used to proxy all new outgoing connections. The proxy can
|
||||
* be overridden by calling this method again or unset by calling {@link #clearProxy}.
|
||||
*
|
||||
* @throws IOException if the host or port are not (structurally) valid, such as a port that
|
||||
* doesn't fit in u16.
|
||||
*/
|
||||
public void setProxy(String host, int port) {
|
||||
public void setProxy(String host, int port) throws IOException {
|
||||
this.connectionManager.setProxy(host, port);
|
||||
}
|
||||
|
||||
@ -120,11 +125,10 @@ public class Network {
|
||||
super(Native.ConnectionManager_new(env.value, userAgent));
|
||||
}
|
||||
|
||||
private void setProxy(String host, int port) {
|
||||
if (port == 0) {
|
||||
throw new IllegalArgumentException("Port cannot be zero");
|
||||
}
|
||||
guardedRun(nativeHandle -> Native.ConnectionManager_set_proxy(nativeHandle, host, port));
|
||||
private void setProxy(String host, int port) throws IOException {
|
||||
filterExceptions(
|
||||
IOException.class,
|
||||
() -> guardedRunChecked(h -> Native.ConnectionManager_set_proxy(h, host, port)));
|
||||
}
|
||||
|
||||
private void clearProxy() {
|
||||
|
@ -7,8 +7,10 @@ package org.signal.libsignal.net;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Test;
|
||||
import org.signal.libsignal.internal.Native;
|
||||
@ -139,4 +141,19 @@ public class ChatServiceTest {
|
||||
chat.connectUnauthenticated().get();
|
||||
chat.disconnect();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectFailsWithInvalidProxy() throws Exception {
|
||||
// The default TLS proxy config doesn't support staging, so we connect to production.
|
||||
final Network net = new Network(Network.Environment.PRODUCTION, USER_AGENT);
|
||||
assertThrows(IOException.class, () -> net.setProxy("signalfoundation.org", 0));
|
||||
assertThrows(IOException.class, () -> net.setProxy("signalfoundation.org", 100_000));
|
||||
assertThrows(IOException.class, () -> net.setProxy("signalfoundation.org", -1));
|
||||
|
||||
final ChatService chat = net.createChatService("", "");
|
||||
// Make sure we *can't* connect.
|
||||
final ExecutionException failure =
|
||||
assertThrows(ExecutionException.class, () -> chat.connectUnauthenticated().get());
|
||||
assertTrue(failure.getCause().toString(), failure.getCause() instanceof ChatServiceException);
|
||||
}
|
||||
}
|
||||
|
@ -176,7 +176,7 @@ public final class Native {
|
||||
public static native void ConnectionManager_Destroy(long handle);
|
||||
public static native void ConnectionManager_clear_proxy(long connectionManager);
|
||||
public static native long ConnectionManager_new(int environment, String userAgent);
|
||||
public static native void ConnectionManager_set_proxy(long connectionManager, String host, int port);
|
||||
public static native void ConnectionManager_set_proxy(long connectionManager, String host, int port) throws Exception;
|
||||
|
||||
public static native void CreateCallLinkCredentialPresentation_CheckValidContents(byte[] presentationBytes) throws Exception;
|
||||
public static native void CreateCallLinkCredentialPresentation_Verify(byte[] presentationBytes, byte[] roomId, long now, byte[] serverParamsBytes, byte[] callLinkParamsBytes) throws Exception;
|
||||
|
@ -219,6 +219,8 @@ export class Net {
|
||||
* Sets a domain name and port to be used to proxy all new outgoing
|
||||
* connections. The proxy can be overridden by calling this method again or
|
||||
* unset by calling {@link #clearProxy}.
|
||||
*
|
||||
* Throws if the host or port is structurally invalid, such as a port that doesn't fit in u16.
|
||||
*/
|
||||
setProxy(host: string, port: number): void {
|
||||
Native.ConnectionManager_set_proxy(this.connectionManager, host, port);
|
||||
|
@ -151,6 +151,21 @@ describe('chat service api', () => {
|
||||
await chatService.disconnect();
|
||||
}).timeout(10000);
|
||||
});
|
||||
|
||||
it('cannot connect through an invalid proxy', async () => {
|
||||
// The default TLS proxy config doesn't support staging, so we connect to production.
|
||||
const net = new Net(Environment.Production, userAgent);
|
||||
expect(() => net.setProxy('signalfoundation.org', 0)).throws(Error);
|
||||
expect(() => net.setProxy('signalfoundation.org', 100_000)).throws(Error);
|
||||
expect(() => net.setProxy('signalfoundation.org', -1)).throws(Error);
|
||||
expect(() => net.setProxy('signalfoundation.org', 0.1)).throws(Error);
|
||||
|
||||
const chatService = net.newChatService();
|
||||
// Make sure we *can't* connect.
|
||||
await expect(chatService.connectUnauthenticated()).to.be.rejectedWith(
|
||||
LibSignalErrorBase
|
||||
);
|
||||
}).timeout(10000);
|
||||
});
|
||||
|
||||
describe('cdsi lookup', () => {
|
||||
|
@ -788,6 +788,7 @@ trivial!(bool);
|
||||
macro_rules! ffi_arg_type {
|
||||
(u8) => (u8);
|
||||
(u16) => (u16);
|
||||
(i32) => (i32);
|
||||
(u32) => (u32);
|
||||
(u64) => (u64);
|
||||
(Option<u32>) => (u32);
|
||||
|
@ -1244,6 +1244,9 @@ macro_rules! jni_arg_type {
|
||||
(u16) => {
|
||||
jni::jint
|
||||
};
|
||||
(i32) => {
|
||||
jni::jint
|
||||
};
|
||||
(u32) => {
|
||||
jni::jint
|
||||
};
|
||||
|
@ -28,7 +28,8 @@ use libsignal_net::env::{add_user_agent_header, Env, Svr3Env};
|
||||
use libsignal_net::infra::connection_manager::MultiRouteConnectionManager;
|
||||
use libsignal_net::infra::dns::DnsResolver;
|
||||
use libsignal_net::infra::tcp_ssl::{
|
||||
DirectConnector as TcpSslDirectConnector, TcpSslConnector, TcpSslConnectorStream,
|
||||
DirectConnector as TcpSslDirectConnector, ProxyConnector as TcpSslProxyConnector,
|
||||
TcpSslConnector, TcpSslConnectorStream,
|
||||
};
|
||||
use libsignal_net::infra::{make_ws_config, EndpointConnection};
|
||||
use libsignal_net::svr::{self, SvrConnection};
|
||||
@ -130,18 +131,40 @@ fn ConnectionManager_new(
|
||||
fn ConnectionManager_set_proxy(
|
||||
connection_manager: &ConnectionManager,
|
||||
host: String,
|
||||
port: AsType<NonZeroU16, u16>,
|
||||
) {
|
||||
let port = port.into_inner();
|
||||
let proxy_addr = (host.as_str(), port);
|
||||
port: i32,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let mut guard = connection_manager
|
||||
.transport_connector
|
||||
.lock()
|
||||
.expect("not poisoned");
|
||||
match &mut *guard {
|
||||
TcpSslConnector::Direct(direct) => *guard = direct.with_proxy(proxy_addr).into(),
|
||||
TcpSslConnector::Proxied(proxied) => proxied.set_proxy(proxy_addr),
|
||||
};
|
||||
// We take port as an i32 because Java 'short' is signed and thus can't represent all port
|
||||
// numbers, and we want too-large port numbers to be handled the same way as 0.
|
||||
match u16::try_from(port)
|
||||
.ok()
|
||||
.and_then(|port| NonZeroU16::try_from(port).ok())
|
||||
{
|
||||
Some(port) => {
|
||||
let proxy_addr = (host.as_str(), port);
|
||||
match &mut *guard {
|
||||
TcpSslConnector::Direct(direct) => *guard = direct.with_proxy(proxy_addr).into(),
|
||||
TcpSslConnector::Proxied(proxied) => proxied.set_proxy(proxy_addr),
|
||||
TcpSslConnector::Invalid(dns_resolver) => {
|
||||
*guard = TcpSslProxyConnector::new(dns_resolver.clone(), proxy_addr).into()
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
match &*guard {
|
||||
TcpSslConnector::Direct(TcpSslDirectConnector { dns_resolver, .. })
|
||||
| TcpSslConnector::Proxied(TcpSslProxyConnector { dns_resolver, .. }) => {
|
||||
*guard = TcpSslConnector::Invalid(dns_resolver.clone())
|
||||
}
|
||||
TcpSslConnector::Invalid(_dns_resolver) => (),
|
||||
}
|
||||
Err(std::io::ErrorKind::InvalidInput.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[bridge_fn]
|
||||
@ -152,8 +175,9 @@ fn ConnectionManager_clear_proxy(connection_manager: &ConnectionManager) {
|
||||
.expect("not poisoned");
|
||||
match &*guard {
|
||||
TcpSslConnector::Direct(_direct) => (),
|
||||
TcpSslConnector::Proxied(proxied) => {
|
||||
*guard = TcpSslDirectConnector::new(proxied.dns_resolver.clone()).into()
|
||||
TcpSslConnector::Proxied(TcpSslProxyConnector { dns_resolver, .. })
|
||||
| TcpSslConnector::Invalid(dns_resolver) => {
|
||||
*guard = TcpSslDirectConnector::new(dns_resolver.clone()).into()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -14,6 +14,8 @@ pub trait LogSafeDisplay: Display {}
|
||||
/// Errors that can occur during transport-level connection establishment.
|
||||
#[derive(displaydoc::Display, Debug, thiserror::Error)]
|
||||
pub enum TransportConnectError {
|
||||
/// Invalid configuration for this connection
|
||||
InvalidConfiguration,
|
||||
/// Failed to establish TCP connection to any of the IPs
|
||||
TcpConnectionFailed,
|
||||
/// DNS lookup failed
|
||||
@ -96,6 +98,7 @@ impl From<TransportConnectError> for std::io::Error {
|
||||
fn from(value: TransportConnectError) -> Self {
|
||||
use std::io::ErrorKind;
|
||||
let kind = match value {
|
||||
TransportConnectError::InvalidConfiguration => ErrorKind::InvalidInput,
|
||||
TransportConnectError::TcpConnectionFailed => ErrorKind::ConnectionRefused,
|
||||
TransportConnectError::SslFailedHandshake(_)
|
||||
| TransportConnectError::SslError(_)
|
||||
|
@ -30,13 +30,17 @@ const CONNECTION_ATTEMPT_DELAY: Duration = Duration::from_millis(200);
|
||||
pub enum TcpSslConnector {
|
||||
Direct(DirectConnector),
|
||||
Proxied(ProxyConnector),
|
||||
/// Used when configuring one of the other kinds of connector isn't possible, perhaps because
|
||||
/// invalid configuration options were provided.
|
||||
Invalid(DnsResolver),
|
||||
}
|
||||
|
||||
impl TcpSslConnector {
|
||||
pub fn set_ipv6_enabled(&mut self, ipv6_enabled: bool) {
|
||||
let dns_resolver = match self {
|
||||
TcpSslConnector::Direct(ref mut c) => &mut c.dns_resolver,
|
||||
TcpSslConnector::Proxied(ref mut c) => &mut c.dns_resolver,
|
||||
TcpSslConnector::Direct(c) => &mut c.dns_resolver,
|
||||
TcpSslConnector::Proxied(c) => &mut c.dns_resolver,
|
||||
TcpSslConnector::Invalid(resolver) => resolver,
|
||||
};
|
||||
dns_resolver.set_ipv6_enabled(ipv6_enabled);
|
||||
}
|
||||
@ -51,7 +55,7 @@ pub struct TcpSslConnectorStream(
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DirectConnector {
|
||||
dns_resolver: DnsResolver,
|
||||
pub dns_resolver: DnsResolver,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@ -323,6 +327,7 @@ impl TransportConnector for TcpSslConnector {
|
||||
.connect(connection_params, alpn)
|
||||
.await
|
||||
.map(|s| s.map_stream(Either::Right)),
|
||||
Self::Invalid(_) => Err(TransportConnectError::InvalidConfiguration),
|
||||
}
|
||||
.map(|s| s.map_stream(TcpSslConnectorStream))
|
||||
}
|
||||
@ -672,4 +677,32 @@ mod test {
|
||||
|
||||
make_http_request_response_over(stream).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn connect_through_invalid() {
|
||||
let (addr, server) = localhost_http_server();
|
||||
let _server_handle = tokio::spawn(server);
|
||||
|
||||
let connector = TcpSslConnector::Invalid(DnsResolver::new_with_static_fallback(
|
||||
HashMap::from([(SERVER_HOSTNAME, LookupResult::localhost())]),
|
||||
));
|
||||
let connection_params = ConnectionParams {
|
||||
route_type: RouteType::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(Cow::Borrowed(SERVER_CERTIFICATE.cert.der())),
|
||||
};
|
||||
|
||||
match connector.connect(&connection_params, Alpn::Http1_1).await {
|
||||
Ok(_) => {
|
||||
// We can't use expect_err() or assert_matches! because the success case isn't Debug.
|
||||
panic!("should have failed");
|
||||
}
|
||||
Err(e) => {
|
||||
assert_matches!(e, TransportConnectError::InvalidConfiguration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,8 @@ public class Net {
|
||||
///
|
||||
/// Sets a domain name and port to be used to proxy all new outgoing connections. The proxy can
|
||||
/// be overridden by calling this method again or unset by calling ``Net/clearProxy()``.
|
||||
///
|
||||
/// - Throws: if the host or port is not structurally valid, such as a port of 0.
|
||||
public func setProxy(host: String, port: UInt16) throws {
|
||||
try self.connectionManager.setProxy(host: host, port: port)
|
||||
}
|
||||
@ -380,11 +382,9 @@ internal class ConnectionManager: NativeHandleOwner {
|
||||
}
|
||||
|
||||
internal func setProxy(host: String, port: UInt16) throws {
|
||||
if port == 0 {
|
||||
throw SignalError.invalidArgument("port cannot be 0")
|
||||
}
|
||||
self.withNativeHandle {
|
||||
failOnError(signal_connection_manager_set_proxy($0, host, port))
|
||||
try self.withNativeHandle {
|
||||
// We have to cast to Int32 because of how the port number is validated...for Java.
|
||||
try checkError(signal_connection_manager_set_proxy($0, host, Int32(port)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1395,7 +1395,7 @@ SignalFfiError *signal_verify_signature(bool *out, SignalBorrowedBuffer cert_pem
|
||||
|
||||
SignalFfiError *signal_connection_manager_new(SignalConnectionManager **out, uint8_t environment, const char *user_agent);
|
||||
|
||||
SignalFfiError *signal_connection_manager_set_proxy(const SignalConnectionManager *connection_manager, const char *host, uint16_t port);
|
||||
SignalFfiError *signal_connection_manager_set_proxy(const SignalConnectionManager *connection_manager, const char *host, int32_t port);
|
||||
|
||||
SignalFfiError *signal_connection_manager_clear_proxy(const SignalConnectionManager *connection_manager);
|
||||
|
||||
|
@ -3,15 +3,15 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
// These testing endpoints aren't generated in device builds, to save on code size.
|
||||
#if !os(iOS) || targetEnvironment(simulator)
|
||||
|
||||
import Foundation
|
||||
@testable import LibSignalClient
|
||||
import SignalFfi
|
||||
import XCTest
|
||||
|
||||
final class ChatServiceTests: TestCaseBase {
|
||||
// These testing endpoints aren't generated in device builds, to save on code size.
|
||||
#if !os(iOS) || targetEnvironment(simulator)
|
||||
|
||||
private static let userAgent = "test"
|
||||
private static let expectedStatus: UInt16 = 200
|
||||
private static let expectedMessage = "OK"
|
||||
@ -72,11 +72,13 @@ final class ChatServiceTests: TestCaseBase {
|
||||
func testConvertError() throws {
|
||||
do {
|
||||
try checkError(signal_testing_chat_service_error_convert())
|
||||
XCTFail("error not thrown")
|
||||
} catch SignalError.connectionTimeoutError(_) {
|
||||
// Okay
|
||||
}
|
||||
do {
|
||||
try checkError(signal_testing_chat_service_inactive_error_convert())
|
||||
XCTFail("error not thrown")
|
||||
} catch SignalError.chatServiceInactive(_) {
|
||||
// Okay
|
||||
}
|
||||
@ -106,6 +108,8 @@ final class ChatServiceTests: TestCaseBase {
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
func testConnectUnauth() async throws {
|
||||
// Use the presence of the proxy server environment setting to know whether we should make network requests in our tests.
|
||||
guard ProcessInfo.processInfo.environment["LIBSIGNAL_TESTING_PROXY_SERVER"] != nil else {
|
||||
@ -142,6 +146,24 @@ final class ChatServiceTests: TestCaseBase {
|
||||
try await chat.connectUnauthenticated()
|
||||
try await chat.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
func testConnectFailsWithInvalidProxy() async throws {
|
||||
// The default TLS proxy config doesn't support staging, so we connect to production.
|
||||
let net = Net(env: .production, userAgent: Self.userAgent)
|
||||
do {
|
||||
try net.setProxy(host: "signalfoundation.org", port: 0)
|
||||
XCTFail("should not allow setting invalid proxy")
|
||||
} catch SignalError.ioError {
|
||||
// Okay
|
||||
}
|
||||
|
||||
let chat = net.createChatService(username: "", password: "")
|
||||
// Make sure we *can't* connect.
|
||||
do {
|
||||
try await chat.connectUnauthenticated()
|
||||
XCTFail("should not allow connecting")
|
||||
} catch SignalError.connectionFailed {
|
||||
// Okay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user