Signal サーバーのプライベート TURN サポートを Coturn で復活させる

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 値と一致させるのが最も安全です。

タグ: signal coturn WebRTC turn stun

6月9日 18:43 投稿