strom: add 1.8 protocol support, raw-to-tmcpr, disable raw-capture

- Add minecraft-data for version 1.8 (protocol 47)
- Generate proto_generated/v1_8 from 1.8 protocol.json
- Fix generator compatibility: missing CompareTo natives (i16, i32, u16, u32,
  i64, string, nbt, optionalNbt), parameterized type placeholder handling ()
- Add raw-to-tmcpr subcommand: converts raw tcpflow captures to TMCPR
  with zlib decompression and state-aware compression threshold tracking
- Extend print-replay: 1.8 state transitions (success->Play, login_start->Play,
  handshake/status initial states, set_protocol NextState detection)
- Comment out broken raw-capture subcommand (use tcpflow instead)
- Fix nbt.Anon Encode/Decode on value receivers
This commit is contained in:
Admin Else
2026-08-04 16:10:15 +02:00
parent 3273244c42
commit b162d0d401
34 changed files with 55210 additions and 249 deletions
+4 -2
View File
@@ -19,7 +19,8 @@ import (
"github.com/admin-else/strom/cmd/strom/packet_inspector"
"github.com/admin-else/strom/cmd/strom/print_nbt"
"github.com/admin-else/strom/cmd/strom/print_replay"
"github.com/admin-else/strom/cmd/strom/raw_capture"
// "github.com/admin-else/strom/cmd/strom/raw_capture" // removed: use tcpflow instead for raw TCP capture
"github.com/admin-else/strom/cmd/strom/raw_to_tmcpr"
"github.com/admin-else/strom/cmd/strom/registry_data_hunter"
"github.com/admin-else/strom/cmd/strom/serve_tmcpr"
"github.com/admin-else/strom/cmd/strom/serve_world"
@@ -61,7 +62,8 @@ var subcommands = map[string]func(args []string) error{
"extract-mca": extract_mca.Run,
"data": data.Run,
"print-replay": print_replay.Run,
"raw-capture": raw_capture.Run,
// "raw-capture": raw_capture.Run, // removed: use tcpflow instead for raw TCP capture, then raw-to-tmcpr
"raw-to-tmcpr": raw_to_tmcpr.Run,
"registry-data-hunter": registry_data_hunter.Run,
"serve-world": serve_world.Run,
"mca-to-tmcpr": mca_to_tmcpr.Run,
+36 -17
View File
@@ -25,7 +25,7 @@ var (
FileFlag = cmd.String("file", "", "The tmcpr file to print")
ProtocolVersion = cmd.Int("protocol", -1, "The protocol version")
CreateTestsPath = cmd.String("create-tests", "", "")
StateFlag = cmd.String("state", "login", "Initial state: login, config, play")
StateFlag = cmd.String("state", "login", "Initial state: handshake, login, config, status, play")
DirectionFlag = cmd.String("direction", "clientbound", "Packet direction: clientbound (ToClient) or serverbound (ToServer)")
)
@@ -106,22 +106,6 @@ func UnpackPacket(r io.Reader) (packet proto_base.EncodeDecodeAble, i proto_base
packet = &proto.UnCodablePacket{Err: proto.BadPacketIdErr, Data: packetData, Info: i}
return
}
switch direction {
case proto_base.ToClient:
switch i.Name {
case "success":
state = proto_base.Configuration
case "finish_configuration":
state = proto_base.Play
}
case proto_base.ToServer:
switch i.Name {
case "login_acknowledged":
state = proto_base.Configuration
case "finish_configuration":
state = proto_base.Play
}
}
packet = reflect.New(reflect.TypeOf(i.Type).Elem()).Interface().(proto_base.EncodeDecodeAble)
err = packet.Decode(b)
if err != nil {
@@ -132,9 +116,40 @@ func UnpackPacket(r io.Reader) (packet proto_base.EncodeDecodeAble, i proto_base
packet = &proto.UnCodablePacket{Err: proto.PacketNotFullyDecodedErr, Data: packetData[indexafterid:], Info: i}
return
}
trackState(packet, i)
return
}
func trackState(packet proto_base.EncodeDecodeAble, i proto_base.PacketInfo) {
switch i.Name {
case "set_protocol":
v := reflect.ValueOf(packet).Elem()
nsField := v.FieldByName("NextState")
if nsField.IsValid() && nsField.Kind() == reflect.Int32 {
switch nsField.Int() {
case 1:
state = proto_base.Status
case 2:
state = proto_base.Login
}
}
case "login_start":
if int32(*ProtocolVersion) <= 47 {
state = proto_base.Play
}
case "success":
if int32(*ProtocolVersion) >= 764 {
state = proto_base.Configuration
} else {
state = proto_base.Play
}
case "finish_configuration":
state = proto_base.Play
case "login_acknowledged":
state = proto_base.Configuration
}
}
func Run(args []string) (err error) {
err = cmd.Parse(args)
if err != nil {
@@ -147,12 +162,16 @@ func Run(args []string) (err error) {
return fmt.Errorf("no file specified")
}
switch *StateFlag {
case "handshake":
state = proto_base.Handshaking
case "login":
state = proto_base.Login
case "config":
state = proto_base.Configuration
case "play":
state = proto_base.Play
case "status":
state = proto_base.Status
default:
return fmt.Errorf("unknown state: %s", *StateFlag)
}
+15 -206
View File
@@ -3,162 +3,29 @@ package raw_capture
import (
"flag"
"fmt"
"log/slog"
"io"
"net"
"os"
"time"
"github.com/admin-else/strom/mc/api"
"github.com/admin-else/strom/mc/client"
"github.com/admin-else/strom/mc/data"
"github.com/admin-else/strom/mc/event"
"github.com/admin-else/strom/mc/proto"
"github.com/admin-else/strom/mc/proto/replay"
"github.com/admin-else/strom/mc/proto_base"
"github.com/admin-else/strom/mc/proto_generated/v1_21_11"
"github.com/admin-else/strom/mc/server"
"github.com/admin-else/strom/mc/text"
)
var Cmd = flag.NewFlagSet("raw-capture", flag.ContinueOnError)
var OfflineNameFlag = Cmd.String("name", "raw_capture", "offline player name")
var Token = Cmd.String("token", "", "token to use")
var TargetAddr = Cmd.String("addr", "127.0.0.1:25565", "address to connect to")
var ListenAddr = Cmd.String("listen", "127.0.0.1:25566", "address to listen on")
var ClientboundFile = Cmd.String("clientbound-file", "clientbound.tmcpr", "file to write clientbound packets to")
var ServerboundFile = Cmd.String("serverbound-file", "serverbound.tmcpr", "file to write serverbound packets to")
var Version = Cmd.String("version", "", "client protocol version (e.g. 26.1); if empty, detect from server status")
var StatusResponse = server.StatusResponse{
Version: server.StatusResponseVersion{
Name: text.Pretty("STROM raw-capture"),
Protocol: 0,
},
Players: server.StatusResponsePlayers{},
Description: text.Pretty("STROM raw-capture proxy"),
Favicon: "",
EnforcesSecureChat: false,
}
type Proxy struct {
Client, Servee *proto.Conn
clientboundFile *os.File
serverboundFile *os.File
clientboundWriter *replay.Writer
serverboundWriter *replay.Writer
startTime time.Time
}
func (p *Proxy) Start() (err error) {
p.startTime = time.Now()
p.Client.RegisterCritical(p.OnAnything)
p.Servee.RegisterCritical(p.OnAnything)
p.Client.RegisterCriticalUntilLatest(p.OnCompress)
p.Servee.RegisterCriticalUntilLatest(p.OnHandshake)
p.Servee.RegisterCriticalUntilLatest(p.OnLoginAcknowledged)
p.Servee.RegisterCriticalUntilLatest(p.OnFinishConfiguration)
errChan := make(chan error)
go func() {
errChan <- p.Client.StartConn()
}()
go func() {
errChan <- p.Servee.StartConn()
}()
err = <-errChan
return
}
func (p *Proxy) timestamp() uint32 {
return uint32(time.Since(p.startTime).Milliseconds())
}
func (p *Proxy) OnAnything(e event.Anything) (err error) {
packet, ok := e.Val.(proto_base.EncodeDecodeAble)
if !ok {
return
}
packetInfo, ok := proto.LookupPacketInfoByType(packet)
if !ok {
return
}
if packetInfo.Name == "encryption_begin" {
return fmt.Errorf("encryption requested by remote server; raw capture cannot continue")
}
switch packetInfo.Direction {
case proto_base.ToServer:
err = p.serverboundWriter.WritePacket(packet, p.timestamp())
if err != nil {
return
}
err = p.Client.Send(packet)
case proto_base.ToClient:
err = p.clientboundWriter.WritePacket(packet, p.timestamp())
if err != nil {
return
}
err = p.Servee.Send(packet)
}
return
}
func (p *Proxy) OnCompress(packet *v1_21_11.LoginToClientPacketCompress) (err error) {
err = p.clientboundWriter.WritePacket(packet, p.timestamp())
func Handle(c net.Conn, addr string) {
sockPuppet, err := net.Dial("tcp", addr)
if err != nil {
return
}
err = p.Servee.Send(packet)
if err != nil {
return
}
p.Servee.SetCompressionThreshold(packet.Threshold)
p.Client.SetCompressionThreshold(packet.Threshold)
return event.DontForwardErr
io.MultiWriter()
go io.Copy(c, sockPuppet)
io.Copy(sockPuppet, c)
}
func (p *Proxy) OnHandshake(packet *v1_21_11.HandshakingToServerPacketSetProtocol) (err error) {
state := proto_base.State(packet.NextState)
p.Servee.SetState(state)
return event.DontForwardErr
}
func (p *Proxy) OnLoginAcknowledged(packet *v1_21_11.LoginToServerPacketLoginAcknowledged) (err error) {
err = p.serverboundWriter.WritePacket(packet, p.timestamp())
if err != nil {
return
}
err = p.Client.Send(packet)
if err != nil {
return
}
p.Servee.SetState(proto_base.Configuration)
p.Client.SetState(proto_base.Configuration)
return event.DontForwardErr
}
func (p *Proxy) OnFinishConfiguration(packet *v1_21_11.ConfigurationToServerPacketFinishConfiguration) (err error) {
err = p.serverboundWriter.WritePacket(packet, p.timestamp())
if err != nil {
return
}
err = p.Client.Send(packet)
if err != nil {
return
}
p.Servee.SetState(proto_base.Play)
p.Client.SetState(proto_base.Play)
return event.DontForwardErr
}
func Run(args []string) (err error) {
err = Cmd.Parse(args)
if err != nil {
@@ -181,73 +48,15 @@ func Run(args []string) (err error) {
if err != nil {
return
}
var targetVersion string
var targetProtocol int32
if *Version == "" {
status, err := client.Status(nil, resolvedAddr)
if err != nil {
return fmt.Errorf("failed to detect server version: %w", err)
}
targetProtocol = status.Version.Protocol
versionInfo, err := data.LookUpVersionByProtocolVersion(targetProtocol)
if err != nil {
return fmt.Errorf("unknown protocol version %d: %w", targetProtocol, err)
}
targetVersion = versionInfo.MinecraftVersion
} else {
versionInfo, err := data.LookUpVersionByName(*Version)
if err != nil {
return fmt.Errorf("unknown version %s: %w", *Version, err)
}
targetVersion = versionInfo.MinecraftVersion
targetProtocol = versionInfo.Version
l, err := net.Listen("tcp", *ListenAddr)
if err != nil {
return
}
StatusResponse.Version.Name = text.Pretty("STROM raw-capture " + targetVersion)
StatusResponse.Version.Protocol = targetProtocol
StatusResponse.Description = text.Pretty(fmt.Sprintf("Raw capture proxy forwards to %s (%s)", resolvedAddr, targetVersion))
slog.Info("Starting raw capture", "listen", *ListenAddr, "target", resolvedAddr, "version", targetVersion, "protocol", targetProtocol, "clientbound", *ClientboundFile, "serverbound", *ServerboundFile)
err = server.StartServerWithOnConn(*ListenAddr, func(serveeConn *proto.Conn) (err error) {
proxy := &Proxy{
Servee: serveeConn,
clientboundFile: clientboundFile,
serverboundFile: serverboundFile,
clientboundWriter: replay.NewWriter(clientboundFile),
serverboundWriter: replay.NewWriter(serverboundFile),
}
acc := api.NewOfflineAccount(*OfflineNameFlag)
if *Token != "" {
acc, err = api.NewAccountFromYGG(*Token)
if err != nil {
return
}
}
_, err = server.ServeLogin(serveeConn, server.WithOtherAccount(acc), server.WithStatus(StatusResponse), server.WithoutOnlineMode(), server.WithoutEncryption())
for {
c, err := l.Accept()
if err != nil {
return
continue
}
c, err := client.Connect(nil, resolvedAddr, targetVersion)
if err != nil {
return
}
defer c.Close()
err = client.LoginRaw(c, acc)
if err != nil {
return
}
proxy.Client = c
proxy.Client.AlwaysDecode = true
proxy.Servee.AlwaysDecode = true
return proxy.Start()
})
return
go Handle(c, resolvedAddr)
}
}
+243
View File
@@ -0,0 +1,243 @@
package raw_to_tmcpr
import (
"bytes"
"compress/zlib"
"errors"
"flag"
"fmt"
"io"
"os"
"time"
"github.com/admin-else/strom/mc/proto/replay"
"github.com/admin-else/strom/mc/proto_base"
)
var Cmd = flag.NewFlagSet("raw-to-tmcpr", flag.ContinueOnError)
var ClientboundIn = Cmd.String("clientbound", "", "raw tcpflow file for clientbound (server->client) direction")
var ServerboundIn = Cmd.String("serverbound", "", "raw tcpflow file for serverbound (client->server) direction")
var ClientboundOut = Cmd.String("clientbound-out", "clientbound.tmcpr", "output TMCPR file for clientbound")
var ServerboundOut = Cmd.String("serverbound-out", "serverbound.tmcpr", "output TMCPR file for serverbound")
var ClientboundState = Cmd.String("clientbound-state", "login", "initial state for clientbound: handshake, login, status, play")
var ServerboundState = Cmd.String("serverbound-state", "handshake", "initial state for serverbound: handshake, login, status, play")
type captureReader struct {
r io.Reader
out *replay.Writer
state proto_base.State
threshold int32
startTime time.Time
haveStartTime bool
direction proto_base.Direction
pendingThreshold int32
}
func (cr *captureReader) setStartTime() {
if !cr.haveStartTime {
cr.startTime = time.Now()
cr.haveStartTime = true
}
}
func (cr *captureReader) timestamp() uint32 {
return uint32(time.Since(cr.startTime).Milliseconds())
}
func readPacketBytes(r io.Reader, threshold int32) ([]byte, error) {
packetLen, err := proto_base.DecodeVarInt(r)
if err != nil {
return nil, fmt.Errorf("read packet length: %w", err)
}
rawBytes := make([]byte, packetLen)
_, err = io.ReadFull(r, rawBytes)
if err != nil {
return nil, fmt.Errorf("read packet data: %w", err)
}
if threshold >= 0 {
buf := bytes.NewReader(rawBytes)
uncompressedLen, err := proto_base.DecodeVarInt(buf)
if err != nil {
return nil, fmt.Errorf("read uncompressed length: %w", err)
}
if uncompressedLen == 0 {
return io.ReadAll(buf)
} else {
zReader, err := zlib.NewReader(buf)
if err != nil {
return nil, fmt.Errorf("zlib reader: %w", err)
}
defer zReader.Close()
decompressed, err := io.ReadAll(zReader)
if err != nil {
return nil, fmt.Errorf("decompress: %w", err)
}
return decompressed, nil
}
}
return rawBytes, nil
}
func (cr *captureReader) trackState(packetBytes []byte) {
buf := bytes.NewReader(packetBytes)
packetId, err := proto_base.DecodeVarInt(buf)
if err != nil {
return
}
switch cr.state {
case proto_base.Handshaking:
if cr.direction == proto_base.ToServer && packetId == 0 {
_, err = proto_base.DecodeVarInt(buf) // protocol version
if err != nil {
return
}
_, err = proto_base.DecodeString(buf) // server address
if err != nil {
return
}
portBytes := make([]byte, 2)
_, err = io.ReadFull(buf, portBytes)
if err != nil {
return
}
nextState, err := proto_base.DecodeVarInt(buf)
if err != nil {
return
}
if nextState == 2 {
cr.state = proto_base.Login
} else if nextState == 1 {
cr.state = proto_base.Status
}
}
case proto_base.Login:
if cr.direction == proto_base.ToClient {
if packetId == 0x03 {
threshold, err := proto_base.DecodeVarInt(buf)
if err == nil {
cr.threshold = threshold
}
} else if packetId == 0x02 {
cr.state = proto_base.Play
}
} else if cr.direction == proto_base.ToServer && packetId == 0 {
if cr.pendingThreshold >= 0 {
cr.threshold = cr.pendingThreshold
}
}
case proto_base.Status:
case proto_base.Play:
}
}
func (cr *captureReader) process() (count int, err error) {
for {
packetBytes, err := readPacketBytes(cr.r, cr.threshold)
if err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
return count, nil
}
return count, err
}
cr.setStartTime()
cr.trackState(packetBytes)
err = cr.out.WriteRaw(packetBytes, cr.timestamp())
if err != nil {
return count, fmt.Errorf("write TMCPR: %w", err)
}
count++
}
}
func Run(args []string) (err error) {
err = Cmd.Parse(args)
if err != nil {
return
}
if *ClientboundIn == "" && *ServerboundIn == "" {
return fmt.Errorf("at least one of -clientbound or -serverbound must be specified")
}
parseState := func(name string) (proto_base.State, error) {
switch name {
case "handshake":
return proto_base.Handshaking, nil
case "login":
return proto_base.Login, nil
case "play":
return proto_base.Play, nil
case "status":
return proto_base.Status, nil
default:
return 0, fmt.Errorf("unknown state: %s (valid: handshake, login, status, play)", name)
}
}
var compressionThreshold int32 = -1
if *ClientboundIn != "" {
state, err := parseState(*ClientboundState)
if err != nil {
return err
}
compressionThreshold, err = processFile(*ClientboundIn, *ClientboundOut, state, proto_base.ToClient, -1)
if err != nil {
return fmt.Errorf("clientbound: %w", err)
}
}
if *ServerboundIn != "" {
state, err := parseState(*ServerboundState)
if err != nil {
return err
}
_, err = processFile(*ServerboundIn, *ServerboundOut, state, proto_base.ToServer, compressionThreshold)
if err != nil {
return fmt.Errorf("serverbound: %w", err)
}
}
return
}
func processFile(inPath, outPath string, initial proto_base.State, dir proto_base.Direction, initialThreshold int32) (finalThreshold int32, err error) {
in, err := os.Open(inPath)
if err != nil {
return initialThreshold, fmt.Errorf("open input: %w", err)
}
defer in.Close()
out, err := os.Create(outPath)
if err != nil {
return initialThreshold, fmt.Errorf("create output: %w", err)
}
defer out.Close()
cr := &captureReader{
r: in,
out: replay.NewWriter(out),
state: initial,
threshold: -1,
direction: dir,
pendingThreshold: initialThreshold,
}
if dir == proto_base.ToClient {
cr.threshold = initialThreshold
}
count, err := cr.process()
if err != nil {
return cr.threshold, err
}
fmt.Printf("%s: %d packets written to %s (threshold=%d)\n", inPath, count, outPath, cr.threshold)
return cr.threshold, nil
}