Skip to content

Commit be8b44d

Browse files
committed
Add noise tunnel connection metrics
1 parent 7ca3604 commit be8b44d

File tree

11 files changed

+96
-12
lines changed

11 files changed

+96
-12
lines changed

service/src/main/java/org/whispersystems/textsecuregcm/grpc/net/EstablishLocalGrpcConnectionHandler.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package org.whispersystems.textsecuregcm.grpc.net;
22

3+
import io.micrometer.core.instrument.Metrics;
4+
import io.micrometer.core.instrument.Tag;
5+
import io.micrometer.core.instrument.Tags;
36
import io.netty.bootstrap.Bootstrap;
47
import io.netty.channel.ChannelFutureListener;
58
import io.netty.channel.ChannelHandlerContext;
@@ -15,31 +18,37 @@
1518
import org.slf4j.Logger;
1619
import org.slf4j.LoggerFactory;
1720
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
21+
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
22+
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
1823

1924
/**
2025
* An "establish local connection" handler waits for a Noise handshake to complete upstream in the pipeline, buffering
2126
* any inbound messages until the connection is fully-established, and then opens a proxy connection to a local gRPC
2227
* server.
2328
*/
2429
public class EstablishLocalGrpcConnectionHandler extends ChannelInboundHandlerAdapter {
30+
private static final Logger log = LoggerFactory.getLogger(EstablishLocalGrpcConnectionHandler.class);
2531

2632
private final GrpcClientConnectionManager grpcClientConnectionManager;
2733

2834
private final LocalAddress authenticatedGrpcServerAddress;
2935
private final LocalAddress anonymousGrpcServerAddress;
36+
private final FramingType framingType;
3037

3138
private final List<Object> pendingReads = new ArrayList<>();
3239

33-
private static final Logger log = LoggerFactory.getLogger(EstablishLocalGrpcConnectionHandler.class);
40+
private static final String CONNECTION_ESTABLISHED_COUNTER_NAME = MetricsUtil.name(EstablishLocalGrpcConnectionHandler.class, "established");
3441

3542
public EstablishLocalGrpcConnectionHandler(final GrpcClientConnectionManager grpcClientConnectionManager,
3643
final LocalAddress authenticatedGrpcServerAddress,
37-
final LocalAddress anonymousGrpcServerAddress) {
44+
final LocalAddress anonymousGrpcServerAddress,
45+
final FramingType framingType) {
3846

3947
this.grpcClientConnectionManager = grpcClientConnectionManager;
4048

4149
this.authenticatedGrpcServerAddress = authenticatedGrpcServerAddress;
4250
this.anonymousGrpcServerAddress = anonymousGrpcServerAddress;
51+
this.framingType = framingType;
4352
}
4453

4554
@Override
@@ -63,6 +72,12 @@ public void userEventTriggered(final ChannelHandlerContext remoteChannelContext,
6372
GrpcClientConnectionManager.handleHandshakeInitiated(
6473
remoteChannelContext.channel(), remoteAddress, userAgent, acceptLanguage);
6574

75+
final List<Tag> tags = UserAgentTagUtil.getLibsignalAndPlatformTags(userAgent);
76+
Metrics.counter(CONNECTION_ESTABLISHED_COUNTER_NAME, Tags.of(tags)
77+
.and("authenticated", Boolean.toString(authenticatedDevice.isPresent()))
78+
.and("framingType", framingType.name()))
79+
.increment();
80+
6681
new Bootstrap()
6782
.remoteAddress(grpcServerAddress)
6883
.channel(LocalChannel.class)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package org.whispersystems.textsecuregcm.grpc.net;
2+
3+
public enum FramingType {
4+
NOISE_DIRECT,
5+
WEBSOCKET
6+
}

service/src/main/java/org/whispersystems/textsecuregcm/grpc/net/NoiseIdentityDeterminedEvent.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ public record NoiseIdentityDeterminedEvent(
1818
Optional<AuthenticatedDevice> authenticatedDevice,
1919
InetAddress remoteAddress,
2020
String userAgent,
21-
String acceptLanguage) {}
21+
String acceptLanguage) {
22+
}

service/src/main/java/org/whispersystems/textsecuregcm/grpc/net/noisedirect/NoiseDirectInboundCloseHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
* Watches for inbound close frames and closes the connection in response
1717
*/
1818
public class NoiseDirectInboundCloseHandler extends ChannelInboundHandlerAdapter {
19-
private static String CLIENT_CLOSE_COUNTER_NAME = MetricsUtil.name(ChannelInboundHandlerAdapter.class, "clientClose");
19+
private static String CLIENT_CLOSE_COUNTER_NAME = MetricsUtil.name(NoiseDirectInboundCloseHandler.class, "clientClose");
2020
@Override
2121
public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
2222
if (msg instanceof NoiseDirectFrame ndf && ndf.frameType() == NoiseDirectFrame.FrameType.CLOSE) {

service/src/main/java/org/whispersystems/textsecuregcm/grpc/net/noisedirect/NoiseDirectOutboundErrorHandler.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
package org.whispersystems.textsecuregcm.grpc.net.noisedirect;
22

3+
import io.micrometer.core.instrument.Metrics;
34
import io.netty.buffer.ByteBuf;
45
import io.netty.buffer.ByteBufOutputStream;
56
import io.netty.channel.ChannelFutureListener;
67
import io.netty.channel.ChannelHandlerContext;
78
import io.netty.channel.ChannelOutboundHandlerAdapter;
89
import io.netty.channel.ChannelPromise;
910
import org.whispersystems.textsecuregcm.grpc.net.OutboundCloseErrorMessage;
11+
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
1012

1113
/**
1214
* Translates {@link OutboundCloseErrorMessage}s into {@link NoiseDirectFrame} error frames. After error frames are
1315
* written, the channel is closed
1416
*/
1517
class NoiseDirectOutboundErrorHandler extends ChannelOutboundHandlerAdapter {
18+
private static String SERVER_CLOSE_COUNTER_NAME = MetricsUtil.name(NoiseDirectInboundCloseHandler.class, "serverClose");
1619

1720
@Override
1821
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
@@ -23,6 +26,8 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
2326
case NOISE_HANDSHAKE_ERROR -> NoiseDirectProtos.CloseReason.Code.HANDSHAKE_ERROR;
2427
case INTERNAL_SERVER_ERROR -> NoiseDirectProtos.CloseReason.Code.INTERNAL_ERROR;
2528
};
29+
Metrics.counter(SERVER_CLOSE_COUNTER_NAME, "reason", code.name()).increment();
30+
2631
final NoiseDirectProtos.CloseReason proto = NoiseDirectProtos.CloseReason.newBuilder()
2732
.setCode(code)
2833
.setMessage(err.message())

service/src/main/java/org/whispersystems/textsecuregcm/grpc/net/noisedirect/NoiseDirectTunnelServer.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.slf4j.LoggerFactory;
1818
import org.whispersystems.textsecuregcm.grpc.net.ErrorHandler;
1919
import org.whispersystems.textsecuregcm.grpc.net.EstablishLocalGrpcConnectionHandler;
20+
import org.whispersystems.textsecuregcm.grpc.net.FramingType;
2021
import org.whispersystems.textsecuregcm.grpc.net.GrpcClientConnectionManager;
2122
import org.whispersystems.textsecuregcm.grpc.net.HAProxyMessageHandler;
2223
import org.whispersystems.textsecuregcm.grpc.net.NoiseHandshakeHandler;
@@ -68,7 +69,9 @@ protected void initChannel(SocketChannel socketChannel) {
6869
// This handler will open a local connection to the appropriate gRPC server and install a ProxyHandler
6970
// once the Noise handshake has completed
7071
.addLast(new EstablishLocalGrpcConnectionHandler(
71-
grpcClientConnectionManager, authenticatedGrpcServerAddress, anonymousGrpcServerAddress))
72+
grpcClientConnectionManager,
73+
authenticatedGrpcServerAddress, anonymousGrpcServerAddress,
74+
FramingType.NOISE_DIRECT))
7275
.addLast(new ErrorHandler());
7376
}
7477
});

service/src/main/java/org/whispersystems/textsecuregcm/grpc/net/websocket/NoiseWebSocketTunnelServer.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ protected void initChannel(SocketChannel socketChannel) {
104104
// request and passed it down the pipeline
105105
.addLast(new WebSocketOpeningHandshakeHandler(AUTHENTICATED_SERVICE_PATH, ANONYMOUS_SERVICE_PATH, HEALTH_CHECK_PATH))
106106
.addLast(new WebSocketServerProtocolHandler("/", true))
107+
// Metrics on inbound/outbound Close frames
108+
.addLast(new WebSocketCloseMetricHandler())
107109
// Turn generic OutboundCloseErrorMessages into websocket close frames
108110
.addLast(new WebSocketOutboundErrorHandler())
109111
.addLast(new RejectUnsupportedMessagesHandler())
@@ -116,7 +118,10 @@ protected void initChannel(SocketChannel socketChannel) {
116118
.addLast(new NoiseHandshakeHandler(clientPublicKeysManager, ecKeyPair))
117119
// This handler will open a local connection to the appropriate gRPC server and install a ProxyHandler
118120
// once the Noise handshake has completed
119-
.addLast(new EstablishLocalGrpcConnectionHandler(grpcClientConnectionManager, authenticatedGrpcServerAddress, anonymousGrpcServerAddress))
121+
.addLast(new EstablishLocalGrpcConnectionHandler(
122+
grpcClientConnectionManager,
123+
authenticatedGrpcServerAddress, anonymousGrpcServerAddress,
124+
FramingType.WEBSOCKET))
120125
.addLast(new ErrorHandler());
121126
}
122127
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2025 Signal Messenger, LLC
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
package org.whispersystems.textsecuregcm.grpc.net.websocket;
6+
7+
import io.micrometer.core.instrument.Metrics;
8+
import io.netty.channel.ChannelDuplexHandler;
9+
import io.netty.channel.ChannelHandlerContext;
10+
import io.netty.channel.ChannelPromise;
11+
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
12+
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
13+
14+
public class WebSocketCloseMetricHandler extends ChannelDuplexHandler {
15+
16+
private static String CLIENT_CLOSE_COUNTER_NAME = MetricsUtil.name(WebSocketCloseMetricHandler.class, "clientClose");
17+
private static String SERVER_CLOSE_COUNTER_NAME = MetricsUtil.name(WebSocketCloseMetricHandler.class, "serverClose");
18+
19+
20+
@Override
21+
public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
22+
if (msg instanceof CloseWebSocketFrame closeFrame) {
23+
Metrics.counter(CLIENT_CLOSE_COUNTER_NAME, "closeCode", validatedCloseCode(closeFrame.statusCode())).increment();
24+
}
25+
ctx.fireChannelRead(msg);
26+
}
27+
28+
29+
@Override
30+
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
31+
if (msg instanceof CloseWebSocketFrame closeFrame) {
32+
Metrics.counter(SERVER_CLOSE_COUNTER_NAME, "closeCode", Integer.toString(closeFrame.statusCode())).increment();
33+
}
34+
ctx.write(msg, promise);
35+
}
36+
37+
private static String validatedCloseCode(int closeCode) {
38+
39+
if (closeCode >= 1000 && closeCode <= 1015) {
40+
// RFC-6455 pre-defined status codes
41+
return Integer.toString(closeCode);
42+
} else if (closeCode >= 4000 && closeCode <= 4100) {
43+
// Application status codes
44+
return Integer.toString(closeCode);
45+
} else {
46+
return "unknown";
47+
}
48+
}
49+
50+
}

service/src/main/java/org/whispersystems/textsecuregcm/grpc/net/websocket/WebSocketOutboundErrorHandler.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
import org.slf4j.Logger;
1111
import org.slf4j.LoggerFactory;
1212
import org.whispersystems.textsecuregcm.grpc.net.OutboundCloseErrorMessage;
13+
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
1314

1415
/**
1516
* Converts {@link OutboundCloseErrorMessage}s written to the pipeline into WebSocket close frames
1617
*/
1718
class WebSocketOutboundErrorHandler extends ChannelDuplexHandler {
19+
private static String SERVER_CLOSE_COUNTER_NAME = MetricsUtil.name(WebSocketOutboundErrorHandler.class, "serverClose");
1820

1921
private boolean websocketHandshakeComplete = false;
2022

service/src/test/java/org/whispersystems/textsecuregcm/grpc/net/client/NoiseTunnelClient.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import javax.net.ssl.SSLException;
4747
import org.signal.libsignal.protocol.ecc.ECKeyPair;
4848
import org.signal.libsignal.protocol.ecc.ECPublicKey;
49+
import org.whispersystems.textsecuregcm.grpc.net.FramingType;
4950
import org.whispersystems.textsecuregcm.grpc.net.NoiseTunnelProtos;
5051
import org.whispersystems.textsecuregcm.grpc.net.noisedirect.NoiseDirectFrame;
5152
import org.whispersystems.textsecuregcm.grpc.net.noisedirect.NoiseDirectFrameCodec;
@@ -64,11 +65,6 @@ public class NoiseTunnelClient implements AutoCloseable {
6465
public static final URI AUTHENTICATED_WEBSOCKET_URI = URI.create("wss://localhost/authenticated");
6566
public static final URI ANONYMOUS_WEBSOCKET_URI = URI.create("wss://localhost/anonymous");
6667

67-
public enum FramingType {
68-
WEBSOCKET,
69-
NOISE_DIRECT
70-
}
71-
7268
public static class Builder {
7369

7470
final SocketAddress remoteServerAddress;

0 commit comments

Comments
 (0)