FiveMでIPv4/IPv6デュアルスタック対応を実現した話

この記事は [ドワンゴ Advent Calendar 2024](https://qiita.com/advent-calendar/2024/dwango) 4日目の記事です。

## はじめに

こんにちは！ @azutake です。

普段はニコニコのサブスクを一画面で管理出来るようにする [定期お支払いの管理](https://subs.nicovideo.jp/my/subscriptions) というものに関わっていたり、[カスタムキャスト](https://customcast.jp/)というアバター作成アプリの開発をしていたりします。

ここ数年では趣味でAS番号を取得して運用してたりする、もはや何をやっているのか分からない変人だったりします。

今回は、[VRCGTA](https://twitter.com/yukad2_/status/1745468102283022812)という、VRChatterだけでGTA5のロールプレイサーバをやろう、という企画に参加した際の知見を書き記しておこうと思います。

## FiveMって？？？？

FiveMとは、簡単に言ってしまえばGTA Onlineのエミュ鯖・MODサーバのようなもので、スクリプトなどを書くことで独自の要素を簡単に実装・マルチプレイが可能なプラットフォームです。それこそストリーマーの方々がやっていたので名前だけは知っている方もいるかもしれません。

GTA Onlineは本来30人が同時接続の限界ですが、FiveMだと特殊な方法によってその上限が引き上げられており、大人数で遊ぶことが出来たりと、かなり単体でも楽しめるものです。是非触ったことが無ければ、一度触ってみてもいいかもしれません。

## 背景：FiveMにおけるIPv6の課題

FiveMは地味にIPv6にも対応してはいるんですが、IPv4フォールバック機能が無いため、実質的にIPv6でサーバを公開するのは非常に困難でした。実際VRCGTAを運用していて思ったのが、まだ半数以上がIPv4 Onlyの環境であったことに少し驚いたほどです。
しかし、現代では、それこそフレッツなどIPv6を標準とする回線が増えてきており、IPv4はMAP-Eなどの変換を経由する必要があるため混雑しやすく、出来ればIPv6で接続可能なユーザはIPv6で繋いであげたい気持ちがあります。

そこで、IPv6が利用可能な環境では優先的にIPv6で接続できるようにする必要性を感じ、この問題に取り組むことにしました。

## FiveMの接続の仕組み

FiveMのサーバー接続プロセスは以下の2段階で構成されています。

1. HTTP通信による認証フェーズ
   - 参加キューイング
   - ゲームサーバーの管理外の処理（認証など）を実施
   - 認証成功後、実際のゲームサーバーのアドレス情報がクライアントに送信される

2. ゲームサーバーへの直接接続フェーズ(TCP/UDP)

そして、実際の挙動を観察していると、**HTTPリクエスト時にはIPv4フォールバックが行われていることに気が付きました**。コードは確認していないですが、おそらく何らかのライブラリやWinAPIなどを用いているのでしょう。

## 解決策：プロキシサーバーの実装

そしたら話は早くて、HTTPサーバ上でクライアントのIPをチェックして返却するアドレスを切り替えるだけです。

今回はついでにDDoS対策やSSL証明書を自動で持ってくれる利便性から、Cloudflare ZeroTrustを用いることにしました。

### 実装の詳細

プロキシサーバはHTTPさえきちんと扱えて、途中のストリームに介入出来れば何でも良いのですが、今回は仕事的にnodejsをよく使っていたのでnodejsで組みました。

```javascript
import express from "express";
import httpProxy from "http-proxy";
import bodyParser from "body-parser";
import streamify from "stream-array";
import { isIPv4, isIPv6 } from "is-ip"

app.use(bodyParser.urlencoded({verify: rawBodySaver, extended: true}));
app.post("/client", (req, res, next) => {
  const clientIp = req.headers["cf-connecting-ip"]

  if(clientIp && req.body.method === "getEndpoints") {
    if(isIPv4(clientIp)) res.status(200).send(JSON.stringify(["ipv4アドレス"]))
    if(isIPv6(clientIp)) res.status(200).send(JSON.stringify(["ipv6アドレス"]))
    return
  }

  proxy.web(req, res, {
    target: `http://<ゲームサーバ>/client`,
    buffer: streamify([req.rawBody]),
  }, next)
})

app.listen(PORT, HOST, () => {
  console.log("Starting FiveM Client Proxy")
})
```

FiveMのHTTP通信では、WebSocketなど使わずにそのままHTTP接続単体でストリーミングを行う実装になっています。この通信をプロキシしながら、 `getEndpoints` を受け取ったら実際のIPアドレスを返却するようになっています。簡単ですね。

クライアントのIPアドレスは、Cloudflare ZeroTrustの場合、 `cf-connecting-ip` で受け取れるので、それを判定としてそのまま利用しています。

## まとめ

VRCGTAでは、ゲーム内では同時に様々なことが発生していましたがこういった取り組みによって**ボイスチャットの安定性やゲーム内の安定性などを実現**することが出来ました。

インターネットはISP間の相性なども存在しますが、このコードを応用すると、例えば特定のISPの場合だけ返答するIPアドレスを変えて最適な経路を用意する、といったことも可能になります。まるでASのルーティングみたいですね。

今回記事内で紹介しているコードは一部分ですが、実際にVRCGTAで稼働させていたコードを[GitHub](https://github.com/azutake/fivem-client-proxy/tree/main)上に置いてあるので、是非参考にしてみてください！

## おわりに

今回の記事は完全に趣味の話でしたが、普段は課金システムの部署に所属しており、一緒にニコニコの課金やサブスクなどのシステムを手伝ってくれる仲間を募集しております。

ぜひお気軽にご応募ください！

* [新卒採用はこちら](https://dwango.snar.jp/index.aspx)
* [キャリア採用はこちら](https://hrmos.co/pages/dwango)