Adopt libsignal-net version with no auto-reconnect

Co-authored-by: Jordan Rose <jrose@signal.org>
This commit is contained in:
Sergey Skrobotov 2024-08-14 20:08:50 -07:00 committed by GitHub
parent 00e6071b1d
commit 30a419bb2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 139 additions and 71 deletions

View file

@ -4733,7 +4733,7 @@ For more information on this, and how to apply and follow the GNU AGPL, see
```
## attest 0.1.0, libsignal-ffi 0.54.0, libsignal-jni 0.54.0, libsignal-jni-testing 0.54.0, libsignal-node 0.54.0, signal-neon-futures 0.1.0, signal-neon-futures-tests 0.1.0, libsignal-bridge 0.1.0, libsignal-bridge-macros 0.1.0, libsignal-bridge-testing 0.1.0, libsignal-bridge-types 0.1.0, libsignal-core 0.1.0, signal-crypto 0.1.0, device-transfer 0.1.0, signal-media 0.1.0, libsignal-message-backup 0.1.0, libsignal-message-backup-macros 0.1.0, libsignal-net 0.1.0, signal-pin 0.1.0, poksho 0.7.0, libsignal-protocol 0.1.0, libsignal-svr3 0.1.0, usernames 0.1.0, zkcredential 0.1.0, zkgroup 0.9.0
## attest 0.1.0, libsignal-ffi 0.55.0, libsignal-jni 0.55.0, libsignal-jni-testing 0.55.0, libsignal-node 0.55.0, signal-neon-futures 0.1.0, signal-neon-futures-tests 0.1.0, libsignal-bridge 0.1.0, libsignal-bridge-macros 0.1.0, libsignal-bridge-testing 0.1.0, libsignal-bridge-types 0.1.0, libsignal-core 0.1.0, signal-crypto 0.1.0, device-transfer 0.1.0, signal-media 0.1.0, libsignal-message-backup 0.1.0, libsignal-message-backup-macros 0.1.0, libsignal-net 0.1.0, signal-pin 0.1.0, poksho 0.7.0, libsignal-protocol 0.1.0, libsignal-svr3 0.1.0, usernames 0.1.0, zkcredential 0.1.0, zkgroup 0.9.0
```
GNU AFFERO GENERAL PUBLIC LICENSE
@ -5986,7 +5986,7 @@ limitations under the License.
```
## boring 4.6.0
## boring 4.9.0
```
Copyright 2011-2017 Google Inc.
@ -6421,7 +6421,7 @@ express Statement of Purpose.
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
```
## boring-sys 4.6.0
## boring-sys 4.9.0
```
/* Copyright (c) 2015, Google Inc.
@ -6792,7 +6792,7 @@ DEALINGS IN THE SOFTWARE.
```
## boring-sys 4.6.0
## boring-sys 4.9.0
```
Copyright (c) 2014 Alex Crichton
@ -7113,7 +7113,7 @@ THE SOFTWARE.
```
## anstyle-wincon 3.0.4
## anstyle-wincon 3.0.3
```
Copyright (c) 2015 Josh Triplett, 2022 The rust-cli Developers
@ -7200,7 +7200,7 @@ DEALINGS IN THE SOFTWARE.
```
## gimli 0.29.0, heck 0.5.0, peeking_take_while 0.1.2, unicode-bidi 0.3.15, unicode-normalization 0.1.23
## gimli 0.29.0, heck 0.4.1, heck 0.5.0, peeking_take_while 0.1.2, unicode-bidi 0.3.15, unicode-normalization 0.1.23
```
Copyright (c) 2015 The Rust Project Developers
@ -7351,7 +7351,7 @@ DEALINGS IN THE SOFTWARE.
```
## boring-sys 4.6.0
## boring-sys 4.9.0
```
Copyright (c) 2015-2016 the fiat-crypto authors (see
@ -7579,7 +7579,7 @@ DEALINGS IN THE SOFTWARE.
```
## hashbrown 0.14.5
## hashbrown 0.12.3, hashbrown 0.14.5
```
Copyright (c) 2016 Amanieu d'Antras
@ -7641,7 +7641,7 @@ DEALINGS IN THE SOFTWARE.
```
## anstyle-parse 0.2.5
## anstyle-parse 0.2.4
```
Copyright (c) 2016 Joe Wilm and individual contributors
@ -7822,7 +7822,7 @@ SOFTWARE.
```
## tokio-boring 4.6.0
## tokio-boring 4.9.0
```
Copyright (c) 2016 Tokio contributors
@ -7885,7 +7885,7 @@ DEALINGS IN THE SOFTWARE.
```
## indexmap 2.2.6
## indexmap 1.9.3, indexmap 2.2.6
```
Copyright (c) 2016--2017
@ -8757,7 +8757,7 @@ DEALINGS IN THE SOFTWARE.
```
## protobuf-codegen 3.5.0, protobuf-json-mapping 3.5.0, protobuf-support 3.5.0, protobuf 3.5.0
## protobuf-codegen 3.4.0, protobuf-json-mapping 3.4.0, protobuf-parse 3.4.0, protobuf-support 3.4.0, protobuf 3.4.0
```
Copyright (c) 2019 Stepan Koltsov
@ -8779,7 +8779,6 @@ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.
```
## ppv-lite86 0.2.17
@ -9306,7 +9305,7 @@ DEALINGS IN THE SOFTWARE.
```
## anstyle 1.0.8
## anstyle 1.0.7
```
Copyright (c) 2022 The rust-cli Developers
@ -9518,7 +9517,7 @@ SOFTWARE.
```
## anstream 0.6.15, anstyle-query 1.1.1, clap 4.5.11, colorchoice 1.0.2, env_filter 0.1.2, env_logger 0.11.5, is_terminal_polyfill 1.70.1, toml_datetime 0.6.7, toml_edit 0.21.1
## anstream 0.6.14, anstyle-query 1.1.0, clap 4.4.18, colorchoice 1.0.1, env_filter 0.1.1, env_logger 0.11.4, is_terminal_polyfill 1.70.0, toml_datetime 0.6.6, toml_edit 0.21.1
```
Copyright (c) Individual contributors
@ -10248,7 +10247,7 @@ SOFTWARE.
```
## cesu8 1.1.0, half 2.4.1, pqcrypto-internals 0.2.5, pqcrypto-kyber 0.7.9, pqcrypto-kyber 0.8.1, pqcrypto-traits 0.3.5, protobuf-parse 3.5.0
## cesu8 1.1.0, half 2.4.1, pqcrypto-internals 0.2.5, pqcrypto-kyber 0.7.9, pqcrypto-kyber 0.8.1, pqcrypto-traits 0.3.5
```
MIT License
@ -10597,7 +10596,7 @@ THE SOFTWARE.
```
## strsim 0.10.0, strsim 0.11.1
## strsim 0.10.0
```
The MIT License (MIT)
@ -10735,7 +10734,7 @@ SOFTWARE.
```
## clap_builder 4.5.11, clap_derive 4.5.11, clap_lex 0.7.2
## clap_builder 4.4.18, clap_derive 4.4.7, clap_lex 0.6.0
```
The MIT License (MIT)
@ -11018,7 +11017,7 @@ SOFTWARE.
```
## version_check 0.9.5
## version_check 0.9.4
```
The MIT License (MIT)
@ -11096,7 +11095,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
## boring-sys 4.6.0, ring 0.17.8
## boring-sys 4.9.0, ring 0.17.8
```
/* ====================================================================

8
package-lock.json generated
View file

@ -22,7 +22,7 @@
"@react-aria/utils": "3.16.0",
"@react-spring/web": "9.5.5",
"@signalapp/better-sqlite3": "8.7.1",
"@signalapp/libsignal-client": "0.54.0",
"@signalapp/libsignal-client": "0.55.0",
"@signalapp/ringrtc": "2.46.0",
"@signalapp/windows-dummy-keystroke": "1.0.0",
"@types/fabric": "4.5.3",
@ -7223,9 +7223,9 @@
}
},
"node_modules/@signalapp/libsignal-client": {
"version": "0.54.0",
"resolved": "https://registry.npmjs.org/@signalapp/libsignal-client/-/libsignal-client-0.54.0.tgz",
"integrity": "sha512-bkdw3r39UU0W0UaFAJJVhLC5AEiMkgCgtdtabI64c1KuT7oeIx4XvegfqtBwF7vKj29ZHz4lODCh3tFyqbrNLw==",
"version": "0.55.0",
"resolved": "https://registry.npmjs.org/@signalapp/libsignal-client/-/libsignal-client-0.55.0.tgz",
"integrity": "sha512-SwsBLUlHsUU6ae6cWsvok2maaRqIoACi0LgW0uGtTrNDeSUdja3j2Dnt/M5InQ+LaU5DQg8xMyUq8KKRp2RmpQ==",
"hasInstallScript": true,
"dependencies": {
"node-gyp-build": "^4.2.3",

View file

@ -104,7 +104,7 @@
"@react-aria/utils": "3.16.0",
"@react-spring/web": "9.5.5",
"@signalapp/better-sqlite3": "8.7.1",
"@signalapp/libsignal-client": "0.54.0",
"@signalapp/libsignal-client": "0.55.0",
"@signalapp/ringrtc": "2.46.0",
"@signalapp/windows-dummy-keystroke": "1.0.0",
"@types/fabric": "4.5.3",

View file

@ -10,6 +10,7 @@ import { getRandomBytes } from '../../Crypto';
import * as Bytes from '../../Bytes';
import { SignalService as Proto, Backups } from '../../protobuf';
import { DataWriter } from '../../sql/Client';
import { APPLICATION_OCTET_STREAM } from '../../types/MIME';
import { generateAci } from '../../types/ServiceId';
import { PaymentEventKind } from '../../types/Payment';
import { ContactFormType } from '../../types/EmbeddedContact';
@ -372,6 +373,11 @@ describe('backup/non-bubble messages', () => {
packId: Bytes.toHex(getRandomBytes(16)),
stickerId: 1,
packKey: Bytes.toBase64(getRandomBytes(32)),
data: {
contentType: APPLICATION_OCTET_STREAM,
error: true,
size: 0,
},
},
reactions: [
{

View file

@ -591,14 +591,12 @@ export class SocketManager extends EventListener {
: TransportOption.Original;
}
private connectLibsignalUnauthenticated(): AbortableProcess<IWebSocketResource> {
return connectUnauthenticatedLibsignal({
libsignalNet: this.libsignalNet,
name: UNAUTHENTICATED_CHANNEL_NAME,
});
}
private async getUnauthenticatedResource(): Promise<IWebSocketResource> {
// awaiting on `this.getProxyAgent()` needs to happen here
// so that there are no calls to `await` between checking
// the value of `this.unauthenticated` and assigning it later in this function
const proxyAgent = await this.getProxyAgent();
if (this.unauthenticated) {
return this.unauthenticated.getResult();
}
@ -613,8 +611,6 @@ export class SocketManager extends EventListener {
log.info('SocketManager: connecting unauthenticated socket');
const proxyAgent = await this.getProxyAgent();
const transportOption = this.transportOption(proxyAgent);
log.info(
`SocketManager: connecting unauthenticated socket, transport option [${transportOption}]`
@ -623,7 +619,10 @@ export class SocketManager extends EventListener {
let process: AbortableProcess<IWebSocketResource>;
if (transportOption === TransportOption.Libsignal) {
process = this.connectLibsignalUnauthenticated();
process = connectUnauthenticatedLibsignal({
libsignalNet: this.libsignalNet,
name: UNAUTHENTICATED_CHANNEL_NAME,
});
} else {
process = this.connectResource({
name: UNAUTHENTICATED_CHANNEL_NAME,

View file

@ -38,7 +38,11 @@ import type { ChatServiceDebugInfo } from '@signalapp/libsignal-client/Native';
import type { Net } from '@signalapp/libsignal-client';
import { Buffer } from 'node:buffer';
import type { ChatServerMessageAck } from '@signalapp/libsignal-client/dist/net';
import type {
ChatServerMessageAck,
ChatServiceListener,
ConnectionEventsListener,
} from '@signalapp/libsignal-client/dist/net';
import type { EventHandler } from './EventTarget';
import EventTarget from './EventTarget';
@ -88,7 +92,6 @@ const AggregatedStatsSchema = z.object({
connectionFailures: z.number(),
requestsCompared: z.number(),
ipVersionMismatches: z.number(),
unexpectedReconnects: z.number(),
healthcheckFailures: z.number(),
healthcheckBadStatus: z.number(),
lastToastTimestamp: z.number(),
@ -133,7 +136,6 @@ export namespace AggregatedStats {
connectionFailures: a.connectionFailures + b.connectionFailures,
healthcheckFailures: a.healthcheckFailures + b.healthcheckFailures,
ipVersionMismatches: a.ipVersionMismatches + b.ipVersionMismatches,
unexpectedReconnects: a.unexpectedReconnects + b.unexpectedReconnects,
healthcheckBadStatus: a.healthcheckBadStatus + b.healthcheckBadStatus,
lastToastTimestamp: Math.max(a.lastToastTimestamp, b.lastToastTimestamp),
};
@ -144,7 +146,6 @@ export namespace AggregatedStats {
requestsCompared: 0,
connectionFailures: 0,
ipVersionMismatches: 0,
unexpectedReconnects: 0,
healthcheckFailures: 0,
healthcheckBadStatus: 0,
lastToastTimestamp: 0,
@ -156,12 +157,11 @@ export namespace AggregatedStats {
if (timeSinceLastToast < durations.DAY || stats.requestsCompared < 1000) {
return false;
}
return (
const totalFailuresSinceLastToast =
stats.healthcheckBadStatus +
stats.healthcheckFailures +
stats.connectionFailures >
20 || stats.unexpectedReconnects > 50
);
stats.healthcheckFailures +
stats.connectionFailures;
return totalFailuresSinceLastToast > 20;
}
export function localStorageKey(name: string): string {
@ -330,6 +330,10 @@ export interface IWebSocketResource extends IResource {
localPort(): number | undefined;
}
type LibsignalWebSocketResourceHolder = {
resource: LibsignalWebSocketResource | undefined;
};
export function connectUnauthenticatedLibsignal({
libsignalNet,
name,
@ -337,7 +341,24 @@ export function connectUnauthenticatedLibsignal({
libsignalNet: Net.Net;
name: string;
}): AbortableProcess<LibsignalWebSocketResource> {
return connectLibsignal(libsignalNet.newUnauthenticatedChatService(), name);
const logId = `LibsignalWebSocketResource(${name})`;
const listener: LibsignalWebSocketResourceHolder & ConnectionEventsListener =
{
resource: undefined,
onConnectionInterrupted(): void {
if (!this.resource) {
logDisconnectedListenerWarn(logId, 'onConnectionInterrupted');
return;
}
this.resource.onConnectionInterrupted();
this.resource = undefined;
},
};
return connectLibsignal(
libsignalNet.newUnauthenticatedChatService(listener),
listener,
logId
);
}
export function connectAuthenticatedLibsignal({
@ -353,12 +374,15 @@ export function connectAuthenticatedLibsignal({
handler: (request: IncomingWebSocketRequest) => void;
receiveStories: boolean;
}): AbortableProcess<LibsignalWebSocketResource> {
const listener = {
const logId = `LibsignalWebSocketResource(${name})`;
const listener: LibsignalWebSocketResourceHolder & ChatServiceListener = {
resource: undefined,
onIncomingMessage(
envelope: Buffer,
timestamp: number,
ack: ChatServerMessageAck
): void {
// Handle incoming messages even if we've disconnected.
const request = new IncomingWebSocketRequestLibsignal(
ServerRequestType.ApiMessage,
envelope,
@ -368,6 +392,10 @@ export function connectAuthenticatedLibsignal({
handler(request);
},
onQueueEmpty(): void {
if (!this.resource) {
logDisconnectedListenerWarn(logId, 'onQueueEmpty');
return;
}
const request = new IncomingWebSocketRequestLibsignal(
ServerRequestType.ApiEmptyQueue,
undefined,
@ -377,7 +405,12 @@ export function connectAuthenticatedLibsignal({
handler(request);
},
onConnectionInterrupted(): void {
log.warn(`LibsignalWebSocketResource(${name}): connection interrupted`);
if (!this.resource) {
logDisconnectedListenerWarn(logId, 'onConnectionInterrupted');
return;
}
this.resource.onConnectionInterrupted();
this.resource = undefined;
},
};
return connectLibsignal(
@ -387,33 +420,40 @@ export function connectAuthenticatedLibsignal({
receiveStories,
listener
),
name
listener,
logId
);
}
function logDisconnectedListenerWarn(logId: string, method: string): void {
log.warn(`${logId} received ${method}, but listener already disconnected`);
}
function connectLibsignal(
chatService: Net.ChatService,
name: string
resourceHolder: LibsignalWebSocketResourceHolder,
logId: string
): AbortableProcess<LibsignalWebSocketResource> {
const connectAsync = async () => {
try {
const debugInfo = await chatService.connect();
log.info(`LibsignalWebSocketResource(${name}) connected`, debugInfo);
return new LibsignalWebSocketResource(
log.info(`${logId} connected`, debugInfo);
const resource = new LibsignalWebSocketResource(
chatService,
IpVersion.fromDebugInfoCode(debugInfo.ipType)
IpVersion.fromDebugInfoCode(debugInfo.ipType),
logId
);
// eslint-disable-next-line no-param-reassign
resourceHolder.resource = resource;
return resource;
} catch (error) {
// Handle any errors that occur during connection
log.error(
`LibsignalWebSocketResource(${name}) connection failed`,
Errors.toLogFormat(error)
);
log.error(`${logId} connection failed`, Errors.toLogFormat(error));
throw error;
}
};
return new AbortableProcess<LibsignalWebSocketResource>(
`LibsignalWebSocketResource.connect(${name})`,
`${logId}.connect`,
{
abort() {
// if interrupted, trying to disconnect
@ -428,9 +468,12 @@ export class LibsignalWebSocketResource
extends EventTarget
implements IWebSocketResource
{
closed = false;
constructor(
private readonly chatService: Net.ChatService,
private readonly socketIpVersion: IpVersion | undefined
private readonly socketIpVersion: IpVersion | undefined,
private readonly logId: string
) {
super();
}
@ -452,12 +495,39 @@ export class LibsignalWebSocketResource
return super.addEventListener(name, handler);
}
public close(_code?: number, _reason?: string): void {
public close(code = 3000, reason?: string): void {
if (this.closed) {
log.info(`${this.logId}.close: Already closed! ${code}/${reason}`);
return;
}
drop(this.chatService.disconnect());
// On linux the socket can wait a long time to emit its close event if we've
// lost the internet connection. On the order of minutes. This speeds that
// process up.
Timers.setTimeout(
() => this.onConnectionInterrupted(),
5 * durations.SECOND
);
}
public shutdown(): void {
drop(this.chatService.disconnect());
this.close(3000, 'Shutdown');
}
onConnectionInterrupted(): void {
if (this.closed) {
log.warn(
`${this.logId}.onConnectionInterrupted called after resource is closed`
);
return;
}
this.closed = true;
log.warn(`${this.logId}: connection closed`);
// TODO: DESKTOP-7519. `reason` should be eventually resolved from the
// disconnect reason error object coming from libsignal.
const reason = undefined;
this.dispatchEvent(new CloseEvent(3000, reason || 'normal'));
}
public forceKeepAlive(): void {
@ -627,12 +697,11 @@ export class WebSocketResourceWithShadowing implements IWebSocketResource {
return;
}
try {
const [healthCheckResult, debugInfo] =
await this.shadowing.sendRequestGetDebugInfo({
verb: 'GET',
path: '/v1/keepalive',
timeout: KEEPALIVE_TIMEOUT_MS,
});
const healthCheckResult = await this.shadowing.sendRequest({
verb: 'GET',
path: '/v1/keepalive',
timeout: KEEPALIVE_TIMEOUT_MS,
});
this.stats.requestsCompared += 1;
if (!isSuccessfulStatusCode(healthCheckResult.status)) {
this.stats.healthcheckBadStatus += 1;
@ -640,7 +709,6 @@ export class WebSocketResourceWithShadowing implements IWebSocketResource {
`${this.logId}: keepalive via libsignal responded with status [${healthCheckResult.status}]`
);
}
this.stats.unexpectedReconnects = debugInfo.reconnectCount;
} catch (error) {
this.stats.healthcheckFailures += 1;
log.warn(

View file

@ -180,7 +180,6 @@ type NetworkStatistics = {
unauthorizedRequestsCompared?: string;
unauthorizedHealthcheckFailures?: string;
unauthorizedHealthcheckBadStatus?: string;
unauthorizedUnexpectedReconnects?: string;
unauthorizedIpVersionMismatches?: string;
};
@ -220,9 +219,6 @@ ipc.on('additional-log-data-request', async event => {
unauthorizedHealthcheckBadStatus: formatCountForLogging(
unauthorizedStats.healthcheckBadStatus
),
unauthorizedUnexpectedReconnects: formatCountForLogging(
unauthorizedStats.unexpectedReconnects
),
unauthorizedIpVersionMismatches: formatCountForLogging(
unauthorizedStats.ipVersionMismatches
),