Files
omv-dijiang/works/patch/protocol/minecraft/protocol.go.patch
T
2026-03-05 15:48:45 +08:00

660 lines
16 KiB
Diff

--- /dev/null
+++ b/protocol/minecraft/protocol.go
@@ -0,0 +1,656 @@
+// OMV
+package minecraft
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/sha1"
+ "crypto/sha256"
+ "crypto/x509"
+ "encoding/binary"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net"
+
+ "github.com/sagernet/sing-box/option"
+ "github.com/sagernet/smux"
+)
+
+// Minecraft protocol constants
+const (
+ protocolVersion = 765 // 1.20.4
+ versionName = "1.20.4"
+ maxPacketSize = 2097151 // 2^21 - 1
+
+ stateHandshake = 0
+ stateStatus = 1
+ stateLogin = 2
+
+ // Handshake state packets
+ packetHandshake = 0x00
+
+ // Status state packets
+ packetStatusRequest = 0x00
+ packetStatusResponse = 0x00
+ packetPingRequest = 0x01
+ packetPingResponse = 0x01
+
+ // Login state packets
+ packetLoginStart = 0x00
+ packetEncryptionRequest = 0x01
+ packetEncryptionResponse = 0x01
+ packetLoginSuccess = 0x02
+ packetLoginDisconnect = 0x00
+
+ rsaKeyBits = 1024
+ verifyTokenLen = 4
+ sharedSecretLen = 16
+)
+
+// VarInt encoding/decoding
+
+func readVarInt(r io.Reader) (int32, error) {
+ var result int32
+ var shift uint
+ buf := make([]byte, 1)
+ for {
+ _, err := io.ReadFull(r, buf)
+ if err != nil {
+ return 0, err
+ }
+ b := buf[0]
+ result |= int32(b&0x7F) << shift
+ if b&0x80 == 0 {
+ break
+ }
+ shift += 7
+ if shift >= 35 {
+ return 0, fmt.Errorf("VarInt too big")
+ }
+ }
+ return result, nil
+}
+
+func writeVarInt(w io.Writer, value int32) error {
+ buf := encodeVarInt(value)
+ _, err := w.Write(buf)
+ return err
+}
+
+func encodeVarInt(value int32) []byte {
+ var buf [5]byte
+ n := 0
+ uv := uint32(value)
+ for {
+ b := byte(uv & 0x7F)
+ uv >>= 7
+ if uv != 0 {
+ b |= 0x80
+ }
+ buf[n] = b
+ n++
+ if uv == 0 {
+ break
+ }
+ }
+ return buf[:n]
+}
+
+func varIntLen(value int32) int {
+ return len(encodeVarInt(value))
+}
+
+// Packet reading/writing
+
+func readPacket(r io.Reader) (packetID int32, data []byte, err error) {
+ length, err := readVarInt(r)
+ if err != nil {
+ return 0, nil, err
+ }
+ if length < 0 || length > maxPacketSize {
+ return 0, nil, fmt.Errorf("invalid packet length: %d", length)
+ }
+ payload := make([]byte, length)
+ _, err = io.ReadFull(r, payload)
+ if err != nil {
+ return 0, nil, err
+ }
+ pr := bytes.NewReader(payload)
+ packetID, err = readVarInt(pr)
+ if err != nil {
+ return 0, nil, err
+ }
+ data = payload[varIntLen(packetID):]
+ return packetID, data, nil
+}
+
+func writePacket(w io.Writer, packetID int32, data []byte) error {
+ idBytes := encodeVarInt(packetID)
+ totalLen := int32(len(idBytes) + len(data))
+ if err := writeVarInt(w, totalLen); err != nil {
+ return err
+ }
+ if _, err := w.Write(idBytes); err != nil {
+ return err
+ }
+ if _, err := w.Write(data); err != nil {
+ return err
+ }
+ return nil
+}
+
+// String encoding (VarInt length + UTF-8 bytes)
+
+func readString(r io.Reader) (string, error) {
+ length, err := readVarInt(r)
+ if err != nil {
+ return "", err
+ }
+ if length < 0 || length > 32767 {
+ return "", fmt.Errorf("string too long: %d", length)
+ }
+ buf := make([]byte, length)
+ _, err = io.ReadFull(r, buf)
+ if err != nil {
+ return "", err
+ }
+ return string(buf), nil
+}
+
+func encodeString(s string) []byte {
+ var buf bytes.Buffer
+ buf.Write(encodeVarInt(int32(len(s))))
+ buf.WriteString(s)
+ return buf.Bytes()
+}
+
+// Byte array encoding (VarInt length + bytes)
+
+func readByteArray(r io.Reader) ([]byte, error) {
+ length, err := readVarInt(r)
+ if err != nil {
+ return nil, err
+ }
+ if length < 0 || length > 1048576 {
+ return nil, fmt.Errorf("byte array too long: %d", length)
+ }
+ buf := make([]byte, length)
+ _, err = io.ReadFull(r, buf)
+ if err != nil {
+ return nil, err
+ }
+ return buf, nil
+}
+
+func encodeByteArray(data []byte) []byte {
+ var buf bytes.Buffer
+ buf.Write(encodeVarInt(int32(len(data))))
+ buf.Write(data)
+ return buf.Bytes()
+}
+
+// Handshake packet
+
+type handshakePacket struct {
+ ProtocolVersion int32
+ ServerAddress string
+ ServerPort uint16
+ NextState int32
+}
+
+func readHandshake(data []byte) (*handshakePacket, error) {
+ r := bytes.NewReader(data)
+ pv, err := readVarInt(r)
+ if err != nil {
+ return nil, err
+ }
+ addr, err := readString(r)
+ if err != nil {
+ return nil, err
+ }
+ var port uint16
+ if err := binary.Read(r, binary.BigEndian, &port); err != nil {
+ return nil, err
+ }
+ ns, err := readVarInt(r)
+ if err != nil {
+ return nil, err
+ }
+ return &handshakePacket{
+ ProtocolVersion: pv,
+ ServerAddress: addr,
+ ServerPort: port,
+ NextState: ns,
+ }, nil
+}
+
+func encodeHandshake(h *handshakePacket) []byte {
+ var buf bytes.Buffer
+ buf.Write(encodeVarInt(h.ProtocolVersion))
+ buf.Write(encodeString(h.ServerAddress))
+ binary.Write(&buf, binary.BigEndian, h.ServerPort)
+ buf.Write(encodeVarInt(h.NextState))
+ return buf.Bytes()
+}
+
+// Login Start packet
+
+type loginStartPacket struct {
+ Name string
+ UUID [16]byte
+}
+
+func readLoginStart(data []byte) (*loginStartPacket, error) {
+ r := bytes.NewReader(data)
+ name, err := readString(r)
+ if err != nil {
+ return nil, err
+ }
+ pkt := &loginStartPacket{Name: name}
+ // Read UUID (16 bytes) if available
+ if r.Len() >= 16 {
+ io.ReadFull(r, pkt.UUID[:])
+ }
+ return pkt, nil
+}
+
+func encodeLoginStart(name string) []byte {
+ var buf bytes.Buffer
+ buf.Write(encodeString(name))
+ // Write zero UUID
+ buf.Write(make([]byte, 16))
+ return buf.Bytes()
+}
+
+// Encryption Request packet
+
+type encryptionRequestPacket struct {
+ ServerID string
+ PublicKey []byte
+ VerifyToken []byte
+}
+
+func encodeEncryptionRequest(serverID string, pubKey []byte, verifyToken []byte) []byte {
+ var buf bytes.Buffer
+ buf.Write(encodeString(serverID))
+ buf.Write(encodeByteArray(pubKey))
+ buf.Write(encodeByteArray(verifyToken))
+ // Note: ShouldAuthenticate field was added in 1.20.5 (protocol 766).
+ // We target 1.20.4 (protocol 765), so this field is absent.
+ return buf.Bytes()
+}
+
+func readEncryptionRequest(data []byte) (*encryptionRequestPacket, error) {
+ r := bytes.NewReader(data)
+ serverID, err := readString(r)
+ if err != nil {
+ return nil, err
+ }
+ pubKey, err := readByteArray(r)
+ if err != nil {
+ return nil, err
+ }
+ verifyToken, err := readByteArray(r)
+ if err != nil {
+ return nil, err
+ }
+ return &encryptionRequestPacket{
+ ServerID: serverID,
+ PublicKey: pubKey,
+ VerifyToken: verifyToken,
+ }, nil
+}
+
+// Encryption Response packet
+
+type encryptionResponsePacket struct {
+ SharedSecret []byte
+ VerifyToken []byte
+}
+
+func encodeEncryptionResponse(sharedSecret []byte, verifyToken []byte) []byte {
+ var buf bytes.Buffer
+ buf.Write(encodeByteArray(sharedSecret))
+ buf.Write(encodeByteArray(verifyToken))
+ return buf.Bytes()
+}
+
+func readEncryptionResponse(data []byte) (*encryptionResponsePacket, error) {
+ r := bytes.NewReader(data)
+ secret, err := readByteArray(r)
+ if err != nil {
+ return nil, err
+ }
+ token, err := readByteArray(r)
+ if err != nil {
+ return nil, err
+ }
+ return &encryptionResponsePacket{
+ SharedSecret: secret,
+ VerifyToken: token,
+ }, nil
+}
+
+// Login Success packet
+
+func encodeLoginSuccess(name string) []byte {
+ // Generate a deterministic UUID from username
+ hash := sha1.Sum([]byte("OfflinePlayer:" + name))
+ hash[6] = hash[6]&0x0f | 0x30 // version 3
+ hash[8] = hash[8]&0x3f | 0x80 // variant 2
+
+ var buf bytes.Buffer
+ buf.Write(hash[:16]) // UUID
+ buf.Write(encodeString(name))
+ buf.Write(encodeVarInt(0)) // Number Of Properties = 0
+ buf.WriteByte(0x01) // Strict Error Handling = true
+ return buf.Bytes()
+}
+
+// Disconnect (Login) packet — plain text reason
+func encodeLoginDisconnect(reason string) []byte {
+ msg, _ := json.Marshal(map[string]string{"text": reason})
+ return encodeString(string(msg))
+}
+
+// Disconnect (Login) packet — translation key (e.g. multiplayer.disconnect.server_full)
+func encodeLoginDisconnectTranslate(key string) []byte {
+ msg, _ := json.Marshal(map[string]string{"translate": key})
+ return encodeString(string(msg))
+}
+
+// Status Response packet — mirrors real Vanilla server JSON format
+
+type statusResponse struct {
+ Version *statusVersion `json:"version,omitempty"`
+ Players *statusPlayers `json:"players,omitempty"`
+ Description json.RawMessage `json:"description,omitempty"`
+ Favicon string `json:"favicon,omitempty"`
+ EnforcesSecureChat bool `json:"enforcesSecureChat,omitempty"`
+}
+
+type statusVersion struct {
+ Name string `json:"name"`
+ Protocol int `json:"protocol"`
+}
+
+type statusPlayers struct {
+ Max int `json:"max"`
+ Online int `json:"online"`
+ Sample []statusPlayerSample `json:"sample,omitempty"`
+}
+
+type statusPlayerSample struct {
+ Name string `json:"name"`
+ ID string `json:"id"`
+}
+
+// encodeDescription converts a json.RawMessage description to the wire format.
+// Accepts a JSON Chat Component (object/array), a JSON string, or null/empty.
+// A JSON string like "hello" is unwrapped and re-wrapped as {"text": "hello"}.
+func encodeDescription(desc json.RawMessage) json.RawMessage {
+ if len(desc) == 0 || string(desc) == "null" {
+ data, _ := json.Marshal(map[string]string{"text": "A Minecraft Server"})
+ return data
+ }
+ // Already an object or array — use as-is
+ if desc[0] == '{' || desc[0] == '[' {
+ return desc
+ }
+ // JSON string value (e.g. "hello") — unwrap and wrap as {"text": "..."}
+ if desc[0] == '"' {
+ var s string
+ if json.Unmarshal(desc, &s) == nil {
+ data, _ := json.Marshal(map[string]string{"text": s})
+ return data
+ }
+ }
+ // Fallback
+ data, _ := json.Marshal(map[string]string{"text": "A Minecraft Server"})
+ return data
+}
+
+func encodeStatusResponse(status *option.MinecraftStatus) []byte {
+ resp := statusResponse{}
+
+ // Version
+ if status.Version != nil {
+ resp.Version = &statusVersion{
+ Name: status.Version.Name,
+ Protocol: status.Version.Protocol,
+ }
+ } else {
+ resp.Version = &statusVersion{Name: versionName, Protocol: protocolVersion}
+ }
+
+ // Players
+ if status.Players != nil {
+ players := &statusPlayers{
+ Max: status.Players.Max,
+ Online: status.Players.Online,
+ }
+ for _, s := range status.Players.Sample {
+ id := s.ID
+ if id == "" {
+ hash := sha1.Sum([]byte("OfflinePlayer:" + s.Name))
+ hash[6] = hash[6]&0x0f | 0x30
+ hash[8] = hash[8]&0x3f | 0x80
+ id = fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", hash[0:4], hash[4:6], hash[6:8], hash[8:10], hash[10:16])
+ }
+ players.Sample = append(players.Sample, statusPlayerSample{Name: s.Name, ID: id})
+ }
+ resp.Players = players
+ } else {
+ resp.Players = &statusPlayers{Max: 20, Online: 0}
+ }
+
+ // Description (plain text or JSON Chat Component)
+ resp.Description = encodeDescription(status.Description)
+
+ // Favicon
+ resp.Favicon = status.Favicon
+
+ // EnforcesSecureChat
+ resp.EnforcesSecureChat = status.EnforcesSecureChat
+
+ data, _ := json.Marshal(resp)
+ return encodeString(string(data))
+}
+
+// Legacy Server List Ping (pre-1.7)
+// Detects 0xFE as first byte; responds with 0xFF kick containing server info
+
+const legacyPingByte = 0xFE
+
+// descriptionToPlainText extracts plain text from a json.RawMessage description.
+// Used for legacy ping which only supports plain text.
+func descriptionToPlainText(desc json.RawMessage) string {
+ if len(desc) == 0 || string(desc) == "null" {
+ return "A Minecraft Server"
+ }
+ // JSON string → unwrap
+ if desc[0] == '"' {
+ var s string
+ if json.Unmarshal(desc, &s) == nil {
+ return s
+ }
+ }
+ // JSON object → extract "text" field
+ if desc[0] == '{' {
+ var obj struct {
+ Text string `json:"text"`
+ }
+ if json.Unmarshal(desc, &obj) == nil && obj.Text != "" {
+ return obj.Text
+ }
+ }
+ return "A Minecraft Server"
+}
+
+func encodeLegacyPingResponse(status *option.MinecraftStatus) []byte {
+ ver := versionName
+ proto := protocolVersion
+ if status.Version != nil {
+ if status.Version.Name != "" {
+ ver = status.Version.Name
+ }
+ if status.Version.Protocol != 0 {
+ proto = status.Version.Protocol
+ }
+ }
+ motd := descriptionToPlainText(status.Description)
+ var online, max int
+ if status.Players != nil {
+ online = status.Players.Online
+ max = status.Players.Max
+ } else {
+ max = 20
+ }
+
+ // Response format: 0xFF + string length (uint16 BE) + UTF-16BE string
+ // String: "§1\0<protocol>\0<version>\0<motd>\0<online>\0<max>"
+ payload := fmt.Sprintf("\u00a71\x00%d\x00%s\x00%s\x00%d\x00%d",
+ proto, ver, motd, online, max)
+ runes := []rune(payload)
+
+ // Build response: 0xFF + length (uint16 BE) + UTF-16BE chars
+ var buf bytes.Buffer
+ buf.WriteByte(0xFF)
+ binary.Write(&buf, binary.BigEndian, uint16(len(runes)))
+ for _, r := range runes {
+ binary.Write(&buf, binary.BigEndian, uint16(r))
+ }
+ return buf.Bytes()
+}
+
+// Shared secret derivation from password + verify token
+
+func deriveSharedSecret(password string, verifyToken []byte) []byte {
+ h := sha256.New()
+ h.Write([]byte(password))
+ h.Write(verifyToken)
+ sum := h.Sum(nil)
+ return sum[:sharedSecretLen]
+}
+
+// AES/CFB8 encrypted connection wrapper
+// Minecraft uses CFB8 mode (1-byte segments), not standard CFB128
+
+type cfb8Cipher struct {
+ block cipher.Block
+ iv []byte
+}
+
+func newCFB8Encrypt(block cipher.Block, iv []byte) *cfb8Cipher {
+ ivCopy := make([]byte, len(iv))
+ copy(ivCopy, iv)
+ return &cfb8Cipher{block: block, iv: ivCopy}
+}
+
+func newCFB8Decrypt(block cipher.Block, iv []byte) *cfb8Cipher {
+ ivCopy := make([]byte, len(iv))
+ copy(ivCopy, iv)
+ return &cfb8Cipher{block: block, iv: ivCopy}
+}
+
+func (c *cfb8Cipher) encrypt(dst, src []byte) {
+ bs := c.block.BlockSize()
+ tmp := make([]byte, bs)
+ for i := range src {
+ c.block.Encrypt(tmp, c.iv)
+ dst[i] = src[i] ^ tmp[0]
+ copy(c.iv, c.iv[1:])
+ c.iv[bs-1] = dst[i]
+ }
+}
+
+func (c *cfb8Cipher) decrypt(dst, src []byte) {
+ bs := c.block.BlockSize()
+ tmp := make([]byte, bs)
+ for i := range src {
+ c.block.Encrypt(tmp, c.iv)
+ copy(c.iv, c.iv[1:])
+ c.iv[bs-1] = src[i]
+ dst[i] = src[i] ^ tmp[0]
+ }
+}
+
+// encryptedConn wraps a net.Conn with Minecraft AES/CFB8 encryption
+type encryptedConn struct {
+ net.Conn
+ enc *cfb8Cipher
+ dec *cfb8Cipher
+}
+
+func newEncryptedConn(conn net.Conn, sharedSecret []byte) (*encryptedConn, error) {
+ block, err := aes.NewCipher(sharedSecret)
+ if err != nil {
+ return nil, err
+ }
+ // Minecraft uses the shared secret as both key and IV
+ return &encryptedConn{
+ Conn: conn,
+ enc: newCFB8Encrypt(block, sharedSecret),
+ dec: newCFB8Decrypt(block, sharedSecret),
+ }, nil
+}
+
+func (c *encryptedConn) Read(b []byte) (int, error) {
+ n, err := c.Conn.Read(b)
+ if n > 0 {
+ c.dec.decrypt(b[:n], b[:n])
+ }
+ return n, err
+}
+
+func (c *encryptedConn) Write(b []byte) (int, error) {
+ encrypted := make([]byte, len(b))
+ c.enc.encrypt(encrypted, b)
+ return c.Conn.Write(encrypted)
+}
+
+// RSA helpers
+
+func generateRSAKeyPair() (*rsa.PrivateKey, error) {
+ return rsa.GenerateKey(rand.Reader, rsaKeyBits)
+}
+
+func marshalPublicKey(pub *rsa.PublicKey) ([]byte, error) {
+ return x509.MarshalPKIXPublicKey(pub)
+}
+
+func parsePublicKey(data []byte) (*rsa.PublicKey, error) {
+ pub, err := x509.ParsePKIXPublicKey(data)
+ if err != nil {
+ return nil, err
+ }
+ rsaPub, ok := pub.(*rsa.PublicKey)
+ if !ok {
+ return nil, fmt.Errorf("not an RSA public key")
+ }
+ return rsaPub, nil
+}
+
+func generateVerifyToken() ([]byte, error) {
+ token := make([]byte, verifyTokenLen)
+ _, err := rand.Read(token)
+ return token, err
+}
+
+// smux config
+
+func smuxConfig() *smux.Config {
+ config := smux.DefaultConfig()
+ config.KeepAliveDisabled = true
+ return config
+}
+
+// Stream protocol constants (same as MySQL protocol)
+const (
+ commandTCP byte = 0x01
+ commandUDP byte = 0x03
+)