LoginSignup
0
0

RCONをGoで自前実装してMinecraftサーバーに"/say Hello!"を送るまで

Last updated at Posted at 2023-12-19

はじめに

この記事はKLab Engineer Advent Calendar 2023 20日目の記事です。

RCONを実装したライブラリは既存のものがすでにたくさんありますが、今回はあえて一からプロトコルを実装してみる記事となっています。

RCONとは?

TCP/IPベースの通信を介してゲームサーバーにコマンドを送信するプロトコルです。

MinecraftのサーバーではSource RCON ProtocolをベースにしたRCONを利用してリモートでコマンドを実行できます。

なお、Minecraftに限らず、Ark: Survival EvolvedRustなど、他のゲームでも同じような仕組みが利用できます。

RCONを普通に使いたい場合

本記事では簡単なコマンドが送れるようになるまでしか実装しないので、単純にRCONを利用したい場合は、実用に耐えうる以下のようなパッケージを利用してください。

また、上記のGo用パッケージ以外にも色々な言語で実装されているので、自分の環境にあったものを利用してください。

Goで実装してみる

では実際にGoでRCONプロトコルを実装してみます。

MinecraftのRCONは詳細な仕様などが公開されていないため、Source RCON Protocolを参考にしながら実装します。

パケットの構造

パケットは以下のように構成されています。

フィールド サイズ 説明
Size int32 (リトルエンディアン) 4バイト パケットのサイズ。(Size自体のサイズは含まない)
ID int32 (リトルエンディアン) 4バイト レスポンスの際に使うID。今回はリクエストしか送らないので0で良い。
Type int32 (リトルエンディアン) 4バイト パケットの種類。詳しい説明はPacket Typeを参照。
Body nullで終了するASCII文字列 10バイト~4086バイト + 1バイト 本文。コマンドなどはここに入る。
Empty String nullで終了するASCII文字列 1バイト 空文字

rcon.drawio (4).png

パケットを作る

パケットの構造がわかったのでGoでコードを書いてみます。
まずは、パケットの構造 通りのパケットを[]byte型で作るところまでやってみます。

パケット用の構造体を作成

type PacketType int32

const (
	ResponseValue             PacketType = 0
	ExecCommandOrAuthResponse            = 2
	Auth                                 = 3
)

type Packet struct {
	Size int32
	ID   int32
	Type PacketType
	Body []byte
}

構造体を初期化する関数

func newPacket(id int32, packetType PacketType, body []byte) Packet {
	return Packet{
        // bodyの長さ + 10でSizeを除く全体の長さが求められる
        // 10の内訳 -> 10 = ID(int32なので4) + Type(int32なので4) + null(1) + null(1)
		Size: int32(len(body) + 10),  
		ID:   id,
		Type: packetType,
		Body: body,
	}
}

構造体をbufferに書き込んで[]byteにする関数

func (p *Packet) marshal() ([]byte, error) {
    // p.Sizeにp.Size自身の長さを足すとp.Size込の長さになる
	buf := bytes.NewBuffer(make([]byte, 0, p.Size+4))

	binary.Write(buf, binary.LittleEndian, p.Size)
	binary.Write(buf, binary.LittleEndian, p.ID)
	binary.Write(buf, binary.LittleEndian, p.Type)
	binary.Write(buf, binary.LittleEndian, p.Body)
    binary.Write(buf, binary.LittleEndian, [2]byte{})  // null

	return buf.Bytes(), nil
}

パケットの[]byteを構造体に戻す

レスポンス等のパケットを読むためにパケットを構造体に戻せるようにしてみます。

func unmashalPacket(r io.Reader) (Packet, error) {
	packet := Packet{}

	err := binary.Read(r, binary.LittleEndian, &packet.Size)
	if err != nil {
		return packet, err
	}

	err = binary.Read(r, binary.LittleEndian, &packet.ID)
	if err != nil {
		return packet, err
	}

	err = binary.Read(r, binary.LittleEndian, &packet.Type)
	if err != nil {
		return packet, err
	}

	// IDとType分のバイト数を引く
	packet.Body = make([]byte, packet.Size-8)
	_, err = io.ReadFull(r, packet.Body)
	if err != nil {
		return packet, err
	}

	// 末尾のnullを取り除く
	packet.Body = packet.Body[:len(packet.Body)-2]

	return packet, nil
}

パケットを送信する

パケット周りの処理が完成したので、次は実際にパケットを送信する処理を作ります。

TCP用のクライアント

TCP用のクライアントを用意します。

type Conn struct {
	conn net.Conn
}

func New(addr string, password string) (Conn, error) {
	conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
	if err != nil {
		return Conn{}, err
	}

	client := Conn{conn}
    // 後で実装
	err = client.authenticate(password)
	if err != nil {
		return Conn{}, err
	}

	return client, nil
}

func (c Conn) Close() {
	c.conn.Close()
}

client.authenticateは後で実装します。

送信処理

func (c Conn) send(packetType PacketType, bodyStr string) (Packet, error) {
	p := newPacket(0, packetType, []byte(bodyStr))
	b, err := p.marshal()
	if err != nil {
		return Packet{}, err
	}

	_, err = c.conn.Write(b)
	if err != nil {
		return Packet{}, err
	}

	resp, err := unmashalPacket(c.conn)
	if err != nil {
		return Packet{}, err
	}

	return resp, nil
}

func (c Conn) Exec(command string) error {
	p, err := c.send(ExecCommandOrAuthResponse, command)
	if err != nil {
		return err
	}

	fmt.Println("Response: " + string(p.Body))

	return nil
}

認証処理

RCONでコマンドを送るためには、はじめに認証処理を行う必要があります。
認証方法は単純で、サーバー側に設定されたパスワードをbodyに入れて送信するだけです。
ただし、PacketTypeを3にする必要がある点に注意してください。(コマンド実行は2

func (c Conn) authenticate(password string) error {
	p, err := c.send(Auth, password)
	if err != nil {
		return err
	}

	// 認証に失敗したらIDに-1が入る
	if p.ID == -1 {
		return errors.New("failed auth")
	}

	return err
}

Minecraft側の準備

RCON用の処理が完成したので、Minecraft側の準備を行っていきます。

サーバーを立てる

まずは、Minecraftのサーバーを立てるために必要なjarファイルを公式サイトからダウンロードします。
また、同じページに

# jarファイルの名前は適宜変更する
java -Xmx1024M -Xms1024M -jar minecraft_server.1.20.4.jar nogui

このような起動用のコマンドの例が書いてあるので、このコマンドをjarファイルと同じ階層で実行します。(ファイル等が生成されるのでjarファイルを適切なディレクトリに移動させてから実行したほうがいいです)

実行後eula.txtが生成されるので、リンク先の内容をよく読み問題なければeula=falseeula=trueに書き換えます。
その後もう一度コマンドを実行するとサーバーが立ち上がります。

サーバーの設定を変える

設定を変更してRCONを有効にします。

jarファイルと同じ階層に設定ファイル(server.properties)が生成されているので、以下の項目を変更します。

enable-rcon=true
rcon.password=任意のパスワード
rcon.port=25575  # 必要があれば変える

変更後一度サーバーを起動してみて以下のようなログが出ていれば設定できています。

[Server thread/INFO]: RCON running on 0.0.0.0:25575

"/say Hello!"を送信してみる

準備が整ったのでサーバーにRCONを使ってコマンドを送信してみます。

先程の実装を最低限CLIで利用できるようにしたサンプルを使って作業を行います。

サーバーを起動した状態で以下を実行します。

# 引数は適宜変更
go run main.go -h localhost:25575 -p パスワード -c "/say Hello!"

実行後、サーバーのログに表示されていたら成功しています。

[RCON Listener #1/INFO]: Thread RCON Client /0:0:0:0:0:0:0:1 started
[Server thread/INFO]: [Not Secure] [Rcon] Hello!
[RCON Client /0:0:0:0:0:0:0:1 #4/INFO]: Thread RCON Client /0:0:0:0:0:0:0:1 shutting down

クライアント側でも確認

2023-12-14_22.12.22.png

うまく送れています。

他のコマンドも試しに送ってみました。

go run main.go -h localhost:25575 -p hogehoge -c "/execute at @a run fill ~ ~ ~ ^10 ^50 ^10 tnt"

2023-12-14_22.26.15.png

問題なさそうですね。

小ネタ

実装の際、参考にしたSource RCON Protocolでは、コマンド(Body)はASCIIのみとなっており、日本語などは送信できない仕様になっています。

ですが、Minecraftで実装されているRCONではUTF-8が送信できます。

2023-12-14_22.33.04.png

そのため、このように日本語が含まれたコマンドが送信できます。

まとめ

今回は自分の勉強も兼ねてプロトコルを実装してみましたが、encoding/binarynetなどのパッケージ周りの理解が深まった気がします。

Minecraft以外のゲームでも、サーバーのコマンド実行用にRCONが実装されているゲームがあるので、自分でサーバーを立てたりするときに活用できると幸せになれると思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0