修正 Signal 私服音视频通话
Signal-Server 新版(2025/02/28)完全移除了对单机部署的 TurnServer 的支持,全面转为了使用 Cloudflare 提供的 Turn 云服务,如果你还不了解什么是 TURN,可 点此 看看 cloudflare 的科普,简言之,他可以让两个处于内网的设备直接进行数据通讯,而「尽量」无须途径第三方。
相比自部署的单节点TURN服务器,cloudflare 的TURN 节点则分布在其全球 Anycast 网络中,在大多数场景下都能有效优化延迟和可用性,而且能大幅度降低系统的运维成本,对于 Signal 来说的确是更划算的。但对于我们希望私有化部署 Signal 来说则过于复杂了,为防止 「卡脖子」(并没有) 考虑修改 Signal 服务端代码,重新加回自部署 Coturn 支持。
事实上 Coturn 与 cloudflare 的实现差异仅在服务端,客户端无须任何变更,客户端甚至并不清楚当前服务的是什么,从接口 GET /v2/calling/relays 的响应结构体 GetCallingRelaysResponse 就可以看出来。
这里将主要的变更列出来,首先是配置文件,需要加入 coturn 的部分配置:
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnConfiguration.java
index 6e69794d6..46107c877 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnConfiguration.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnConfiguration.java
@@ -5,5 +5,8 @@
package org.whispersystems.textsecuregcm.configuration;
-public record TurnConfiguration(CloudflareTurnConfiguration cloudflare) {
+public record TurnConfiguration(
+ CloudflareTurnConfiguration cloudflare,
+ CoturnConfiguration coturn
+ ) {
}
然后是 WEB 接口代码处,这里做了简化处理,未根据客户端IP来执行路由(单节点也没啥好路由的):
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerV2.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerV2.java
index 57e92728e..a5cae012a 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerV2.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerV2.java
@@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.controllers;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
+import com.fasterxml.jackson.core.JsonProcessingException;
import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Metrics;
@@ -19,31 +20,92 @@ import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.time.Duration;
+import java.time.Instant;
import java.util.ArrayList;
+import java.util.Base64;
import java.util.List;
import java.util.UUID;
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
import org.whispersystems.textsecuregcm.auth.CloudflareTurnCredentialsManager;
import org.whispersystems.textsecuregcm.auth.TurnToken;
+import org.whispersystems.textsecuregcm.configuration.CoturnConfiguration;
+import org.whispersystems.textsecuregcm.configuration.TurnConfiguration;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
+import org.whispersystems.textsecuregcm.util.SystemMapper;
+import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.websocket.auth.ReadOnly;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
@io.swagger.v3.oas.annotations.tags.Tag(name = "Calling")
@Path("/v2/calling")
public class CallRoutingControllerV2 {
private static final Counter CLOUDFLARE_TURN_ERROR_COUNTER = Metrics.counter(name(CallRoutingControllerV2.class, "cloudflareTurnError"));
private final RateLimiters rateLimiters;
private final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager;
+ private final TurnConfiguration turn;
+
+ private static final String ALGORITHM = "HmacSHA1";
+ private static final String WithUrlsProtocol = "00";
+ private static final String WithIpsProtocol = "01";
public CallRoutingControllerV2(
final RateLimiters rateLimiters,
- final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager
+ final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager,
+ final TurnConfiguration turn
) {
this.rateLimiters = rateLimiters;
+ this.turn = turn;
this.cloudflareTurnCredentialsManager = cloudflareTurnCredentialsManager;
}
+ private TurnToken getCallingRelayOld() {
+ final CoturnConfiguration coturn = turn.coturn();
+ final TurnToken rs;
+
+ try {
+ rs = generateToken(
+ Base64.getDecoder().decode(coturn.secret().value()),
+ coturn.hostname(),
+ coturn.urlsWithIps(),
+ coturn.urlsWithHostname()
+ );
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ } catch (InvalidKeyException e) {
+ throw new RuntimeException(e);
+ }
+
+ return rs;
+ }
+
+ private TurnToken generateToken(byte[] turnSecret, String hostname, List<String> urlsWithIps, List<String> urlsWithHostname)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ final Mac mac = Mac.getInstance(ALGORITHM);
+ final long validUntilSeconds = Instant.now().plus(Duration.ofDays(1)).getEpochSecond();
+ final long user = Util.ensureNonNegativeInt(new SecureRandom().nextInt());
+ final String userTime = validUntilSeconds + ":" + user;
+ final String protocol = urlsWithIps != null && !urlsWithIps.isEmpty()
+ ? WithIpsProtocol
+ : WithUrlsProtocol;
+ final String protocolUserTime = userTime + "#" + protocol;
+
+ mac.init(new SecretKeySpec(turnSecret, ALGORITHM));
+ final String password = Base64.getEncoder().encodeToString(mac.doFinal(protocolUserTime.getBytes()));
+
+ return new TurnToken(protocolUserTime, password, urlsWithHostname, urlsWithIps, hostname);
+ }
+
@GET
@Path("/relays")
@Produces(MediaType.APPLICATION_JSON)
@@ -66,7 +128,8 @@ public class CallRoutingControllerV2 {
List<TurnToken> tokens = new ArrayList<>();
try {
- tokens.add(cloudflareTurnCredentialsManager.retrieveFromCloudflare());
+// tokens.add(cloudflareTurnCredentialsManager.retrieveFromCloudflare());
+ tokens.add(getCallingRelayOld());
} catch (Exception e) {
CallRoutingControllerV2.CLOUDFLARE_TURN_ERROR_COUNTER.increment();
throw e;
再调整 Signal-Server 的服务器配置,在 config/config.yml 的 turn 区段下,增加 coturn 子对象,类似如下:
coturn:
secret: secret://turn.secret
hostname: turn.signal.mysignal.cn
urlsWithIps:
- 111.123.145.167:3478
urlsWithHostname:
- turn.signal.mysignal.cn:3478
将你的 coturn IP地址与域名都写上,默认情况下,客户端会优先使用 IP 地址的版本。配置完成后,重启服务端~
赶紧开模拟器试试:

完美~

浙公网安备 33010602011771号