Skip to content

パケットモニター (MITM ロガー)

クライアント(Minecraftアプリ)と実際の外部サーバーの間に割り込み、やり取りされる全パケットをリアルタイムにコンソールへダンプ・解析するプロキシツールの実践サンプルです。


完全なコード例

go
package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"os/signal"
	"sync"
	"syscall"

	"github.com/sandertv/gophertunnel/minecraft"
	"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
)

func main() {
	var (
		listenAddr = flag.String("listen", "0.0.0.0:19132", "プロキシ待受アドレス")
		remoteAddr = flag.String("remote", "127.0.0.1:19133", "転送先サーバーアドレス")
	)
	flag.Parse()

	// 1. プロキシのリスナーを初期化
	lc := minecraft.ListenConfig{
		AuthenticationDisabled: true,
	}

	listener, err := lc.Listen("raknet", *listenAddr)
	if err != nil {
		log.Fatalf("リスナー起動失敗: %v", err)
	}
	defer listener.Close()

	log.Printf("パケットモニター起動: %s -> %s\n", *listenAddr, *remoteAddr)

	// Ctrl+C で安全に終了
	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		<-sigCh
		_ = listener.Close()
		os.Exit(0)
	}()

	for {
		clientConn, err := listener.Accept()
		if err != nil {
			log.Printf("Accept 失敗: %v", err)
			break
		}

		go handleSession(clientConn.(*minecraft.Conn), *remoteAddr)
	}
}

func handleSession(clientConn *minecraft.Conn, remoteAddr string) {
	defer clientConn.Close()

	log.Printf("[+] クライアント接続: %s (%s)", clientConn.RemoteAddr(), clientConn.IdentityData().DisplayName)

	// 2. リモートサーバーへクライアント情報を使って代理接続
	dialer := minecraft.Dialer{
		ClientData:   clientConn.ClientData(),
		IdentityData: clientConn.IdentityData(),
	}

	serverConn, err := dialer.Dial("raknet", remoteAddr)
	if err != nil {
		log.Printf("[-] リモート接続失敗: %v", err)
		return
	}
	defer serverConn.Close()

	// 3. リモートのゲーム初期状態をクライアントに同期
	if err := clientConn.StartGame(serverConn.GameData()); err != nil {
		log.Printf("[-] StartGame 同期エラー: %v", err)
		return
	}

	var wg sync.WaitGroup
	wg.Add(2)

	// Client -> Server
	go func() {
		defer wg.Done()
		for {
			pk, err := clientConn.ReadPacket()
			if err != nil {
				break
			}

			// パケットのロギング
			logPacket("C -> S", pk)

			if err := serverConn.WritePacket(pk); err != nil {
				break
			}
		}
	}()

	// Server -> Client
	go func() {
		defer wg.Done()
		for {
			pk, err := serverConn.ReadPacket()
			if err != nil {
				break
			}

			// パケットのロギング
			logPacket("S -> C", pk)

			if err := clientConn.WritePacket(pk); err != nil {
				break
			}
		}
	}()

	wg.Wait()
	log.Printf("[-] セッション切断: %s", clientConn.RemoteAddr())
}

// パケット種別に応じたハイライト表示
func logPacket(direction string, pk packet.Packet) {
	switch p := pk.(type) {
	case *packet.Text:
		fmt.Printf("[%s] [Text] %s: %s\n", direction, p.SourceName, p.Message)
	case *packet.MovePlayer:
		// 頻繁に流れるため詳細のみ1行表示
		fmt.Printf("[%s] [MovePlayer] Pos=%v, Yaw=%.1f, Pitch=%.1f\n", direction, p.Position, p.Yaw, p.Pitch)
	case *packet.PlayerAction:
		fmt.Printf("[%s] [PlayerAction] Action=%d, Pos=%v\n", direction, p.ActionType, p.BlockPosition)
	case *packet.InventoryTransaction:
		fmt.Printf("[%s] [InventoryTransaction] Type=%d, ActionsCount=%d\n", direction, p.TransactionData, len(p.Actions))
	case *packet.LevelChunk:
		fmt.Printf("[%s] [Chunk] X=%d, Z=%d, SubChunks=%d\n", direction, p.Position.X(), p.Position.Z(), p.SubChunkCount)
	default:
		// その他のパケット
		fmt.Printf("[%s] [0x%02x] %T\n", direction, pk.ID(), pk)
	}
}

Released under the MIT License.