Signal Server の 2025年2月リリース以降、従来の自己ホスト型 TURN サービス(Coturn)への対応が完全に削除され、Cloudflare TURN のみが利用可能になりました。これはグローバルなネットワーク品質と運用簡易性を重視した合理的な選択ですが、オンプレミス環境や完全なデータ主権を求める組織にとっては制約となります。
本稿では、Signal Server のソースコードを改修し、Coturn をバックエンドとして再統合する方法を解説します。クライアント側の変更は一切不要であり、通信プロトコルレベルでも互換性が保たれます — なぜなら、TURN 認証トークンの生成ロジックのみがサーバー側で置き換えられるためです。
まず、設定モデルに Coturn 構成を追加します:
// service/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnConfiguration.java
public record TurnConfiguration(
CloudflareTurnConfiguration cloudflare,
CoturnConfiguration coturn
) {}
次に、/v2/calling/relays エンドポイントの実装を拡張します。以下は簡易化されたバージョンで、単一ノード構成を想定し、IP ベースのルーティングは省略しています:
// service/src/main/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerV2.java
private final TurnConfiguration turn;
public CallRoutingControllerV2(
final RateLimiters rateLimiters,
final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager,
final TurnConfiguration turn
) {
this.rateLimiters = rateLimiters;
this.cloudflareTurnCredentialsManager = cloudflareTurnCredentialsManager;
this.turn = turn;
}
private TurnToken fetchCoturnRelay() {
final CoturnConfiguration cfg = turn.coturn();
try {
return buildCoturnToken(
Base64.getDecoder().decode(cfg.secret().value()),
cfg.hostname(),
cfg.urlsWithIps(),
cfg.urlsWithHostname()
);
} catch (Exception e) {
throw new RuntimeException("Failed to generate Coturn token", e);
}
}
private TurnToken buildCoturnToken(
byte[] secretKey,
String host,
List<String> ipUrls,
List<String> domainUrls
) throws NoSuchAlgorithmException, InvalidKeyException {
final Mac hmac = Mac.getInstance("HmacSHA1");
final long expiry = Instant.now().plus(Duration.ofDays(1)).getEpochSecond();
final int randomUser = Math.abs(new SecureRandom().nextInt());
final String userString = expiry + ":" + randomUser;
final String protocolFlag = ipUrls != null && !ipUrls.isEmpty() ? "01" : "00";
final String payload = userString + "#" + protocolFlag;
hmac.init(new SecretKeySpec(secretKey, "HmacSHA1"));
final String password = Base64.getEncoder().encodeToString(hmac.doFinal(payload.getBytes()));
return new TurnToken(userString, password, domainUrls, ipUrls, host);
}
@GET
@Path("/relays")
@Produces(MediaType.APPLICATION_JSON)
public GetCallingRelaysResponse getRelays(@Auth AuthenticatedDevice device) {
final List<TurnToken> tokens = List.of(fetchCoturnRelay()); // Cloudflare 呼び出しを無効化
return new GetCallingRelaysResponse(tokens);
}
続いて、config.yml に Coturn の設定ブロックを追加します:
turn:
cloudflare:
# (既存のCloudflare設定は残してもよいが未使用)
coturn:
secret: secret://turn.shared_secret
hostname: turn.internal.example.com
urlsWithIps:
- 192.168.10.5:3478
- [2001:db8::1]:3478
urlsWithHostname:
- turn.internal.example.com:3478
※ urlsWithIps はクライアントが優先的に使用するアドレスリストです。IPv4/IPv6 の両方を指定可能で、NAT 環境下での接続確率向上に寄与します。
設定反映後、Signal Server を再起動します。その後、任意の Signal クライアント(Android/iOS/デスクトップ)から音声・映像通話テストを実行すると、Coturn を経由したピア間接続が正常に確立されます。
Coturn サーバー自体は標準的な設定で動作し、STUN/TURN の両モードを有効にしておく必要があります。認証方式は long-term credential を推奨し、use-auth-secret および static-auth-secret を用いて上記の secret 値と一致させるのが最も安全です。