これまでマイクラは、mc.ch1w4.com が自宅のグローバルIPを向いていて、IX2215でポートを開けて受けていた。DNSは誰でも引けるので、自宅のIPが丸見えだった。
Webのほうは Cloudflare Tunnel で隠せているけど、マイクラ(25565/TCP・19132/UDP)はTunnelに乗らない。そこで、外に中継サーバーを1台置いて、プレイヤーにはそちらのIPだけを見せることにした。
中継サーバー選び
条件はこの4つ。
- rootで入れるLinux(自分でトンネルや転送を設定したい)
- 日本にある(遅延を増やしたくない)
- TCPとUDPの両方を、好きなポートで受けられる
- できればお金をかけない
最初は「買い切り」のホスティングも見たけど、中身はWebサイト置き場の共有ホスティングで、rootもポート開放もできない。しかもサーバーは海外だけだった。
結局、Oracle Cloud の Always Free にした。大阪リージョンを選べて、ずっと無料。VMは「インスタンスの作成」画面からではなく、Cloud Shell で oci コマンドを使い、ネットワーク(VCN・インターネットゲートウェイ・セキュリティリスト)ごと作った。
つなぎ方:Tailscale
中継サーバー(relay01)と自宅は Tailscale でつなぐ。自宅には VLAN30(DMZ)を流しているサブネットルーター(vpn01)がすでにあるので、mc01 に追加で何か入れる必要はなかった。relay01 で --accept-routes を付けるだけで、mc01 の自宅IPに届く。
プレイヤー → relay01(Oracle 大阪)
├ 25565/TCP → HAProxy ─┐
└ 19132/UDP → iptables ┤ Tailscale
└→ vpn01 → mc01(Velocity)
ただし、relay01 はインターネットに直接さらされるサーバーなので、乗っ取られたときにラボ全体に入られないようにするのが大事。Tailscale のACLはそれまで「全部許可」だったので、relay01 にタグを付けて、mc01 のマイクラの2ポートだけに絞った。
"tagOwners": { "tag:relay": ["autogroup:admin"] },
"grants": [
{"src": ["autogroup:member"], "dst": ["*"], "ip": ["*"]},
{"src": ["tag:relay"], "dst": ["192.168.30.20"], "ip": ["tcp:25565", "udp:19132"]},
],
"tests": [
{"src": "tag:relay", "accept": ["192.168.30.20:25565"],
"deny": ["192.168.30.20:22", "192.168.20.40:5432"]},
],
tests を書いておくと、保存のたびに「relay01 から SSH や DB には入れない」ことを Tailscale が確かめてくれる。タグを付けた機器は、鍵の期限切れも自動でなくなる。
一度、タグなしの tailscale up を打ってしまい、表示された承認URLを開く寸前で気づいた。あのまま承認していたら、relay01 が「自分の端末」扱いになって、全部に入れていた。
Oracle のファイアウォールは2重
Oracle は、クラウド側のセキュリティリストに加えて、Ubuntu のイメージ自体に厳しい iptables のルールが最初から入っている。セキュリティリストで開けても、VMの中で REJECT されて届かない。必要なルールは -I で既存の REJECT より前に入れて、netfilter-persistent save で保存する。
統合版(UDP)は、iptables の DNAT と MASQUERADE で mc01 に流している。
本当のIPを渡す:PROXY protocol
ただ転送するだけだと、mc01 から見るとプレイヤーが全員 vpn01 のIPになる。困るのは2つ。
- Velocity のログイン制限(同じIPから続けて入るのを止める機能)で、ほかの人がはじかれる
- BANやログのIPが、全員同じになる
そこで、Java版は relay01 の HAProxy で受けて、send-proxy-v2 で本当のIPを Velocity に渡すことにした。
frontend mc_java
bind :25565
default_backend velocity
backend velocity
server mc01 192.168.30.20:25565 send-proxy-v2
Velocity 側は velocity.toml の haproxy-protocol = true。この設定は、有効にした瞬間から、PROXY protocol のヘッダーを付けてこない接続をすべて切る。なので、切り替えは次の順番で一気にやった。
- relay01 を HAProxy に切り替える(この時点では誰も relay01 を使っていないので影響なし)
- Velocity の設定を書き換えて再起動
- Cloudflare で
mc.ch1w4.comを CNAME(自宅)から A(relay01)に変える
切り替えたあと、Velocity のログに Java 版の人の本当のIPが出るのを確認できた。
ハマったこと:統合版だけ入れない
Java版は入れるのに、統合版だけ、入った瞬間に切られるようになった。
最初に疑ったのは Geyser。ちょうど同じ再起動で、Geyser が latest 指定のせいで b1258 → b1262 に自動で上がっていた。b1258 に戻して固定したけど、直らなかった。版の固定自体は安全のためにそのままにしている。
本当の原因は、さっきの haproxy-protocol = true だった。統合版の人は、UDPで Geyser に着いたあと、Geyser が Velocity につなぎ直す。このときに PROXY protocol のヘッダーを付けていないので、Velocity に切られていた。Java版は HAProxy がヘッダーを付けるので入れる、という差だった。
Geyser の設定(Geyser-Velocity/config.yml)で、Java 側(java: の中)の use-haproxy-protocol を true にする。bedrock: の中にも同じ名前の項目があるので、そちらは false のまま。
反省:PROXY protocol を入れるときは、「Velocity につないでくるもの」を全部洗い出しておくべきだった。HAProxy だけでなく、Geyser のように中から接続してくるものもある。
最後の片付け
- DDNS をやめる:
home.ch1w4.comの A レコードが残っていると、そこから自宅のIPが分かってしまう。mc01 の DDNS コンテナを止めて、レコードと API トークンを消す。 - IX のポート開放を閉じる:入口は relay01 だけになったので、25565・19132 の静的NATはもう要らない。
- relay01 のIPを予約済みIPにする:今のIPは VM を作り直すと変わる。無料のVMは使っていないと回収されることがあるので、予約済みのパブリックIPに付け替えておく。
これで、プレイヤーから見えるのは Oracle のIPだけになり、自宅のIPはどこにも出なくなる。