Relay(プロキシ)活用例
Relay は「サーバー + クライアント」を組み合わせた中間者(MTM)です。クライアントと宛先サーバーの間のパケットを観測・改変・遮断でき、さらにクライアントへ直接パケットを送ることもできます。
基本形: 中継するだけの Relay
リポジトリの examples/relay.js 相当です。クライアントは Relay に接続し、Relay がバックエンドサーバーへ転送します。
const { Relay } = require('bedrock-protocol')
const relay = new Relay({
version: '1.21.50',
host: '0.0.0.0', // クライアントの受け口
port: 19132,
destination: { // 転送先(先に 19131 番でサーバーを起動しておく)
host: '127.0.0.1',
port: 19131
}
})
relay.listen()
relay.on('connect', player => {
console.log('接続:', player.connection.address)
})キックメッセージの書き換え
宛先サーバーからの disconnect(キック)を傍受して、メッセージを差し替えます。
player.on('clientbound', ({ name, params }) => {
if (name === 'disconnect') {
params.message = 'Intercepted' // どんな理由でも「Intercepted」に
}
})チャットへの加工(発言に時刻を付加)
プレイヤー → サーバー方向のチャットに情報を追記します。
player.on('serverbound', ({ name, params }) => {
if (name === 'text' && params.type === 'chat') {
params.message += `, on ${new Date().toLocaleString()}`
}
})特定コマンドの遮断
危険なコマンドや不要な操作をサーバーに届く前に握りつぶします。
player.on('serverbound', ({ name, params }, des) => {
if (name === 'command_request') {
const cmd = params.command
if (cmd.startsWith('/op') || cmd === '/stop') {
console.log('遮断:', params.command)
des.canceled = true // サーバーへ転送しない
// 遮断した旨をクライアントに通知
player.queue('text', {
type: 'system', needs_translation: false,
message: 'このコマンドはこのプロキシで禁止されています',
xuid: '', platform_chat_id: '', filtered_message: ''
})
}
}
})クライアントへの通知(Toast / チャット / 効果音)
Relay はクライアントにとってサーバーそのものなので、player.queue() で任意の clientbound パケットを直接送れます。上流サーバーには送られません。
player.on('join', () => {
// toast
player.queue('toast_request', { title: 'ようこそ', message: 'プロキシを経由しています' })
// チャット欄
player.queue('text', {
type: 'system', needs_translation: false,
message: 'このサーバーはプロキシを経由しています',
xuid: '', platform_chat_id: '', filtered_message: ''
})
// 効果音
player.queue('play_sound', {
name: 'note.pling', coordinates: { x: 0, y: 0, z: 0 }, volume: 1.0, pitch: 1.0
})
})フォーム(メニュー)の表示と応答処理
フォームの応答はクライアント → Relay 方向(modal_form_response)で届くため、serverbound で捕捉して des.canceled = true にするのがポイントです。
player.on('join', () => {
player.queue('modal_form_request', {
form_id: 100,
data: JSON.stringify({
type: 'custom_form',
title: 'プロキシ設定',
content: [
{ type: 'toggle', text: 'チャットに時間を付加する', default: true },
{ type: 'dropdown', text: '言語', options: ['日本語', 'English'], default: 0 },
{ type: 'input', text: 'あだ名', placeholder: '任意' }
]
})
})
})
player.on('serverbound', ({ name, params }, des) => {
if (name === 'modal_form_response' && params.form_id === 100) {
des.canceled = true // 上流サーバーへ転送しない(必須)
if (params.has_response_data) {
const [appendTime, lang, nickname] = JSON.parse(params.data)
console.log('設定変更:', { appendTime, lang, nickname })
}
}
})全プレイヤーへのブロードキャスト
relay.clients から接続中のプレイヤー全員に送信します(通常サーバーと同じ構造)。
function broadcast(relay, message) {
for (const addr in relay.clients) {
relay.clients[addr].queue('text', {
type: 'system', needs_translation: false, message,
xuid: '', platform_chat_id: '', filtered_message: ''
})
}
}
// 例: チャットを出すたびにカウントして告知
let chatCount = 0
player.on('serverbound', ({ name, params }) => {
if (name === 'text' && params.type === 'chat') {
chatCount++
broadcast(relay, `チャット総数: ${chatCount} 件目`)
}
})パケット統計(トラフィックの観測)
clientbound / serverbound をカウントして、接続の様子を可視化できます。
const stats = { clientbound: {}, serverbound: {} }
relay.on('connect', player => {
player.on('clientbound', ({ name }) => {
stats.clientbound[name] = (stats.clientbound[name] || 0) + 1
})
player.on('serverbound', ({ name }) => {
stats.serverbound[name] = (stats.serverbound[name] || 0) + 1
})
})
setInterval(() => {
const top = Object.entries(stats.clientbound).sort((a, b) => b[1] - a[1]).slice(0, 5)
console.log('上位クライアント向けパケット:', top.map(([n, c]) => `${n}(${c})`).join(' '))
}, 30_000)上流への独自パケット送信
クライアントに代わって上流サーバーへパケットを送るには player.upstream.queue() を使います。
// クライアントが参加したら、上流サーバーへ独自コマンドを実行
player.on('join', () => {
player.upstream.queue('command_request', {
command: '/give @a diamond 1',
origin: { type: 'server' }, // フォーマットはサーバー仕様に合わせる
internal: false,
version: 'latest'
})
})パケット形式の整合
上流へ送るパケットは上流サーバーのバージョンに合わせたフィールド構成が必要です。command_request はバージョン依存のフィールドがあるため、詳細はクライアントガイドの注意点を参照してください。
参加時の挨拶ダイアログ(モーダル)+ 応答
メニュー表示の応答処理例として、modal タイプで 2 択を返してもらうパターンです。
player.on('join', () => {
player.queue('modal_form_request', {
form_id: 1,
data: JSON.stringify({
type: 'modal',
title: '確認',
content: 'プロキシ利用を許可しますか?',
button1: 'はい',
button2: 'いいえ'
})
})
})
player.on('serverbound', ({ name, params }, des) => {
if (name === 'modal_form_response' && params.form_id === 1) {
des.canceled = true
const ok = params.has_response_data && params.data === 'true'
player.queue('toast_request', {
title: ok ? '許可されました' : '拒否されました',
message: ok ? 'それでは楽しんでください' : '入室を拒否されました'
})
if (!ok) setTimeout(() => player.disconnect('プロキシ利用を拒否しました'), 200)
}
})Realm を経由する Relay
destination.realms を指定すると、宛先を Realm にできます(実サーバーへの中継と同じ挙動)。
const relay = new Relay({
host: '0.0.0.0',
port: 19132,
destination: {
realms: { pickRealm: realms => realms[0] }
}
})
relay.listen()実装例はリポジトリの examples/realmRelay.js を参照してください。
上級: relayPlayer による中継ロジックの差し替え
relayPlayer オプションで独自の RelayPlayer サブクラスを注入すると、中継挙動を丸ごと制御できます。
const { Relay, Player } = require('bedrock-protocol')
class MyPlayer extends Player {
// ここで RelayPlayer 相当の中継処理を実装する(高度)
}
const relay = new Relay({ ..., relayPlayer: MyPlayer })詳細はRelay ガイドの拡張ポイントを参照してください。