Skip to content

Relay(プロキシ)活用例 ​

Relay は「サーバー + クライアント」を組み合わせた中間者(MTM)です。クライアントと宛先サーバーの間のパケットを観測・改変・遮断でき、さらにクライアントへ直接パケットを送ることもできます。

基本形: 中継するだけの Relay ​

リポジトリの examples/relay.js 相当です。クライアントは Relay に接続し、Relay がバックエンドサーバーへ転送します。

js
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(キック)を傍受して、メッセージを差し替えます。

js
player.on('clientbound', ({ name, params }) => {
  if (name === 'disconnect') {
    params.message = 'Intercepted' // どんな理由でも「Intercepted」に
  }
})

チャットへの加工(発言に時刻を付加) ​

プレイヤー → サーバー方向のチャットに情報を追記します。

js
player.on('serverbound', ({ name, params }) => {
  if (name === 'text' && params.type === 'chat') {
    params.message += `, on ${new Date().toLocaleString()}`
  }
})

特定コマンドの遮断 ​

危険なコマンドや不要な操作をサーバーに届く前に握りつぶします。

js
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 パケットを直接送れます。上流サーバーには送られません。

js
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 にするのがポイントです。

js
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 から接続中のプレイヤー全員に送信します(通常サーバーと同じ構造)。

js
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 をカウントして、接続の様子を可視化できます。

js
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() を使います。

js
// クライアントが参加したら、上流サーバーへ独自コマンドを実行
player.on('join', () => {
  player.upstream.queue('command_request', {
    command: '/give @a diamond 1',
    origin: { type: 'server' }, // フォーマットはサーバー仕様に合わせる
    internal: false,
    version: 'latest'
  })
})

パケット形式の整合

上流へ送るパケットは上流サーバーのバージョンに合わせたフィールド構成が必要です。command_request はバージョン依存のフィールドがあるため、詳細はクライアントガイドの注意点を参照してください。

参加時の挨拶ダイアログ(モーダル)+ 応答 ​

メニュー表示の応答処理例として、modal タイプで 2 択を返してもらうパターンです。

js
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 にできます(実サーバーへの中継と同じ挙動)。

js
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 サブクラスを注入すると、中継挙動を丸ごと制御できます。

js
const { Relay, Player } = require('bedrock-protocol')

class MyPlayer extends Player {
  // ここで RelayPlayer 相当の中継処理を実装する(高度)
}

const relay = new Relay({ ..., relayPlayer: MyPlayer })

詳細はRelay ガイドの拡張ポイントを参照してください。