Skip to content

Commit bd9a11f

Browse files
authored
bridge: Separate proxy validation from proxy setting
And expose an explicit setInvalidProxy, both for testing and in case apps want to do their own connection-poisoning. This makes it easier to be consistent about "if you try to set a proxy that turns out not to be valid, the ConnectionManager should end up in the invalid state until explicitly cleared", whether the validation is done on the Rust side of the bridge or the app language side.
1 parent bf680b0 commit bd9a11f

File tree

13 files changed

+265
-215
lines changed

13 files changed

+265
-215
lines changed

RELEASE_NOTES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,9 @@ v0.65.5
33
- Introduces an overload of `Net.setProxy()` that supports HTTP and SOCKS proxies in addition to the
44
"transparent TLS proxies" already supported. Supported schemes: "socks5" (or just "socks"),
55
"socks5h", "socks4", "socks4a", "https", "http", and "org.signal.tls".
6+
7+
- `Net.setInvalidProxy()` disables new connections until the proxy settings are updated.
8+
9+
- Desktop: `Net.setProxyFromUrl()` translates from URL syntax for specifying a proxy.
10+
611
- keytrans: Verify consistency proofs

java/client/src/main/java/org/signal/libsignal/net/Network.java

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,21 @@ public void setProxy(String host, int port) throws IOException {
9393
this.connectionManager.setProxy(SIGNAL_TLS_PROXY_SCHEME, host, port, username, null);
9494
}
9595

96+
/**
97+
* Refuses to make any new connections until a new proxy configuration is set or {@link
98+
* #clearProxy} is called.
99+
*
100+
* <p>Existing connections will not be affected.
101+
*/
102+
public void setInvalidProxy() {
103+
this.connectionManager.setInvalidProxy();
104+
}
105+
96106
/**
97107
* Ensures that future connections will be made directly, not through a proxy.
98108
*
99-
* <p>Clears any proxy configuration set via {@link #setProxy}. If none was set, calling this
100-
* method is a no-op.
109+
* <p>Clears any proxy configuration set via {@link #setProxy} or {@link #setInvalidProxy}. If
110+
* none was set, calling this method is a no-op.
101111
*
102112
* <p>Existing connections and services will continue with the setting they were created with. (In
103113
* particular, changing this setting will not affect any existing {@link ChatService
@@ -236,20 +246,34 @@ private ConnectionManager(Environment env, String userAgent) {
236246
private void setProxy(
237247
String scheme, String host, Integer port, String username, String password)
238248
throws IOException {
239-
filterExceptions(
240-
IOException.class,
241-
() ->
242-
guardedRunChecked(
243-
h ->
244-
Native.ConnectionManager_set_proxy(
245-
h,
246-
scheme,
247-
host,
248-
// Integer.MIN_VALUE represents "no port provided"; we don't expect anyone
249-
// to pass that manually.
250-
port != null ? port : Integer.MIN_VALUE,
251-
username,
252-
password)));
249+
long rawProxyConfig;
250+
try {
251+
rawProxyConfig =
252+
filterExceptions(
253+
IOException.class,
254+
() ->
255+
Native.ConnectionProxyConfig_new(
256+
scheme,
257+
host,
258+
// Integer.MIN_VALUE represents "no port provided"; we don't expect anyone
259+
// to pass that manually.
260+
port != null ? port : Integer.MIN_VALUE,
261+
username,
262+
password));
263+
} catch (IOException | RuntimeException | Error e) {
264+
setInvalidProxy();
265+
throw e;
266+
}
267+
268+
try {
269+
guardedRun(h -> Native.ConnectionManager_set_proxy(h, rawProxyConfig));
270+
} finally {
271+
Native.ConnectionProxyConfig_Destroy(rawProxyConfig);
272+
}
273+
}
274+
275+
private void setInvalidProxy() {
276+
guardedRun(Native::ConnectionManager_set_invalid_proxy);
253277
}
254278

255279
private void clearProxy() {

java/client/src/test/java/org/signal/libsignal/net/ChatServiceTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,12 @@ public void testInvalidProxyRejected() throws Exception {
323323
check.accept(() -> net.setProxy("signalfoundation.org", -1));
324324

325325
check.accept(() -> net.setProxy("socks+shoes", "signalfoundation.org", null, null, null));
326+
327+
check.accept(
328+
() -> {
329+
net.setInvalidProxy();
330+
throw new IOException("to match all the other test cases");
331+
});
326332
}
327333

328334
private void injectServerRequest(ChatService chat, String requestBase64) {

java/shared/java/org/signal/libsignal/internal/Native.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,11 @@ private Native() {}
250250
public static native long ConnectionManager_new(int environment, String userAgent);
251251
public static native void ConnectionManager_on_network_change(long connectionManager);
252252
public static native void ConnectionManager_set_censorship_circumvention_enabled(long connectionManager, boolean enabled);
253-
public static native void ConnectionManager_set_proxy(long connectionManager, String scheme, String host, int port, String username, String password) throws Exception;
253+
public static native void ConnectionManager_set_invalid_proxy(long connectionManager);
254+
public static native void ConnectionManager_set_proxy(long connectionManager, long proxy);
255+
256+
public static native void ConnectionProxyConfig_Destroy(long handle);
257+
public static native long ConnectionProxyConfig_new(String scheme, String host, int port, String username, String password) throws Exception;
254258

255259
public static native void CreateCallLinkCredentialPresentation_CheckValidContents(byte[] presentationBytes) throws Exception;
256260
public static native void CreateCallLinkCredentialPresentation_Verify(byte[] presentationBytes, byte[] roomId, long now, byte[] serverParamsBytes, byte[] callLinkParamsBytes) throws Exception;

node/Native.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,10 @@ export function ConnectionManager_clear_proxy(connectionManager: Wrapper<Connect
233233
export function ConnectionManager_new(environment: number, userAgent: string): ConnectionManager;
234234
export function ConnectionManager_on_network_change(connectionManager: Wrapper<ConnectionManager>): void;
235235
export function ConnectionManager_set_censorship_circumvention_enabled(connectionManager: Wrapper<ConnectionManager>, enabled: boolean): void;
236+
export function ConnectionManager_set_invalid_proxy(connectionManager: Wrapper<ConnectionManager>): void;
236237
export function ConnectionManager_set_ipv6_enabled(connectionManager: Wrapper<ConnectionManager>, ipv6Enabled: boolean): void;
237-
export function ConnectionManager_set_proxy(connectionManager: Wrapper<ConnectionManager>, scheme: string, host: string, port: number, username: string | null, password: string | null): void;
238+
export function ConnectionManager_set_proxy(connectionManager: Wrapper<ConnectionManager>, proxy: Wrapper<ConnectionProxyConfig>): void;
239+
export function ConnectionProxyConfig_new(scheme: string, host: string, port: number, username: string | null, password: string | null): ConnectionProxyConfig;
238240
export function CreateCallLinkCredentialPresentation_CheckValidContents(presentationBytes: Buffer): void;
239241
export function CreateCallLinkCredentialPresentation_Verify(presentationBytes: Buffer, roomId: Buffer, now: Timestamp, serverParamsBytes: Buffer, callLinkParamsBytes: Buffer): void;
240242
export function CreateCallLinkCredentialRequestContext_CheckValidContents(contextBytes: Buffer): void;
@@ -617,6 +619,7 @@ interface CiphertextMessage { readonly __type: unique symbol; }
617619
interface ComparableBackup { readonly __type: unique symbol; }
618620
interface ComparableBackup { readonly __type: unique symbol; }
619621
interface ConnectionManager { readonly __type: unique symbol; }
622+
interface ConnectionProxyConfig { readonly __type: unique symbol; }
620623
interface DecryptionErrorMessage { readonly __type: unique symbol; }
621624
interface ExpiringProfileKeyCredential { readonly __type: unique symbol; }
622625
interface ExpiringProfileKeyCredentialResponse { readonly __type: unique symbol; }

node/ts/net.ts

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -907,15 +907,22 @@ export class Net {
907907
};
908908
}
909909
const { scheme, host, port, username, password } = hostOrOptions;
910-
Native.ConnectionManager_set_proxy(
911-
this._connectionManager,
912-
scheme,
913-
host,
914-
// i32::MIN represents "no port provided"; we don't expect anyone to pass that manually.
915-
port ?? -0x8000_0000,
916-
username ?? null,
917-
password ?? null
918-
);
910+
try {
911+
const proxyConfig = newNativeHandle(
912+
Native.ConnectionProxyConfig_new(
913+
scheme,
914+
host,
915+
// i32::MIN represents "no port provided"; we don't expect anyone to pass that manually.
916+
port ?? -0x8000_0000,
917+
username ?? null,
918+
password ?? null
919+
)
920+
);
921+
Native.ConnectionManager_set_proxy(this._connectionManager, proxyConfig);
922+
} catch (e) {
923+
this.setInvalidProxy();
924+
throw e;
925+
}
919926
}
920927

921928
/**
@@ -936,11 +943,7 @@ export class Net {
936943
} catch (e) {
937944
// Make sure we set an invalid proxy on error,
938945
// so no connection can be made until the problem is fixed.
939-
try {
940-
this.setProxy({ scheme: 'https', host: '' });
941-
} catch (_) {
942-
// Ignore any errors here, we want to rethrow the original error.
943-
}
946+
this.setInvalidProxy();
944947
throw e;
945948
}
946949

@@ -985,11 +988,21 @@ export class Net {
985988
return { scheme, username, password, host, port };
986989
}
987990

991+
/**
992+
* Refuses to make any new connections until a new proxy configuration is set or
993+
* {@link #clearProxy} is called.
994+
*
995+
* Existing connections will not be affected.
996+
*/
997+
setInvalidProxy(): void {
998+
Native.ConnectionManager_set_invalid_proxy(this._connectionManager);
999+
}
1000+
9881001
/**
9891002
* Ensures that future connections will be made directly, not through a proxy.
9901003
*
991-
* Clears any proxy configuration set via {@link #setProxy}. If none was set, calling this
992-
* method is a no-op.
1004+
* Clears any proxy configuration set via {@link #setProxy} or {@link #setInvalidProxy}. If none
1005+
* was set, calling this method is a no-op.
9931006
*/
9941007
clearProxy(): void {
9951008
Native.ConnectionManager_clear_proxy(this._connectionManager);

node/ts/test/NetTest.ts

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -180,34 +180,24 @@ describe('chat service api', () => {
180180
userAgent: userAgent,
181181
});
182182

183-
function check(callback: () => void, expectedProxyMode: number = -1): void {
183+
function check(callback: () => void): void {
184184
expect(
185185
TESTING_ConnectionManager_isUsingProxy(net._connectionManager)
186186
).equals(0);
187187
expect(callback).throws(Error);
188188
expect(
189189
TESTING_ConnectionManager_isUsingProxy(net._connectionManager)
190-
).equals(expectedProxyMode);
190+
).equals(-1);
191191
net.clearProxy();
192192
}
193193

194194
check(() => net.setProxy('signalfoundation.org', 0));
195195
check(() => net.setProxy('signalfoundation.org', 100_000));
196196
check(() => net.setProxy('signalfoundation.org', -1));
197-
198-
// Ideally these would poison the Net instance like all the other invalid options,
199-
// but unfortunately it's checked at a layer that makes that awkward.
200-
// Hopefully no one actually tries to set a fractional or very large port!
201-
check(() => net.setProxy('signalfoundation.org', 0.1), 0);
202-
check(
203-
() => net.setProxy('signalfoundation.org', Number.MAX_SAFE_INTEGER),
204-
0
205-
);
206-
check(() => net.setProxy('signalfoundation.org', Number.MAX_VALUE), 0);
207-
check(
208-
() => net.setProxy('signalfoundation.org', Number.POSITIVE_INFINITY),
209-
0
210-
);
197+
check(() => net.setProxy('signalfoundation.org', 0.1));
198+
check(() => net.setProxy('signalfoundation.org', Number.MAX_SAFE_INTEGER));
199+
check(() => net.setProxy('signalfoundation.org', Number.MAX_VALUE));
200+
check(() => net.setProxy('signalfoundation.org', Number.POSITIVE_INFINITY));
211201

212202
check(() =>
213203
net.setProxy({ scheme: 'socks+shoes', host: 'signalfoundation.org' })
@@ -227,6 +217,11 @@ describe('chat service api', () => {
227217
'https://signalfoundation.org#fragment-for-some-reason'
228218
)
229219
);
220+
221+
check(() => {
222+
net.setInvalidProxy();
223+
throw new Error('to match the behavior of all the other calls');
224+
});
230225
});
231226

232227
it('parses proxy URLs the way we expect, if not always ideally', () => {

rust/bridge/shared/src/net.rs

Lines changed: 62 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use libsignal_bridge_macros::bridge_fn;
1010
pub use libsignal_bridge_types::net::{ConnectionManager, Environment, TokioAsyncContext};
1111
use libsignal_net::auth::Auth;
1212
use libsignal_net::chat::ConnectionInfo;
13+
use libsignal_net::infra::errors::LogSafeDisplay;
14+
use libsignal_net::infra::route::ConnectionProxyConfig;
1315

1416
use crate::support::*;
1517
use crate::*;
@@ -21,43 +23,86 @@ mod tokio;
2123

2224
bridge_handle_fns!(ConnectionInfo, clone = false, jni = false);
2325

24-
bridge_handle_fns!(ConnectionManager, clone = false);
25-
26-
#[bridge_fn]
27-
fn ConnectionManager_new(
28-
environment: AsType<Environment, u8>,
29-
user_agent: String,
30-
) -> ConnectionManager {
31-
ConnectionManager::new(environment.into_inner(), user_agent.as_str())
32-
}
26+
bridge_handle_fns!(ConnectionProxyConfig);
3327

3428
#[bridge_fn]
35-
fn ConnectionManager_set_proxy(
36-
connection_manager: &ConnectionManager,
29+
fn ConnectionProxyConfig_new(
3730
scheme: String,
3831
host: String,
3932
port: i32,
4033
username: Option<String>,
4134
password: Option<String>,
42-
) -> Result<(), std::io::Error> {
35+
) -> Result<ConnectionProxyConfig, std::io::Error> {
4336
// We take port as an i32 because Java 'short' is signed and thus can't represent all port
4437
// numbers, and we want too-large port numbers to be handled the same way as 0. However, we
4538
// *also* want to have a representation that means "no port provided". We'll use something
4639
// unlikely for anyone to have typed manually, especially in decimal: `i32::MIN`. (We're not
4740
// using 0 as the placeholder because an explicitly-specified zero should be diagnosed as
48-
// invalid.) Note that we also *don't* return early from this function if the port is invalid;
49-
// that should poison the connection manager. See [`ConnectionManager::set_proxy`].
50-
let port: Option<Result<NonZeroU16, i32>> = if port == i32::MIN {
41+
// invalid.)
42+
let port = if port == i32::MIN {
5143
None
5244
} else {
5345
Some(
5446
u16::try_from(port)
5547
.ok()
5648
.and_then(NonZeroU16::new)
57-
.ok_or(port),
49+
.ok_or_else(|| {
50+
std::io::Error::new(
51+
std::io::ErrorKind::InvalidInput,
52+
format!("invalid port '{port}'"),
53+
)
54+
})?,
5855
)
5956
};
60-
connection_manager.set_proxy(&scheme, &host, port, username, password)
57+
58+
let auth = match (username, password) {
59+
(None, None) => None,
60+
(None, Some(_)) => {
61+
return Err(std::io::Error::new(
62+
std::io::ErrorKind::InvalidInput,
63+
"cannot have password without username",
64+
));
65+
}
66+
(Some(username), password) => Some((username, password.unwrap_or_default())),
67+
};
68+
69+
ConnectionProxyConfig::from_parts(&scheme, &host, port, auth).map_err(|e| {
70+
use libsignal_net::infra::route::ProxyFromPartsError;
71+
static_assertions::assert_impl_all!(ProxyFromPartsError: LogSafeDisplay);
72+
match e {
73+
ProxyFromPartsError::UnsupportedScheme(_) => {
74+
std::io::Error::new(std::io::ErrorKind::Unsupported, e.to_string())
75+
}
76+
ProxyFromPartsError::MissingHost
77+
| ProxyFromPartsError::SchemeDoesNotSupportUsernames(_)
78+
| ProxyFromPartsError::SchemeDoesNotSupportPasswords(_) => {
79+
std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string())
80+
}
81+
}
82+
})
83+
}
84+
85+
bridge_handle_fns!(ConnectionManager, clone = false);
86+
87+
#[bridge_fn]
88+
fn ConnectionManager_new(
89+
environment: AsType<Environment, u8>,
90+
user_agent: String,
91+
) -> ConnectionManager {
92+
ConnectionManager::new(environment.into_inner(), user_agent.as_str())
93+
}
94+
95+
#[bridge_fn]
96+
fn ConnectionManager_set_proxy(
97+
connection_manager: &ConnectionManager,
98+
proxy: &ConnectionProxyConfig,
99+
) {
100+
connection_manager.set_proxy(proxy.clone())
101+
}
102+
103+
#[bridge_fn]
104+
fn ConnectionManager_set_invalid_proxy(connection_manager: &ConnectionManager) {
105+
connection_manager.set_invalid_proxy()
61106
}
62107

63108
#[bridge_fn]
@@ -93,16 +138,3 @@ fn CreateOTPFromBase64(username: String, secret: String) -> String {
93138
let secret = BASE64_STANDARD.decode(secret).expect("valid base64");
94139
Auth::otp(&username, &secret, std::time::SystemTime::now())
95140
}
96-
97-
#[cfg(test)]
98-
mod test {
99-
use test_case::test_case;
100-
101-
use super::*;
102-
103-
#[test_case(Environment::Staging; "staging")]
104-
#[test_case(Environment::Prod; "prod")]
105-
fn can_create_connection_manager(env: Environment) {
106-
let _ = ConnectionManager::new(env, "test-user-agent");
107-
}
108-
}

0 commit comments

Comments
 (0)