From cc581a4c2165ac29c57f51f0cedffbe04904dc12 Mon Sep 17 00:00:00 2001 From: Exclude0122 <173652916+Exclude0122@users.noreply.github.com> Date: Thu, 28 May 2026 05:59:47 -0400 Subject: [PATCH 01/13] minecraft finalmask (WIP) --- infra/conf/transport_finalmask.go | 40 +++ .../internet/finalmask/minecraft/config.go | 15 + .../internet/finalmask/minecraft/config.pb.go | 160 +++++++++ .../internet/finalmask/minecraft/config.proto | 17 + .../internet/finalmask/minecraft/protocol.go | 327 ++++++++++++++++++ .../internet/finalmask/minecraft/server.go | 294 ++++++++++++++++ .../internet/finalmask/minecraft/stream.go | 68 ++++ 7 files changed, 921 insertions(+) create mode 100644 transport/internet/finalmask/minecraft/config.go create mode 100644 transport/internet/finalmask/minecraft/config.pb.go create mode 100644 transport/internet/finalmask/minecraft/config.proto create mode 100644 transport/internet/finalmask/minecraft/protocol.go create mode 100644 transport/internet/finalmask/minecraft/server.go create mode 100644 transport/internet/finalmask/minecraft/stream.go diff --git a/infra/conf/transport_finalmask.go b/infra/conf/transport_finalmask.go index bdcf8294e933..dda1803099b9 100644 --- a/infra/conf/transport_finalmask.go +++ b/infra/conf/transport_finalmask.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" + "fmt" "net/netip" "net/url" "regexp" @@ -13,6 +14,7 @@ import ( "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/transport/internet/finalmask/fragment" "github.com/xtls/xray-core/transport/internet/finalmask/header/custom" + "github.com/xtls/xray-core/transport/internet/finalmask/minecraft" "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm" "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/header" "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/original" @@ -715,6 +717,44 @@ func (c *Xdns) Build() (proto.Message, error) { }, nil } +type Minecraft struct { + Usernames []string `json:"usernames"` + ShortId string `json:"shortId"` + PublicKeySha256 string `json:"publicKeySha256"` + + ShortIds []string `json:"shortIds"` + PrivateKey string `json:"privateKey"` +} + +func (c *Minecraft) Build() (proto.Message, error) { + + if c.Usernames == nil || len(c.Usernames) == 0 { + c.Usernames = []string{"Dream"} + } + + shortIds := make([][]byte, len(c.ShortIds)) + for k, v := range c.ShortIds { + var err error + shortIds[k], err = hex.DecodeString(v) + if err != nil || len(shortIds) > 8 { + return nil, fmt.Errorf("bad short ids: %s", v) + } + } + + shortId, err := hex.DecodeString(c.ShortId) + if err != nil { + return nil, fmt.Errorf("bad short id: %s", c.ShortId) + } + + return &minecraft.Config{ + ShortIds: shortIds, + ShortId: shortId, + Usernames: c.Usernames, + PublicKeySha256: c.PublicKeySha256, + PrivateKey: c.PrivateKey, + }, nil +} + type Xicmp struct { DGRAM bool `json:"dgram"` IPs []string `json:"ips"` diff --git a/transport/internet/finalmask/minecraft/config.go b/transport/internet/finalmask/minecraft/config.go new file mode 100644 index 000000000000..e48291eaaa2a --- /dev/null +++ b/transport/internet/finalmask/minecraft/config.go @@ -0,0 +1,15 @@ +package minecraft + +import ( + "net" +) + +func (c *Config) TCP() { +} + +func (c *Config) WrapConnClient(net.Conn) (net.Conn, error) { +} + +func (c *Config) WrapConnServer(conn net.Conn) (net.Conn, error) { + return wrapConnServer(conn, c.ShortIds, c.PrivateKey) +} diff --git a/transport/internet/finalmask/minecraft/config.pb.go b/transport/internet/finalmask/minecraft/config.pb.go new file mode 100644 index 000000000000..5097638a168e --- /dev/null +++ b/transport/internet/finalmask/minecraft/config.pb.go @@ -0,0 +1,160 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.19.6 +// source: config.proto + +package minecraft + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + ShortIds [][]byte `protobuf:"bytes,1,rep,name=short_ids,json=shortIds,proto3" json:"short_ids,omitempty"` + Usernames []string `protobuf:"bytes,2,rep,name=usernames,proto3" json:"usernames,omitempty"` + PrivateKey string `protobuf:"bytes,3,opt,name=private_key,json=privateKey,proto3" json:"private_key,omitempty"` + ShortId []byte `protobuf:"bytes,4,opt,name=short_id,json=shortId,proto3" json:"short_id,omitempty"` + PublicKeySha256 string `protobuf:"bytes,5,opt,name=public_key_sha256,json=publicKeySha256,proto3" json:"public_key_sha256,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetShortIds() [][]byte { + if x != nil { + return x.ShortIds + } + return nil +} + +func (x *Config) GetUsernames() []string { + if x != nil { + return x.Usernames + } + return nil +} + +func (x *Config) GetPrivateKey() string { + if x != nil { + return x.PrivateKey + } + return "" +} + +func (x *Config) GetShortId() []byte { + if x != nil { + return x.ShortId + } + return nil +} + +func (x *Config) GetPublicKeySha256() string { + if x != nil { + return x.PublicKeySha256 + } + return "" +} + +var File_config_proto protoreflect.FileDescriptor + +const file_config_proto_rawDesc = "" + + "\n" + + "\fconfig.proto\x12+xray.transport.internet.finalmask.minecraft\"\xab\x01\n" + + "\x06Config\x12\x1b\n" + + "\tshort_ids\x18\x01 \x03(\fR\bshortIds\x12\x1c\n" + + "\tusernames\x18\x02 \x03(\tR\tusernames\x12\x1f\n" + + "\vprivate_key\x18\x03 \x01(\tR\n" + + "privateKey\x12\x19\n" + + "\bshort_id\x18\x04 \x01(\fR\ashortId\x12*\n" + + "\x11public_key_sha256\x18\x05 \x01(\tR\x0fpublicKeySha256B\xa3\x01\n" + + "/com.xray.transport.internet.finalmask.minecraftP\x01Z@github.com/xtls/xray-core/transport/internet/finalmask/minecraft\xaa\x02+Xray.Transport.Internet.Finalmask.Minecraftb\x06proto3" + +var ( + file_config_proto_rawDescOnce sync.Once + file_config_proto_rawDescData []byte +) + +func file_config_proto_rawDescGZIP() []byte { + file_config_proto_rawDescOnce.Do(func() { + file_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_config_proto_rawDesc), len(file_config_proto_rawDesc))) + }) + return file_config_proto_rawDescData +} + +var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.finalmask.minecraft.Config +} +var file_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_config_proto_init() } +func file_config_proto_init() { + if File_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_config_proto_rawDesc), len(file_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_config_proto_goTypes, + DependencyIndexes: file_config_proto_depIdxs, + MessageInfos: file_config_proto_msgTypes, + }.Build() + File_config_proto = out.File + file_config_proto_goTypes = nil + file_config_proto_depIdxs = nil +} diff --git a/transport/internet/finalmask/minecraft/config.proto b/transport/internet/finalmask/minecraft/config.proto new file mode 100644 index 000000000000..39b4aa452bf2 --- /dev/null +++ b/transport/internet/finalmask/minecraft/config.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package xray.transport.internet.finalmask.minecraft; +option csharp_namespace = "Xray.Transport.Internet.Finalmask.Minecraft"; +option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/minecraft"; +option java_package = "com.xray.transport.internet.finalmask.minecraft"; +option java_multiple_files = true; + + +message Config { + repeated bytes short_ids = 1; + string private_key = 3; + + repeated string usernames = 2; + bytes short_id = 4; + string public_key_sha256 = 5; +} \ No newline at end of file diff --git a/transport/internet/finalmask/minecraft/protocol.go b/transport/internet/finalmask/minecraft/protocol.go new file mode 100644 index 000000000000..4c713502e543 --- /dev/null +++ b/transport/internet/finalmask/minecraft/protocol.go @@ -0,0 +1,327 @@ +package minecraft + +import ( + "bytes" + "fmt" + "io" +) + +type field interface { + readFrom(r io.Reader) error + writeTo(w io.Writer) error +} + +type mcPacket struct { + packetID int + data []byte +} + +func readPacket(b io.Reader) (*mcPacket, error) { + + var packetLength Varint + err := packetLength.readFrom(b) + if err != nil { + return nil, fmt.Errorf("read packet length: %w", err) + } + + var packetID Varint + err = packetID.readFrom(b) + if err != nil { + return nil, fmt.Errorf("read packet ID: %w", err) + } + + dataLength := int(packetLength) - varintSize(packetID) + if dataLength < 0 || dataLength > 1024*32 { + return nil, fmt.Errorf("read packet: bad length: %d", dataLength) + } + + data := make([]byte, dataLength) + _, err = io.ReadFull(b, data) + if err != nil { + return nil, fmt.Errorf("read packet data: %w", err) + } + + return &mcPacket{ + packetID: int(packetID), + data: data, + }, nil +} + +func (p *mcPacket) readFields(fields ...field) error { + + r := bytes.NewReader(p.data) + + for _, field := range fields { + err := field.readFrom(r) + if err != nil { + return fmt.Errorf("read packet field: %w", err) + } + } + + return nil +} + +type Varint int32 + +func (v *Varint) readFrom(r io.Reader) error { + SEGMENT_BITS := byte(0x7F) + CONTINUE_BIT := byte(0x80) + + var err error + + var value int32 = 0 + var position int32 = 0 + var currentByte byte + + for true { + currentByte, err = readByte(r) + if err != nil { + return fmt.Errorf("read varint: %w", err) + } + value |= int32(currentByte&SEGMENT_BITS) << position + + if (currentByte & CONTINUE_BIT) == 0 { + break + } + + position += 7 + + if position >= 32 { + return fmt.Errorf("read varint: too large") + } + } + + *v = Varint(value) + + return nil +} + +func (v *Varint) writeTo(w io.Writer) error { + SEGMENT_BITS := byte(0x7F) + CONTINUE_BIT := byte(0x80) + + value := int32(*v) + + for { + currentByte := byte(value & int32(SEGMENT_BITS)) + value >>= 7 + if value != 0 { + currentByte |= CONTINUE_BIT + } + + _, err := w.Write([]byte{currentByte}) + if err != nil { + return fmt.Errorf("write varint: %w", err) + } + + if value == 0 { + break + } + } + + return nil +} + +func varintSize(value Varint) int { + size := 0 + for { + size++ + value >>= 7 + if value == 0 { + break + } + } + return size +} + +type String string + +func (v *String) readFrom(r io.Reader) error { + var length Varint = 0 + + err := length.readFrom(r) + if err != nil { + return fmt.Errorf("read string: %w", err) + } + + if length < 0 || length > 4096 { + return fmt.Errorf("read string: bad length: %d", length) + } + + buf := make([]byte, length) + _, err = io.ReadFull(r, buf) + if err != nil { + return fmt.Errorf("read string: %w", err) + } + + *v = String(string(buf)) + + return nil +} + +func (v *String) writeTo(w io.Writer) error { + strBytes := []byte(*v) + length := Varint(len(strBytes)) + + err := length.writeTo(w) + if err != nil { + return fmt.Errorf("write string: %w", err) + } + + _, err = w.Write(strBytes) + if err != nil { + return fmt.Errorf("write string: %w", err) + } + + return nil +} + +type UnsignedShort uint16 + +func (v *UnsignedShort) readFrom(r io.Reader) error { + var buf [2]byte + _, err := io.ReadFull(r, buf[:]) + if err != nil { + return fmt.Errorf("read unsigned short: %w", err) + } + + *v = UnsignedShort(buf[0])<<8 | UnsignedShort(buf[1]) + + return nil +} + +func (v *UnsignedShort) writeTo(w io.Writer) error { + buf := []byte{byte(*v >> 8), byte(*v & 0xFF)} + _, err := w.Write(buf) + if err != nil { + return fmt.Errorf("write unsigned short: %w", err) + } + return nil +} + +type Long int64 + +func (v *Long) readFrom(r io.Reader) error { + var buf [8]byte + _, err := io.ReadFull(r, buf[:]) + if err != nil { + return fmt.Errorf("read long: %w", err) + } + + *v = Long(buf[0])<<56 | Long(buf[1])<<48 | Long(buf[2])<<40 | Long(buf[3])<<32 | + Long(buf[4])<<24 | Long(buf[5])<<16 | Long(buf[6])<<8 | Long(buf[7]) + + return nil +} + +func (v *Long) writeTo(w io.Writer) error { + buf := []byte{ + byte(*v >> 56), byte((*v >> 48) & 0xFF), byte((*v >> 40) & 0xFF), byte((*v >> 32) & 0xFF), + byte((*v >> 24) & 0xFF), byte((*v >> 16) & 0xFF), byte((*v >> 8) & 0xFF), byte(*v & 0xFF), + } + + _, err := w.Write(buf) + if err != nil { + return fmt.Errorf("write long: %w", err) + } + + return nil +} + +type UUID [16]byte + +func (v *UUID) readFrom(r io.Reader) error { + _, err := io.ReadFull(r, v[:]) + if err != nil { + return fmt.Errorf("read UUID: %w", err) + } + + return nil +} + +func (v *UUID) writeTo(w io.Writer) error { + _, err := w.Write(v[:]) + if err != nil { + return fmt.Errorf("write UUID: %w", err) + } + return nil +} + +type Bytes []byte + +func (v *Bytes) readFrom(r io.Reader) error { + + var length Varint + err := length.readFrom(r) + if err != nil { + return fmt.Errorf("read bytes: %w", err) + } + + if length < 0 || length >= 1024 { + return fmt.Errorf("read bytes: invalid size: %d", err) + } + + buf := make([]byte, length) + + _, err = io.ReadFull(r, buf) + if err != nil { + return fmt.Errorf("read bytes: %w", err) + } + + *v = append([]byte(*v), buf...) + + return nil +} + +func (v *Bytes) writeTo(w io.Writer) error { + _, err := w.Write(*v) + if err != nil { + return fmt.Errorf("write bytes: %w", err) + } + + return nil +} + +func readByte(r io.Reader) (byte, error) { + var buf [1]byte + _, err := io.ReadFull(r, buf[:]) + if err != nil { + return 0, fmt.Errorf("read byte: %w", err) + } + + return buf[0], nil +} + +func writePacket(w io.Writer, packetID int, fields ...field) error { + var dataBuf bytes.Buffer + + for _, field := range fields { + err := field.writeTo(&dataBuf) + if err != nil { + return fmt.Errorf("write packet field: %w", err) + } + } + + var buf bytes.Buffer + + var packetLength Varint = Varint(varintSize(Varint(packetID)) + dataBuf.Len()) + err := packetLength.writeTo(&buf) + if err != nil { + return fmt.Errorf("write packet length: %w", err) + } + + var packetIDVarint Varint = Varint(packetID) + err = packetIDVarint.writeTo(&buf) + if err != nil { + return fmt.Errorf("write packet ID: %w", err) + } + + buf.Write(dataBuf.Bytes()) + + _, err = w.Write(buf.Bytes()) + if err != nil { + return fmt.Errorf("write packet data: %w", err) + } + + return nil +} diff --git a/transport/internet/finalmask/minecraft/server.go b/transport/internet/finalmask/minecraft/server.go new file mode 100644 index 000000000000..b46ec05de39a --- /dev/null +++ b/transport/internet/finalmask/minecraft/server.go @@ -0,0 +1,294 @@ +package minecraft + +import ( + "bufio" + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/subtle" + "crypto/x509" + "encoding/pem" + "fmt" + "io" + "net" + "sync" + "time" +) + +// Response by vanilla 26.1.2 server. +var statusResponse = `{"description":"A Minecraft Server","players":{"max":20,"online":0},"version":{"name":"26.1.2","protocol":775},"enforcesSecureChat":true}` + +type serverState int + +var ( + stateHandshake serverState = 1 + stateFallback serverState = 2 + stateProxy serverState = 3 +) + +type serverConn struct { + reader io.Reader + writer io.Writer + c net.Conn + + state serverState + + handshakeLock sync.Mutex + shortIds [][]byte + rsaPrivateKey *rsa.PrivateKey + rsaPublicKey []byte +} + +func (c *serverConn) handshake() error { + c.handshakeLock.Lock() + defer c.handshakeLock.Unlock() + + if c.state != stateHandshake { + return nil + } + + // handshake timeout + err := c.c.SetDeadline(time.Now().Add(time.Second * 30)) + if err != nil { + return fmt.Errorf("set deadline: %w", err) + } + defer c.c.SetDeadline(time.Time{}) + + var ( + protocolVersion Varint + serverAddress String + serverPort UnsignedShort + nextState Varint + ) + + // handshake packet + + pkt, err := readPacket(c.reader) + if err != nil { + return fmt.Errorf("read handshake packet: %w", err) + } + + if pkt.packetID != 0 { + return fmt.Errorf("bad handshake packet id") + } + + err = pkt.readFields(&protocolVersion, &serverAddress, &serverPort, &nextState) + if err != nil { + return fmt.Errorf("read handshake packet: %w", err) + } + + if nextState == 1 { + + // Ping + + for range 2 { + + pkt, err := readPacket(c.reader) + if err != nil { + return fmt.Errorf("read packet: %w", err) + } + + if pkt.packetID == 0 { + // Status Request + + err = writePacket(c.writer, 0, new(String((statusResponse)))) + if err != nil { + return fmt.Errorf("write status response: %w", err) + } + + } else if pkt.packetID == 1 { + // Ping + + var payload Long + err = pkt.readFields(&payload) + if err != nil { + return fmt.Errorf("read ping packet: %w", err) + } + + err = writePacket(c.writer, 1, &payload) + if err != nil { + return fmt.Errorf("write ping response: %w", err) + } + + } + + } + + return fmt.Errorf("ping") + + } else if nextState == 2 { + + // Login + + // login start + + pkt, err := readPacket(c.reader) + if err != nil { + return fmt.Errorf("read login start packet: %w", err) + } + + if pkt.packetID != 0 { + return fmt.Errorf("bad login start packet id") + } + + var ( + username String + uuid UUID + ) + + err = pkt.readFields(&username, &uuid) + if err != nil { + return fmt.Errorf("read login start packet: %w", err) + } + + // encrypt request + + var ( + serverId String = String("") + publicKey Bytes = Bytes(c.rsaPublicKey) + verifyToken Bytes = Bytes(make([]byte, 4)) + shouldAuthenticate Varint = Varint(1) + ) + + rand.Read(verifyToken) + + err = writePacket(c.writer, 0x01, &serverId, &publicKey, &verifyToken, &shouldAuthenticate) + if err != nil { + return fmt.Errorf("write encryption request: %w", err) + } + + // encrypt response + + var ( + encryptedSharedSecret Bytes + encryptedVerifyToken Bytes + + sharedSecret []byte + decryptedVerifyToken []byte + ) + + pkt, err = readPacket(c.reader) + if err != nil { + return fmt.Errorf("read encrypt response: %w", err) + } + + if pkt.packetID != 0x01 { + return fmt.Errorf("bad encrypt response packet id") + } + + err = pkt.readFields(&encryptedSharedSecret, &encryptedVerifyToken) + if err != nil { + return fmt.Errorf("read encrypt response: %w", err) + } + + sharedSecret, err = rsa.DecryptPKCS1v15(rand.Reader, c.rsaPrivateKey, encryptedSharedSecret) + if err != nil { + return fmt.Errorf("decrypt shared secret: %w", err) + } + + decryptedVerifyToken, err = rsa.DecryptPKCS1v15(rand.Reader, c.rsaPrivateKey, encryptedVerifyToken) + if err != nil { + return fmt.Errorf("decrypt verify token: %w", err) + } + + if len(decryptedVerifyToken) < 4 || !bytes.Equal(verifyToken, decryptedVerifyToken[:4]) { + return fmt.Errorf("verify token mismatch") + } + + c.reader, err = newCryptoReader(c.reader, sharedSecret) + if err != nil { + return fmt.Errorf("new crypto reader: %w", err) + } + + c.writer, err = newCryptoWriter(c.writer, sharedSecret) + if err != nil { + return fmt.Errorf("new crypto writer: %w", err) + } + + // verify short id + + shortId := decryptedVerifyToken[4:] + + var ok byte = 0 + for _, s := range c.shortIds { + ok |= byte(subtle.ConstantTimeCompare(s, shortId)) + } + + if ok == 0 { + return fmt.Errorf("bad short id") + } + + return nil + + } else { + return fmt.Errorf("bad handshake packet: bad next state: %d", nextState) + } +} + +func (c *serverConn) Read(b []byte) (int, error) { + err := c.handshake() + if err != nil { + return 0, fmt.Errorf("handshake: %w", err) + } + + return c.reader.Read(b) +} + +func (c *serverConn) Write(b []byte) (int, error) { + err := c.handshake() + if err != nil { + return 0, fmt.Errorf("handshake: %w", err) + } + + return c.writer.Write(b) +} + +func (c *serverConn) Close() error { + return c.c.Close() +} + +func (c *serverConn) LocalAddr() net.Addr { + return c.c.LocalAddr() +} + +func (c *serverConn) RemoteAddr() net.Addr { + return c.c.RemoteAddr() +} + +func (c *serverConn) SetDeadline(t time.Time) error { + return c.c.SetDeadline(t) +} + +func (c *serverConn) SetReadDeadline(t time.Time) error { + return c.c.SetReadDeadline(t) +} + +func (c *serverConn) SetWriteDeadline(t time.Time) error { + return c.c.SetWriteDeadline(t) +} + +func wrapConnServer(c net.Conn, shortIds [][]byte, privateKey string) (net.Conn, error) { + + s := &serverConn{ + reader: bufio.NewReader(c), + writer: c, + c: c, + state: stateHandshake, + shortIds: shortIds, + } + + p, _ := pem.Decode([]byte(privateKey)) + if p == nil { + panic("minecraft finalmask: malformatted private key") + } + + var err error + s.rsaPrivateKey, err = x509.ParsePKCS1PrivateKey(p.Bytes) + if err != nil { + panic("minecraft finalmask: malformatted private key") + } + + s.rsaPublicKey = x509.MarshalPKCS1PublicKey(&s.rsaPrivateKey.PublicKey) + + return s, nil +} diff --git a/transport/internet/finalmask/minecraft/stream.go b/transport/internet/finalmask/minecraft/stream.go new file mode 100644 index 000000000000..a95d715edd62 --- /dev/null +++ b/transport/internet/finalmask/minecraft/stream.go @@ -0,0 +1,68 @@ +// Minecraft stream cipher +package minecraft + +import ( + "crypto/aes" + "crypto/cipher" + "fmt" + "io" +) + +type cryptoStream struct { + stream cipher.Stream + r io.Reader + w io.Writer +} + +func newCryptoReader(r io.Reader, sharedSecret []byte) (*cryptoStream, error) { + blockCipher, err := aes.NewCipher(sharedSecret) + if err != nil { + return nil, fmt.Errorf("new aes cipher: %w", err) + } + + stream := cipher.NewCFBDecrypter(blockCipher, sharedSecret) + + return &cryptoStream{stream: stream, r: r}, nil +} + +func (c *cryptoStream) Read(b []byte) (int, error) { + if c.r == nil { + panic("read on a write-only crypto stream") + } + + n, err := c.r.Read(b) + if err != nil { + return 0, fmt.Errorf("crypto reader: read: %w", err) + } + + c.stream.XORKeyStream(b[:n], b[:n]) + + return n, nil +} + +func newCryptoWriter(w io.Writer, sharedSecret []byte) (*cryptoStream, error) { + blockCipher, err := aes.NewCipher(sharedSecret) + if err != nil { + return nil, fmt.Errorf("new aes cipher: %w", err) + } + + stream := cipher.NewCFBEncrypter(blockCipher, sharedSecret) + + return &cryptoStream{stream: stream, w: w}, nil +} + +func (c *cryptoStream) Write(b []byte) (int, error) { + if c.w == nil { + panic("write on a read-only crypto stream") + } + + encrypted := make([]byte, len(b)) + c.stream.XORKeyStream(encrypted, b) + + n, err := c.w.Write(encrypted) + if err != nil { + return 0, fmt.Errorf("crypto writer: write: %w", err) + } + + return n, nil +} From e70c4bfb7e785daa5ddd03036e6cf01a4833760f Mon Sep 17 00:00:00 2001 From: Exclude0122 <173652916+Exclude0122@users.noreply.github.com> Date: Thu, 28 May 2026 09:30:47 -0400 Subject: [PATCH 02/13] minecraft finalmask --- main/commands/all/commands.go | 1 + main/commands/all/minecraft_rsa.go | 60 +++++ .../internet/finalmask/minecraft/cfb8.go | 64 +++++ .../internet/finalmask/minecraft/client.go | 227 ++++++++++++++++++ .../internet/finalmask/minecraft/config.go | 16 +- .../internet/finalmask/minecraft/protocol.go | 13 +- .../internet/finalmask/minecraft/server.go | 44 ++-- .../internet/finalmask/minecraft/stream.go | 4 +- 8 files changed, 407 insertions(+), 22 deletions(-) create mode 100644 main/commands/all/minecraft_rsa.go create mode 100644 transport/internet/finalmask/minecraft/cfb8.go create mode 100644 transport/internet/finalmask/minecraft/client.go diff --git a/main/commands/all/commands.go b/main/commands/all/commands.go index fba3a4b8bb43..70c4edd8dd4b 100644 --- a/main/commands/all/commands.go +++ b/main/commands/all/commands.go @@ -19,5 +19,6 @@ func init() { cmdMLDSA65, cmdMLKEM768, cmdVLESSEnc, + cmdMinecraftRSA, ) } diff --git a/main/commands/all/minecraft_rsa.go b/main/commands/all/minecraft_rsa.go new file mode 100644 index 000000000000..16958a9dbfd9 --- /dev/null +++ b/main/commands/all/minecraft_rsa.go @@ -0,0 +1,60 @@ +package all + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "fmt" + "strings" + + "github.com/xtls/xray-core/main/commands/base" +) + +var cmdMinecraftRSA = &base.Command{ + UsageLine: `{{.Exec}} minecraft`, + Short: `Generate RSA private key used in Minecraft finalmask`, + Long: ` +Generate RSA private key used in Minecraft finalmask. + +Output private key in PEM format and the SHA256 of the public key in PKIX format. +`, +} + +func init() { + cmdMinecraftRSA.Run = executeMinecraftRSA // break init loop +} + +func executeMinecraftRSA(cmd *base.Command, args []string) { + privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + panic(err) + } + + pkcs8, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + panic(err) + } + + pkcs8pem := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: pkcs8, + }) + + fmt.Println("Private Key:") + fmt.Println(strings.ReplaceAll(string(pkcs8pem), "\n", "\\n")) + + rsaPublicKey, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) + if err != nil { + panic(err) + } + + h := sha256.New() + h.Write(rsaPublicKey) + hash := h.Sum([]byte{}) + + fmt.Println("Public Key SHA256:") + fmt.Println(hex.EncodeToString(hash)) +} diff --git a/transport/internet/finalmask/minecraft/cfb8.go b/transport/internet/finalmask/minecraft/cfb8.go new file mode 100644 index 000000000000..d21fc94643fc --- /dev/null +++ b/transport/internet/finalmask/minecraft/cfb8.go @@ -0,0 +1,64 @@ +package minecraft + +// Source - https://stackoverflow.com/a/37234233 +// Posted by kostya, modified by community. See post 'Timeline' for change history +// Retrieved 2026-05-28, License - CC BY-SA 4.0 + +import "crypto/cipher" + +// CFB stream with 8 bit segment size +// See http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf +type cfb8 struct { + b cipher.Block + blockSize int + in []byte + out []byte + + decrypt bool +} + +func (x *cfb8) XORKeyStream(dst, src []byte) { + for i := range src { + x.b.Encrypt(x.out, x.in) + copy(x.in[:x.blockSize-1], x.in[1:]) + if x.decrypt { + x.in[x.blockSize-1] = src[i] + } + dst[i] = src[i] ^ x.out[0] + if !x.decrypt { + x.in[x.blockSize-1] = dst[i] + } + } +} + +// NewCFB8Encrypter returns a Stream which encrypts with cipher feedback mode +// (segment size = 8), using the given Block. The iv must be the same length as +// the Block's block size. +func newCFB8Encrypter(block cipher.Block, iv []byte) cipher.Stream { + return newCFB8(block, iv, false) +} + +// NewCFB8Decrypter returns a Stream which decrypts with cipher feedback mode +// (segment size = 8), using the given Block. The iv must be the same length as +// the Block's block size. +func newCFB8Decrypter(block cipher.Block, iv []byte) cipher.Stream { + return newCFB8(block, iv, true) +} + +func newCFB8(block cipher.Block, iv []byte, decrypt bool) cipher.Stream { + blockSize := block.BlockSize() + if len(iv) != blockSize { + // stack trace will indicate whether it was de or encryption + panic("cipher.newCFB: IV length must equal block size") + } + x := &cfb8{ + b: block, + blockSize: blockSize, + out: make([]byte, blockSize), + in: make([]byte, blockSize), + decrypt: decrypt, + } + copy(x.in, iv) + + return x +} diff --git a/transport/internet/finalmask/minecraft/client.go b/transport/internet/finalmask/minecraft/client.go new file mode 100644 index 000000000000..7d6d25ae6dfb --- /dev/null +++ b/transport/internet/finalmask/minecraft/client.go @@ -0,0 +1,227 @@ +package minecraft + +import ( + "bufio" + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "fmt" + "io" + "math/big" + "net" + "sync" + "time" +) + +type clientConn struct { + reader io.Reader + writer io.Writer + c net.Conn + + state clientState + + handshakeLock sync.Mutex + usernames []string + shortId []byte + publicKeyHash []byte +} + +type clientState int + +var ( + clientStateHandshake clientState = 1 + clientStateProxy clientState = 2 +) + +func newClientConn(c net.Conn, usernames []string, shortId []byte, publicKeyHashHex string) (*clientConn, error) { + publicKeyHash, err := hex.DecodeString(publicKeyHashHex) + if err != nil { + return nil, fmt.Errorf("decode public key hash: %w", err) + } + + return &clientConn{ + reader: bufio.NewReader(c), + writer: c, + c: c, + state: clientStateHandshake, + handshakeLock: sync.Mutex{}, + usernames: usernames, + shortId: shortId, + publicKeyHash: publicKeyHash, + }, nil +} + +func (c *clientConn) handshake() error { + c.handshakeLock.Lock() + defer c.handshakeLock.Unlock() + + if c.state != clientStateHandshake { + return nil + } + + // Handshake timeout + err := c.c.SetDeadline(time.Now().Add(time.Second * 30)) + if err != nil { + return fmt.Errorf("set deadline: %w", err) + } + defer c.c.SetDeadline(time.Time{}) + + var ( + protocolVersion Varint = Varint(775) + serverAddress String = String("mc.hypixel.net") + serverPort UnsignedShort = UnsignedShort(25565) + nextState Varint = Varint(2) + ) + + err = writePacket(c.writer, 0x00, &protocolVersion, &serverAddress, &serverPort, &nextState) + if err != nil { + return fmt.Errorf("write handshake packet: %w", err) + } + + // Login Start + var ( + username string + offlineUUID UUID + ) + + randomUsername, _ := rand.Int(rand.Reader, big.NewInt(int64(len(c.usernames)))) + username = c.usernames[randomUsername.Int64()] + generateOfflineUUID(&offlineUUID, string(username)) + + err = writePacket(c.writer, 0x00, new(String(username)), &offlineUUID) + if err != nil { + return fmt.Errorf("write login start: %w", err) + } + + // Encryption Request + pkt, err := readPacket(c.reader) + if err != nil { + return fmt.Errorf("read encryption request: %w", err) + } + + if pkt.packetID != 0x01 { + return fmt.Errorf("bad encrypt request packet id") + } + + var ( + serverId String + publicKey Bytes + verifyToken Bytes + ) + + err = pkt.readFields(&serverId, &publicKey, &verifyToken) + if err != nil { + return fmt.Errorf("read encryption request fields: %w", err) + } + + // verify public key hash + if len(c.publicKeyHash) > 0 { + keyHash := sha256.Sum256(publicKey) + if !bytes.Equal(keyHash[:], c.publicKeyHash) { + return fmt.Errorf("server public key mismatch") + } + } + + k, err := x509.ParsePKIXPublicKey(publicKey) + if err != nil { + return fmt.Errorf("parse server public key: %w", err) + } + + rsaPublicKey, ok := k.(*rsa.PublicKey) + if !ok { + return fmt.Errorf("parse server public key: not rsa") + } + + sharedSecret := make([]byte, 16) + rand.Read(sharedSecret) + + encryptedSharedSecret, err := rsa.EncryptPKCS1v15(rand.Reader, rsaPublicKey, sharedSecret) + if err != nil { + return fmt.Errorf("encrypt shared secret: %w", err) + } + + verifyToken = append(verifyToken, c.shortId...) // append short id + + encryptedVerifyToken, err := rsa.EncryptPKCS1v15(rand.Reader, rsaPublicKey, verifyToken) + if err != nil { + return fmt.Errorf("encrypt verify token: %w", err) + } + + // Send Encryption Response + err = writePacket( + c.writer, + 0x01, + (*Bytes)(&encryptedSharedSecret), + (*Bytes)(&encryptedVerifyToken), + ) + if err != nil { + return fmt.Errorf("write encryption response: %w", err) + } + + // Enable encryption + c.reader, err = newCryptoReader(c.reader, sharedSecret) + if err != nil { + return fmt.Errorf("new crypto reader: %w", err) + } + + c.writer, err = newCryptoWriter(c.writer, sharedSecret) + if err != nil { + return fmt.Errorf("new crypto writer: %w", err) + } + + c.state = clientStateProxy + + return nil +} + +func (c *clientConn) Read(b []byte) (int, error) { + err := c.handshake() + if err != nil { + return 0, fmt.Errorf("handshake: %w", err) + } + + return c.reader.Read(b) +} + +func (c *clientConn) Write(b []byte) (int, error) { + err := c.handshake() + if err != nil { + return 0, fmt.Errorf("handshake: %w", err) + } + + return c.writer.Write(b) +} + +func (c *clientConn) Close() error { + return c.c.Close() +} + +func (c *clientConn) LocalAddr() net.Addr { + return c.c.LocalAddr() +} + +func (c *clientConn) RemoteAddr() net.Addr { + return c.c.RemoteAddr() +} + +func (c *clientConn) SetDeadline(t time.Time) error { + return c.c.SetDeadline(t) +} + +func (c *clientConn) SetReadDeadline(t time.Time) error { + return c.c.SetReadDeadline(t) +} + +func (c *clientConn) SetWriteDeadline(t time.Time) error { + return c.c.SetWriteDeadline(t) +} + +func generateOfflineUUID(uuid *UUID, username string) { + h := sha256.Sum256([]byte("OfflinePlayer:" + username)) + copy(uuid[:], h[:16]) + uuid[6] = (uuid[6] & 0x0f) | 0x30 // UUID version 3 + uuid[8] = (uuid[8] & 0x3f) | 0x80 // UUID variant +} diff --git a/transport/internet/finalmask/minecraft/config.go b/transport/internet/finalmask/minecraft/config.go index e48291eaaa2a..4a47a1c26966 100644 --- a/transport/internet/finalmask/minecraft/config.go +++ b/transport/internet/finalmask/minecraft/config.go @@ -1,15 +1,27 @@ package minecraft import ( + "fmt" "net" ) func (c *Config) TCP() { } -func (c *Config) WrapConnClient(net.Conn) (net.Conn, error) { +func (c *Config) WrapConnClient(conn net.Conn) (net.Conn, error) { + cc, err := newClientConn(conn, c.Usernames, c.ShortId, c.PublicKeySha256) + if err != nil { + return nil, fmt.Errorf("minecraft finalmask: %w", err) + } + + return cc, nil } func (c *Config) WrapConnServer(conn net.Conn) (net.Conn, error) { - return wrapConnServer(conn, c.ShortIds, c.PrivateKey) + cc, err := wrapConnServer(conn, c.ShortIds, c.PrivateKey) + if err != nil { + return nil, fmt.Errorf("minecraft finalmask: %w", err) + } + + return cc, nil } diff --git a/transport/internet/finalmask/minecraft/protocol.go b/transport/internet/finalmask/minecraft/protocol.go index 4c713502e543..bd3bb6df9afc 100644 --- a/transport/internet/finalmask/minecraft/protocol.go +++ b/transport/internet/finalmask/minecraft/protocol.go @@ -1,3 +1,4 @@ +// Minecraft protocol package minecraft import ( @@ -274,7 +275,13 @@ func (v *Bytes) readFrom(r io.Reader) error { } func (v *Bytes) writeTo(w io.Writer) error { - _, err := w.Write(*v) + length := Varint(len(*v)) + err := length.writeTo(w) + if err != nil { + return fmt.Errorf("write bytes length: %w", err) + } + + _, err = w.Write(*v) if err != nil { return fmt.Errorf("write bytes: %w", err) } @@ -325,3 +332,7 @@ func writePacket(w io.Writer, packetID int, fields ...field) error { return nil } + +func writeDisconnectPacket(w io.Writer, reason string) error { + return writePacket(w, 0x00, new(String(reason))) +} diff --git a/transport/internet/finalmask/minecraft/server.go b/transport/internet/finalmask/minecraft/server.go index b46ec05de39a..40a2006faa32 100644 --- a/transport/internet/finalmask/minecraft/server.go +++ b/transport/internet/finalmask/minecraft/server.go @@ -21,9 +21,8 @@ var statusResponse = `{"description":"A Minecraft Server","players":{"max":20,"o type serverState int var ( - stateHandshake serverState = 1 - stateFallback serverState = 2 - stateProxy serverState = 3 + serverStateHandshake serverState = 1 + serverStateProxy serverState = 3 ) type serverConn struct { @@ -43,7 +42,7 @@ func (c *serverConn) handshake() error { c.handshakeLock.Lock() defer c.handshakeLock.Unlock() - if c.state != stateHandshake { + if c.state != serverStateHandshake { return nil } @@ -77,7 +76,8 @@ func (c *serverConn) handshake() error { return fmt.Errorf("read handshake packet: %w", err) } - if nextState == 1 { + switch nextState { + case 1: // Ping @@ -88,16 +88,15 @@ func (c *serverConn) handshake() error { return fmt.Errorf("read packet: %w", err) } - if pkt.packetID == 0 { - // Status Request + switch pkt.packetID { + case 0: // Status Request err = writePacket(c.writer, 0, new(String((statusResponse)))) if err != nil { return fmt.Errorf("write status response: %w", err) } - } else if pkt.packetID == 1 { - // Ping + case 1: // Ping var payload Long err = pkt.readFields(&payload) @@ -116,7 +115,7 @@ func (c *serverConn) handshake() error { return fmt.Errorf("ping") - } else if nextState == 2 { + case 2: // Login @@ -215,12 +214,15 @@ func (c *serverConn) handshake() error { } if ok == 0 { + writeDisconnectPacket(c.writer, `{"type":"translatable","translate":"multiplayer.disconnect.authservers_down"}`) return fmt.Errorf("bad short id") } + c.state = serverStateProxy + return nil - } else { + default: return fmt.Errorf("bad handshake packet: bad next state: %d", nextState) } } @@ -267,13 +269,13 @@ func (c *serverConn) SetWriteDeadline(t time.Time) error { return c.c.SetWriteDeadline(t) } -func wrapConnServer(c net.Conn, shortIds [][]byte, privateKey string) (net.Conn, error) { +func wrapConnServer(c net.Conn, shortIds [][]byte, privateKey string) (*serverConn, error) { s := &serverConn{ reader: bufio.NewReader(c), writer: c, c: c, - state: stateHandshake, + state: serverStateHandshake, shortIds: shortIds, } @@ -282,13 +284,21 @@ func wrapConnServer(c net.Conn, shortIds [][]byte, privateKey string) (net.Conn, panic("minecraft finalmask: malformatted private key") } - var err error - s.rsaPrivateKey, err = x509.ParsePKCS1PrivateKey(p.Bytes) + k, err := x509.ParsePKCS8PrivateKey(p.Bytes) if err != nil { - panic("minecraft finalmask: malformatted private key") + panic("minecraft finalmask: malformatted private key: " + err.Error()) + } + + var ok bool + s.rsaPrivateKey, ok = k.(*rsa.PrivateKey) + if !ok { + panic("minecraft finalmask: rsa private key required") } - s.rsaPublicKey = x509.MarshalPKCS1PublicKey(&s.rsaPrivateKey.PublicKey) + s.rsaPublicKey, err = x509.MarshalPKIXPublicKey(&s.rsaPrivateKey.PublicKey) + if err != nil { + panic("minecraft finalmask: marshal public key: " + err.Error()) + } return s, nil } diff --git a/transport/internet/finalmask/minecraft/stream.go b/transport/internet/finalmask/minecraft/stream.go index a95d715edd62..bd41dced9a4a 100644 --- a/transport/internet/finalmask/minecraft/stream.go +++ b/transport/internet/finalmask/minecraft/stream.go @@ -20,7 +20,7 @@ func newCryptoReader(r io.Reader, sharedSecret []byte) (*cryptoStream, error) { return nil, fmt.Errorf("new aes cipher: %w", err) } - stream := cipher.NewCFBDecrypter(blockCipher, sharedSecret) + stream := newCFB8Decrypter(blockCipher, sharedSecret) return &cryptoStream{stream: stream, r: r}, nil } @@ -46,7 +46,7 @@ func newCryptoWriter(w io.Writer, sharedSecret []byte) (*cryptoStream, error) { return nil, fmt.Errorf("new aes cipher: %w", err) } - stream := cipher.NewCFBEncrypter(blockCipher, sharedSecret) + stream := newCFB8Encrypter(blockCipher, sharedSecret) return &cryptoStream{stream: stream, w: w}, nil } From bc1481a1c26740ac9d1ef7380bf6f260c25b6777 Mon Sep 17 00:00:00 2001 From: Exclude0122 <173652916+Exclude0122@users.noreply.github.com> Date: Fri, 29 May 2026 05:14:36 -0400 Subject: [PATCH 03/13] custom address & port --- infra/conf/transport_finalmask.go | 14 ++++++- .../internet/finalmask/minecraft/client.go | 10 +++-- .../internet/finalmask/minecraft/config.go | 2 +- .../internet/finalmask/minecraft/config.pb.go | 42 +++++++++++++------ .../internet/finalmask/minecraft/config.proto | 2 + 5 files changed, 53 insertions(+), 17 deletions(-) diff --git a/infra/conf/transport_finalmask.go b/infra/conf/transport_finalmask.go index dda1803099b9..31a2bfacb011 100644 --- a/infra/conf/transport_finalmask.go +++ b/infra/conf/transport_finalmask.go @@ -718,6 +718,8 @@ func (c *Xdns) Build() (proto.Message, error) { } type Minecraft struct { + Address string `json:"address"` + Port uint16 `json:"port"` Usernames []string `json:"usernames"` ShortId string `json:"shortId"` PublicKeySha256 string `json:"publicKeySha256"` @@ -728,10 +730,18 @@ type Minecraft struct { func (c *Minecraft) Build() (proto.Message, error) { - if c.Usernames == nil || len(c.Usernames) == 0 { + if len(c.Usernames) == 0 { c.Usernames = []string{"Dream"} } + if c.Port == 0 { + c.Port = 25565 + } + + if c.Address == "" { + c.Address = "localhost" + } + shortIds := make([][]byte, len(c.ShortIds)) for k, v := range c.ShortIds { var err error @@ -752,6 +762,8 @@ func (c *Minecraft) Build() (proto.Message, error) { Usernames: c.Usernames, PublicKeySha256: c.PublicKeySha256, PrivateKey: c.PrivateKey, + Addresss: c.Address, + Port: uint32(c.Port), }, nil } diff --git a/transport/internet/finalmask/minecraft/client.go b/transport/internet/finalmask/minecraft/client.go index 7d6d25ae6dfb..3e4a08cc4249 100644 --- a/transport/internet/finalmask/minecraft/client.go +++ b/transport/internet/finalmask/minecraft/client.go @@ -27,6 +27,8 @@ type clientConn struct { usernames []string shortId []byte publicKeyHash []byte + address string + port uint16 } type clientState int @@ -36,7 +38,7 @@ var ( clientStateProxy clientState = 2 ) -func newClientConn(c net.Conn, usernames []string, shortId []byte, publicKeyHashHex string) (*clientConn, error) { +func newClientConn(c net.Conn, usernames []string, shortId []byte, publicKeyHashHex string, address string, port uint16) (*clientConn, error) { publicKeyHash, err := hex.DecodeString(publicKeyHashHex) if err != nil { return nil, fmt.Errorf("decode public key hash: %w", err) @@ -51,6 +53,8 @@ func newClientConn(c net.Conn, usernames []string, shortId []byte, publicKeyHash usernames: usernames, shortId: shortId, publicKeyHash: publicKeyHash, + address: address, + port: port, }, nil } @@ -71,8 +75,8 @@ func (c *clientConn) handshake() error { var ( protocolVersion Varint = Varint(775) - serverAddress String = String("mc.hypixel.net") - serverPort UnsignedShort = UnsignedShort(25565) + serverAddress String = String(c.address) + serverPort UnsignedShort = UnsignedShort(c.port) nextState Varint = Varint(2) ) diff --git a/transport/internet/finalmask/minecraft/config.go b/transport/internet/finalmask/minecraft/config.go index 4a47a1c26966..ed40e9b00240 100644 --- a/transport/internet/finalmask/minecraft/config.go +++ b/transport/internet/finalmask/minecraft/config.go @@ -9,7 +9,7 @@ func (c *Config) TCP() { } func (c *Config) WrapConnClient(conn net.Conn) (net.Conn, error) { - cc, err := newClientConn(conn, c.Usernames, c.ShortId, c.PublicKeySha256) + cc, err := newClientConn(conn, c.Usernames, c.ShortId, c.PublicKeySha256, c.Addresss, uint16(c.Port)) if err != nil { return nil, fmt.Errorf("minecraft finalmask: %w", err) } diff --git a/transport/internet/finalmask/minecraft/config.pb.go b/transport/internet/finalmask/minecraft/config.pb.go index 5097638a168e..e71ad9a01998 100644 --- a/transport/internet/finalmask/minecraft/config.pb.go +++ b/transport/internet/finalmask/minecraft/config.pb.go @@ -24,10 +24,12 @@ const ( type Config struct { state protoimpl.MessageState `protogen:"open.v1"` ShortIds [][]byte `protobuf:"bytes,1,rep,name=short_ids,json=shortIds,proto3" json:"short_ids,omitempty"` - Usernames []string `protobuf:"bytes,2,rep,name=usernames,proto3" json:"usernames,omitempty"` PrivateKey string `protobuf:"bytes,3,opt,name=private_key,json=privateKey,proto3" json:"private_key,omitempty"` + Usernames []string `protobuf:"bytes,2,rep,name=usernames,proto3" json:"usernames,omitempty"` ShortId []byte `protobuf:"bytes,4,opt,name=short_id,json=shortId,proto3" json:"short_id,omitempty"` PublicKeySha256 string `protobuf:"bytes,5,opt,name=public_key_sha256,json=publicKeySha256,proto3" json:"public_key_sha256,omitempty"` + Addresss string `protobuf:"bytes,6,opt,name=addresss,proto3" json:"addresss,omitempty"` + Port uint32 `protobuf:"varint,7,opt,name=port,proto3" json:"port,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -69,18 +71,18 @@ func (x *Config) GetShortIds() [][]byte { return nil } -func (x *Config) GetUsernames() []string { +func (x *Config) GetPrivateKey() string { if x != nil { - return x.Usernames + return x.PrivateKey } - return nil + return "" } -func (x *Config) GetPrivateKey() string { +func (x *Config) GetUsernames() []string { if x != nil { - return x.PrivateKey + return x.Usernames } - return "" + return nil } func (x *Config) GetShortId() []byte { @@ -97,18 +99,34 @@ func (x *Config) GetPublicKeySha256() string { return "" } +func (x *Config) GetAddresss() string { + if x != nil { + return x.Addresss + } + return "" +} + +func (x *Config) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + var File_config_proto protoreflect.FileDescriptor const file_config_proto_rawDesc = "" + "\n" + - "\fconfig.proto\x12+xray.transport.internet.finalmask.minecraft\"\xab\x01\n" + + "\fconfig.proto\x12+xray.transport.internet.finalmask.minecraft\"\xdb\x01\n" + "\x06Config\x12\x1b\n" + - "\tshort_ids\x18\x01 \x03(\fR\bshortIds\x12\x1c\n" + - "\tusernames\x18\x02 \x03(\tR\tusernames\x12\x1f\n" + + "\tshort_ids\x18\x01 \x03(\fR\bshortIds\x12\x1f\n" + "\vprivate_key\x18\x03 \x01(\tR\n" + - "privateKey\x12\x19\n" + + "privateKey\x12\x1c\n" + + "\tusernames\x18\x02 \x03(\tR\tusernames\x12\x19\n" + "\bshort_id\x18\x04 \x01(\fR\ashortId\x12*\n" + - "\x11public_key_sha256\x18\x05 \x01(\tR\x0fpublicKeySha256B\xa3\x01\n" + + "\x11public_key_sha256\x18\x05 \x01(\tR\x0fpublicKeySha256\x12\x1a\n" + + "\baddresss\x18\x06 \x01(\tR\baddresss\x12\x12\n" + + "\x04port\x18\a \x01(\rR\x04portB\xa3\x01\n" + "/com.xray.transport.internet.finalmask.minecraftP\x01Z@github.com/xtls/xray-core/transport/internet/finalmask/minecraft\xaa\x02+Xray.Transport.Internet.Finalmask.Minecraftb\x06proto3" var ( diff --git a/transport/internet/finalmask/minecraft/config.proto b/transport/internet/finalmask/minecraft/config.proto index 39b4aa452bf2..7b7f6c621749 100644 --- a/transport/internet/finalmask/minecraft/config.proto +++ b/transport/internet/finalmask/minecraft/config.proto @@ -14,4 +14,6 @@ message Config { repeated string usernames = 2; bytes short_id = 4; string public_key_sha256 = 5; + string addresss = 6; + uint32 port = 7; } \ No newline at end of file From bb7164cae22fb448ee44f330a004b9f00f481779 Mon Sep 17 00:00:00 2001 From: Exclude0122 <173652916+Exclude0122@users.noreply.github.com> Date: Fri, 29 May 2026 05:48:48 -0400 Subject: [PATCH 04/13] check short id max length --- infra/conf/transport_finalmask.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/conf/transport_finalmask.go b/infra/conf/transport_finalmask.go index 31a2bfacb011..2cbc2bbbff2f 100644 --- a/infra/conf/transport_finalmask.go +++ b/infra/conf/transport_finalmask.go @@ -752,7 +752,7 @@ func (c *Minecraft) Build() (proto.Message, error) { } shortId, err := hex.DecodeString(c.ShortId) - if err != nil { + if err != nil || len(shortId) > 8 { return nil, fmt.Errorf("bad short id: %s", c.ShortId) } From 6fd34e89a0bc3131cd9a121ddace91f92e4565e3 Mon Sep 17 00:00:00 2001 From: Exclude0122 <173652916+Exclude0122@users.noreply.github.com> Date: Fri, 29 May 2026 23:37:50 -0400 Subject: [PATCH 05/13] derive rsa private key from password --- infra/conf/transport_finalmask.go | 46 +++--- .../internet/finalmask/minecraft/client.go | 26 ++- .../internet/finalmask/minecraft/config.go | 4 +- .../internet/finalmask/minecraft/config.pb.go | 70 ++++---- .../internet/finalmask/minecraft/config.proto | 10 +- .../finalmask/minecraft/derivation.go | 103 ++++++++++++ .../finalmask/minecraft/derivation_test.go | 58 +++++++ .../finalmask/minecraft/handshake_test.go | 156 ++++++++++++++++++ .../internet/finalmask/minecraft/server.go | 55 +++--- 9 files changed, 404 insertions(+), 124 deletions(-) create mode 100644 transport/internet/finalmask/minecraft/derivation.go create mode 100644 transport/internet/finalmask/minecraft/derivation_test.go create mode 100644 transport/internet/finalmask/minecraft/handshake_test.go diff --git a/infra/conf/transport_finalmask.go b/infra/conf/transport_finalmask.go index 2cbc2bbbff2f..8acfef3341d3 100644 --- a/infra/conf/transport_finalmask.go +++ b/infra/conf/transport_finalmask.go @@ -1,6 +1,7 @@ package conf import ( + "crypto/x509" "encoding/base64" "encoding/hex" "encoding/json" @@ -718,14 +719,10 @@ func (c *Xdns) Build() (proto.Message, error) { } type Minecraft struct { - Address string `json:"address"` - Port uint16 `json:"port"` - Usernames []string `json:"usernames"` - ShortId string `json:"shortId"` - PublicKeySha256 string `json:"publicKeySha256"` - - ShortIds []string `json:"shortIds"` - PrivateKey string `json:"privateKey"` + Address string `json:"address"` + Port uint16 `json:"port"` + Usernames []string `json:"usernames"` + Password string `json:"password"` } func (c *Minecraft) Build() (proto.Message, error) { @@ -742,28 +739,27 @@ func (c *Minecraft) Build() (proto.Message, error) { c.Address = "localhost" } - shortIds := make([][]byte, len(c.ShortIds)) - for k, v := range c.ShortIds { - var err error - shortIds[k], err = hex.DecodeString(v) - if err != nil || len(shortIds) > 8 { - return nil, fmt.Errorf("bad short ids: %s", v) - } + if c.Password == "" { + return nil, fmt.Errorf("empty password") } - shortId, err := hex.DecodeString(c.ShortId) - if err != nil || len(shortId) > 8 { - return nil, fmt.Errorf("bad short id: %s", c.ShortId) + rsaPrivateKey, err := minecraft.DeriveRSAKey(c.Password) + if err != nil { + return nil, fmt.Errorf("derive minecraft rsa key: %w", err) + } + + rsaPublicKey, err := x509.MarshalPKIXPublicKey(&rsaPrivateKey.PublicKey) + if err != nil { + return nil, fmt.Errorf("marshal minecraft rsa public key: %w", err) } return &minecraft.Config{ - ShortIds: shortIds, - ShortId: shortId, - Usernames: c.Usernames, - PublicKeySha256: c.PublicKeySha256, - PrivateKey: c.PrivateKey, - Addresss: c.Address, - Port: uint32(c.Port), + Password: c.Password, + Usernames: c.Usernames, + Addresss: c.Address, + Port: uint32(c.Port), + RsaPrivateKey: x509.MarshalPKCS1PrivateKey(rsaPrivateKey), + RsaPublicKey: rsaPublicKey, }, nil } diff --git a/transport/internet/finalmask/minecraft/client.go b/transport/internet/finalmask/minecraft/client.go index 3e4a08cc4249..77fe76cb9f55 100644 --- a/transport/internet/finalmask/minecraft/client.go +++ b/transport/internet/finalmask/minecraft/client.go @@ -7,7 +7,6 @@ import ( "crypto/rsa" "crypto/sha256" "crypto/x509" - "encoding/hex" "fmt" "io" "math/big" @@ -25,8 +24,8 @@ type clientConn struct { handshakeLock sync.Mutex usernames []string - shortId []byte - publicKeyHash []byte + password string + rsaPublicKey []byte address string port uint16 } @@ -38,10 +37,9 @@ var ( clientStateProxy clientState = 2 ) -func newClientConn(c net.Conn, usernames []string, shortId []byte, publicKeyHashHex string, address string, port uint16) (*clientConn, error) { - publicKeyHash, err := hex.DecodeString(publicKeyHashHex) - if err != nil { - return nil, fmt.Errorf("decode public key hash: %w", err) +func newClientConn(c net.Conn, usernames []string, password string, rsaPublicKey []byte, address string, port uint16) (*clientConn, error) { + if len(rsaPublicKey) == 0 { + return nil, fmt.Errorf("empty rsa public key") } return &clientConn{ @@ -51,8 +49,8 @@ func newClientConn(c net.Conn, usernames []string, shortId []byte, publicKeyHash state: clientStateHandshake, handshakeLock: sync.Mutex{}, usernames: usernames, - shortId: shortId, - publicKeyHash: publicKeyHash, + password: password, + rsaPublicKey: rsaPublicKey, address: address, port: port, }, nil @@ -121,12 +119,8 @@ func (c *clientConn) handshake() error { return fmt.Errorf("read encryption request fields: %w", err) } - // verify public key hash - if len(c.publicKeyHash) > 0 { - keyHash := sha256.Sum256(publicKey) - if !bytes.Equal(keyHash[:], c.publicKeyHash) { - return fmt.Errorf("server public key mismatch") - } + if !bytes.Equal(publicKey, c.rsaPublicKey) { + return fmt.Errorf("server public key mismatch") } k, err := x509.ParsePKIXPublicKey(publicKey) @@ -147,7 +141,7 @@ func (c *clientConn) handshake() error { return fmt.Errorf("encrypt shared secret: %w", err) } - verifyToken = append(verifyToken, c.shortId...) // append short id + verifyToken = append(verifyToken, []byte(c.password)...) // append pre-shared password encryptedVerifyToken, err := rsa.EncryptPKCS1v15(rand.Reader, rsaPublicKey, verifyToken) if err != nil { diff --git a/transport/internet/finalmask/minecraft/config.go b/transport/internet/finalmask/minecraft/config.go index ed40e9b00240..a904d4079113 100644 --- a/transport/internet/finalmask/minecraft/config.go +++ b/transport/internet/finalmask/minecraft/config.go @@ -9,7 +9,7 @@ func (c *Config) TCP() { } func (c *Config) WrapConnClient(conn net.Conn) (net.Conn, error) { - cc, err := newClientConn(conn, c.Usernames, c.ShortId, c.PublicKeySha256, c.Addresss, uint16(c.Port)) + cc, err := newClientConn(conn, c.Usernames, c.Password, c.RsaPublicKey, c.Addresss, uint16(c.Port)) if err != nil { return nil, fmt.Errorf("minecraft finalmask: %w", err) } @@ -18,7 +18,7 @@ func (c *Config) WrapConnClient(conn net.Conn) (net.Conn, error) { } func (c *Config) WrapConnServer(conn net.Conn) (net.Conn, error) { - cc, err := wrapConnServer(conn, c.ShortIds, c.PrivateKey) + cc, err := wrapConnServer(conn, c.Password, c.RsaPrivateKey, c.RsaPublicKey) if err != nil { return nil, fmt.Errorf("minecraft finalmask: %w", err) } diff --git a/transport/internet/finalmask/minecraft/config.pb.go b/transport/internet/finalmask/minecraft/config.pb.go index e71ad9a01998..dac853ad889a 100644 --- a/transport/internet/finalmask/minecraft/config.pb.go +++ b/transport/internet/finalmask/minecraft/config.pb.go @@ -22,16 +22,15 @@ const ( ) type Config struct { - state protoimpl.MessageState `protogen:"open.v1"` - ShortIds [][]byte `protobuf:"bytes,1,rep,name=short_ids,json=shortIds,proto3" json:"short_ids,omitempty"` - PrivateKey string `protobuf:"bytes,3,opt,name=private_key,json=privateKey,proto3" json:"private_key,omitempty"` - Usernames []string `protobuf:"bytes,2,rep,name=usernames,proto3" json:"usernames,omitempty"` - ShortId []byte `protobuf:"bytes,4,opt,name=short_id,json=shortId,proto3" json:"short_id,omitempty"` - PublicKeySha256 string `protobuf:"bytes,5,opt,name=public_key_sha256,json=publicKeySha256,proto3" json:"public_key_sha256,omitempty"` - Addresss string `protobuf:"bytes,6,opt,name=addresss,proto3" json:"addresss,omitempty"` - Port uint32 `protobuf:"varint,7,opt,name=port,proto3" json:"port,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` + Usernames []string `protobuf:"bytes,2,rep,name=usernames,proto3" json:"usernames,omitempty"` + Addresss string `protobuf:"bytes,6,opt,name=addresss,proto3" json:"addresss,omitempty"` + Port uint32 `protobuf:"varint,7,opt,name=port,proto3" json:"port,omitempty"` + RsaPrivateKey []byte `protobuf:"bytes,8,opt,name=rsa_private_key,json=rsaPrivateKey,proto3" json:"rsa_private_key,omitempty"` + RsaPublicKey []byte `protobuf:"bytes,9,opt,name=rsa_public_key,json=rsaPublicKey,proto3" json:"rsa_public_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Config) Reset() { @@ -64,16 +63,9 @@ func (*Config) Descriptor() ([]byte, []int) { return file_config_proto_rawDescGZIP(), []int{0} } -func (x *Config) GetShortIds() [][]byte { +func (x *Config) GetPassword() string { if x != nil { - return x.ShortIds - } - return nil -} - -func (x *Config) GetPrivateKey() string { - if x != nil { - return x.PrivateKey + return x.Password } return "" } @@ -85,48 +77,46 @@ func (x *Config) GetUsernames() []string { return nil } -func (x *Config) GetShortId() []byte { +func (x *Config) GetAddresss() string { if x != nil { - return x.ShortId + return x.Addresss } - return nil + return "" } -func (x *Config) GetPublicKeySha256() string { +func (x *Config) GetPort() uint32 { if x != nil { - return x.PublicKeySha256 + return x.Port } - return "" + return 0 } -func (x *Config) GetAddresss() string { +func (x *Config) GetRsaPrivateKey() []byte { if x != nil { - return x.Addresss + return x.RsaPrivateKey } - return "" + return nil } -func (x *Config) GetPort() uint32 { +func (x *Config) GetRsaPublicKey() []byte { if x != nil { - return x.Port + return x.RsaPublicKey } - return 0 + return nil } var File_config_proto protoreflect.FileDescriptor const file_config_proto_rawDesc = "" + "\n" + - "\fconfig.proto\x12+xray.transport.internet.finalmask.minecraft\"\xdb\x01\n" + - "\x06Config\x12\x1b\n" + - "\tshort_ids\x18\x01 \x03(\fR\bshortIds\x12\x1f\n" + - "\vprivate_key\x18\x03 \x01(\tR\n" + - "privateKey\x12\x1c\n" + - "\tusernames\x18\x02 \x03(\tR\tusernames\x12\x19\n" + - "\bshort_id\x18\x04 \x01(\fR\ashortId\x12*\n" + - "\x11public_key_sha256\x18\x05 \x01(\tR\x0fpublicKeySha256\x12\x1a\n" + + "\fconfig.proto\x12+xray.transport.internet.finalmask.minecraft\"\xc0\x01\n" + + "\x06Config\x12\x1a\n" + + "\bpassword\x18\x01 \x01(\tR\bpassword\x12\x1c\n" + + "\tusernames\x18\x02 \x03(\tR\tusernames\x12\x1a\n" + "\baddresss\x18\x06 \x01(\tR\baddresss\x12\x12\n" + - "\x04port\x18\a \x01(\rR\x04portB\xa3\x01\n" + + "\x04port\x18\a \x01(\rR\x04port\x12&\n" + + "\x0frsa_private_key\x18\b \x01(\fR\rrsaPrivateKey\x12$\n" + + "\x0ersa_public_key\x18\t \x01(\fR\frsaPublicKeyB\xa3\x01\n" + "/com.xray.transport.internet.finalmask.minecraftP\x01Z@github.com/xtls/xray-core/transport/internet/finalmask/minecraft\xaa\x02+Xray.Transport.Internet.Finalmask.Minecraftb\x06proto3" var ( diff --git a/transport/internet/finalmask/minecraft/config.proto b/transport/internet/finalmask/minecraft/config.proto index 7b7f6c621749..beb580c613e7 100644 --- a/transport/internet/finalmask/minecraft/config.proto +++ b/transport/internet/finalmask/minecraft/config.proto @@ -8,12 +8,10 @@ option java_multiple_files = true; message Config { - repeated bytes short_ids = 1; - string private_key = 3; - + string password = 1; repeated string usernames = 2; - bytes short_id = 4; - string public_key_sha256 = 5; string addresss = 6; uint32 port = 7; -} \ No newline at end of file + bytes rsa_private_key = 8; + bytes rsa_public_key = 9; +} diff --git a/transport/internet/finalmask/minecraft/derivation.go b/transport/internet/finalmask/minecraft/derivation.go new file mode 100644 index 000000000000..bd1cbd595dfd --- /dev/null +++ b/transport/internet/finalmask/minecraft/derivation.go @@ -0,0 +1,103 @@ +package minecraft + +import ( + "crypto/rsa" + "crypto/sha256" + "fmt" + "math/big" +) + +type sha256Stream struct { + seed []byte + counter uint64 + buf []byte +} + +func newSHA256Stream(seed []byte) *sha256Stream { + return &sha256Stream{ + seed: seed, + } +} + +func (s *sha256Stream) Read(p []byte) (n int, err error) { + for len(p) > len(s.buf) { + h := sha256.New() + h.Write(s.seed) + h.Write([]byte(fmt.Sprintf("-%d", s.counter))) + s.counter++ + s.buf = append(s.buf, h.Sum(nil)...) + } + n = copy(p, s.buf) + s.buf = s.buf[n:] + return n, nil +} + +func derivePrime(stream *sha256Stream) *big.Int { + pBytes := make([]byte, 64) // 512 bits + _, _ = stream.Read(pBytes) + pBytes[0] |= 0xc0 // ensure it is big enough so p*q is 1024 bits + pBytes[63] |= 0x01 // ensure odd + + p := new(big.Int).SetBytes(pBytes) + for { + if p.ProbablyPrime(20) { + pMinus1 := new(big.Int).Sub(p, big.NewInt(1)) + e := big.NewInt(65537) + gcd := new(big.Int).GCD(nil, nil, pMinus1, e) + if gcd.Cmp(big.NewInt(1)) == 0 { + return p + } + } + p.Add(p, big.NewInt(2)) + } +} + +// DeriveRSAKey derives a 1024-bit RSA private key from a password. +func DeriveRSAKey(password string) (*rsa.PrivateKey, error) { + seed := []byte(password) + + pStream := newSHA256Stream(append(seed, []byte("-p-prime")...)) + qStream := newSHA256Stream(append(seed, []byte("-q-prime")...)) + + p := derivePrime(pStream) + q := derivePrime(qStream) + + // ensure p != q (if they are, let's search q further) + for p.Cmp(q) == 0 { + q.Add(q, big.NewInt(2)) + for { + if q.ProbablyPrime(20) { + qMinus1 := new(big.Int).Sub(q, big.NewInt(1)) + e := big.NewInt(65537) + gcd := new(big.Int).GCD(nil, nil, qMinus1, e) + if gcd.Cmp(big.NewInt(1)) == 0 { + break + } + } + q.Add(q, big.NewInt(2)) + } + } + + n := new(big.Int).Mul(p, q) + pMinus1 := new(big.Int).Sub(p, big.NewInt(1)) + qMinus1 := new(big.Int).Sub(q, big.NewInt(1)) + totient := new(big.Int).Mul(pMinus1, qMinus1) + + e := big.NewInt(65537) + d := new(big.Int).ModInverse(e, totient) + if d == nil { + return nil, fmt.Errorf("failed to compute mod inverse") + } + + priv := &rsa.PrivateKey{ + PublicKey: rsa.PublicKey{ + N: n, + E: 65537, + }, + D: d, + Primes: []*big.Int{p, q}, + } + priv.Precompute() + + return priv, nil +} diff --git a/transport/internet/finalmask/minecraft/derivation_test.go b/transport/internet/finalmask/minecraft/derivation_test.go new file mode 100644 index 000000000000..2d16129baa06 --- /dev/null +++ b/transport/internet/finalmask/minecraft/derivation_test.go @@ -0,0 +1,58 @@ +package minecraft + +import ( + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "testing" +) + +func TestDeriveRSAKey(t *testing.T) { + password := "my-very-secret-password-12345" + + key1, err := DeriveRSAKey(password) + if err != nil { + t.Fatalf("failed to derive key: %v", err) + } + + err = key1.Validate() + if err != nil { + t.Fatalf("key is not valid RSA key: %v", err) + } + + key2, err := DeriveRSAKey(password) + if err != nil { + t.Fatalf("failed to derive key second time: %v", err) + } + + // Verify determinism + if key1.D.Cmp(key2.D) != 0 || key1.N.Cmp(key2.N) != 0 { + t.Errorf("derived keys are not identical for the same password") + } + + // Verify different passwords yield different keys + keyDifferent, err := DeriveRSAKey(password + "-different") + if err != nil { + t.Fatalf("failed to derive different key: %v", err) + } + + if key1.D.Cmp(keyDifferent.D) == 0 || key1.N.Cmp(keyDifferent.N) == 0 { + t.Errorf("derived keys are identical for different passwords") + } +} + +func TestDeriveRSAKeyGoldenPrivateKey(t *testing.T) { + const password = "deterministic-rsa-key-golden" + const wantPKCS1DERHash = "3a8c4ad56a6fb42dab73c4d5fc3af754460a2db1441edc0970cbc7f4e0798d2f" + + key, err := DeriveRSAKey(password) + if err != nil { + t.Fatalf("failed to derive key: %v", err) + } + + gotHash := sha256.Sum256(x509.MarshalPKCS1PrivateKey(key)) + got := hex.EncodeToString(gotHash[:]) + if got != wantPKCS1DERHash { + t.Fatalf("derived private key changed\nwant sha256: %s\n got sha256: %s", wantPKCS1DERHash, got) + } +} diff --git a/transport/internet/finalmask/minecraft/handshake_test.go b/transport/internet/finalmask/minecraft/handshake_test.go new file mode 100644 index 000000000000..9756e7e562e8 --- /dev/null +++ b/transport/internet/finalmask/minecraft/handshake_test.go @@ -0,0 +1,156 @@ +package minecraft + +import ( + "bytes" + "crypto/x509" + "net" + "testing" + "time" +) + +func deriveTestRSAKey(t *testing.T, password string) ([]byte, []byte) { + t.Helper() + + key, err := DeriveRSAKey(password) + if err != nil { + t.Fatalf("failed to derive rsa key: %v", err) + } + + publicKey, err := x509.MarshalPKIXPublicKey(&key.PublicKey) + if err != nil { + t.Fatalf("failed to marshal public key: %v", err) + } + + return x509.MarshalPKCS1PrivateKey(key), publicKey +} + +func TestHandshakeSuccess(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + defer ln.Close() + + password := "super-secure-shared-key-12345" + usernames := []string{"test_user"} + privateKey, publicKey := deriveTestRSAKey(t, password) + + go func() { + rawConn, err := ln.Accept() + if err != nil { + return + } + defer rawConn.Close() + + server, err := wrapConnServer(rawConn, password, privateKey, publicKey) + if err != nil { + t.Errorf("failed to wrap server: %v", err) + return + } + + buf := make([]byte, 1024) + n, err := server.Read(buf) + if err != nil { + t.Errorf("server read error: %v", err) + return + } + + if !bytes.Equal(buf[:n], []byte("hello server")) { + t.Errorf("unexpected payload from client: %s", string(buf[:n])) + return + } + + _, err = server.Write([]byte("hello client")) + if err != nil { + t.Errorf("server write error: %v", err) + return + } + }() + + clientRaw, err := net.Dial("tcp", ln.Addr().String()) + if err != nil { + t.Fatalf("failed to dial: %v", err) + } + defer clientRaw.Close() + + client, err := newClientConn(clientRaw, usernames, password, publicKey, "localhost", uint16(ln.Addr().(*net.TCPAddr).Port)) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + + _, err = client.Write([]byte("hello server")) + if err != nil { + t.Fatalf("client write error: %v", err) + } + + buf := make([]byte, 1024) + n, err := client.Read(buf) + if err != nil { + t.Fatalf("client read error: %v", err) + } + + if !bytes.Equal(buf[:n], []byte("hello client")) { + t.Errorf("unexpected payload from server: %s", string(buf[:n])) + } +} + +func TestHandshakePasswordMismatch(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + defer ln.Close() + + clientPassword := "client-secret-123" + serverPassword := "server-secret-456" + usernames := []string{"test_user"} + serverPrivateKey, serverPublicKey := deriveTestRSAKey(t, serverPassword) + _, clientPublicKey := deriveTestRSAKey(t, clientPassword) + + go func() { + rawConn, err := ln.Accept() + if err != nil { + return + } + defer rawConn.Close() + + server, err := wrapConnServer(rawConn, serverPassword, serverPrivateKey, serverPublicKey) + if err != nil { + // Wrapping is synchronous and shouldn't fail initially simply because key derivation works with any string + t.Logf("wrapped server: %v", err) + } + + // When client sends data, handshake happens and should fail + buf := make([]byte, 1024) + _, err = server.Read(buf) + if err == nil { + t.Errorf("expected handshake to fail due to password mismatch, but it succeeded") + } else { + t.Logf("server read failed as expected: %v", err) + } + }() + + clientRaw, err := net.Dial("tcp", ln.Addr().String()) + if err != nil { + t.Fatalf("failed to dial: %v", err) + } + defer clientRaw.Close() + + client, err := newClientConn(clientRaw, usernames, clientPassword, clientPublicKey, "localhost", uint16(ln.Addr().(*net.TCPAddr).Port)) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + + // Wait briefly or just perform write/read + _ = clientRaw.SetDeadline(time.Now().Add(100 * time.Millisecond)) + + _, err = client.Write([]byte("hello server")) + if err == nil { + // Try to read too + buf := make([]byte, 1024) + _, _ = client.Read(buf) + } + + // Check if we lost connection or received error + t.Log("Handshake mismatch tested") +} diff --git a/transport/internet/finalmask/minecraft/server.go b/transport/internet/finalmask/minecraft/server.go index 40a2006faa32..25e2e776f87b 100644 --- a/transport/internet/finalmask/minecraft/server.go +++ b/transport/internet/finalmask/minecraft/server.go @@ -7,7 +7,6 @@ import ( "crypto/rsa" "crypto/subtle" "crypto/x509" - "encoding/pem" "fmt" "io" "net" @@ -33,7 +32,7 @@ type serverConn struct { state serverState handshakeLock sync.Mutex - shortIds [][]byte + password string rsaPrivateKey *rsa.PrivateKey rsaPublicKey []byte } @@ -204,18 +203,12 @@ func (c *serverConn) handshake() error { return fmt.Errorf("new crypto writer: %w", err) } - // verify short id + // verify password + receivedPassword := decryptedVerifyToken[4:] - shortId := decryptedVerifyToken[4:] - - var ok byte = 0 - for _, s := range c.shortIds { - ok |= byte(subtle.ConstantTimeCompare(s, shortId)) - } - - if ok == 0 { + if subtle.ConstantTimeCompare(receivedPassword, []byte(c.password)) != 1 { writeDisconnectPacket(c.writer, `{"type":"translatable","translate":"multiplayer.disconnect.authservers_down"}`) - return fmt.Errorf("bad short id") + return fmt.Errorf("bad password") } c.state = serverStateProxy @@ -269,35 +262,27 @@ func (c *serverConn) SetWriteDeadline(t time.Time) error { return c.c.SetWriteDeadline(t) } -func wrapConnServer(c net.Conn, shortIds [][]byte, privateKey string) (*serverConn, error) { - - s := &serverConn{ - reader: bufio.NewReader(c), - writer: c, - c: c, - state: serverStateHandshake, - shortIds: shortIds, +func wrapConnServer(c net.Conn, password string, rsaPrivateKeyDER []byte, rsaPublicKey []byte) (*serverConn, error) { + if len(rsaPrivateKeyDER) == 0 { + return nil, fmt.Errorf("empty rsa private key") } - - p, _ := pem.Decode([]byte(privateKey)) - if p == nil { - panic("minecraft finalmask: malformatted private key") + if len(rsaPublicKey) == 0 { + return nil, fmt.Errorf("empty rsa public key") } - k, err := x509.ParsePKCS8PrivateKey(p.Bytes) + rsaPrivateKey, err := x509.ParsePKCS1PrivateKey(rsaPrivateKeyDER) if err != nil { - panic("minecraft finalmask: malformatted private key: " + err.Error()) - } - - var ok bool - s.rsaPrivateKey, ok = k.(*rsa.PrivateKey) - if !ok { - panic("minecraft finalmask: rsa private key required") + return nil, fmt.Errorf("parse rsa private key: %w", err) } - s.rsaPublicKey, err = x509.MarshalPKIXPublicKey(&s.rsaPrivateKey.PublicKey) - if err != nil { - panic("minecraft finalmask: marshal public key: " + err.Error()) + s := &serverConn{ + reader: bufio.NewReader(c), + writer: c, + c: c, + state: serverStateHandshake, + password: password, + rsaPrivateKey: rsaPrivateKey, + rsaPublicKey: rsaPublicKey, } return s, nil From dc96b63407750c80b66b2db6a55c8294fc236563 Mon Sep 17 00:00:00 2001 From: Exclude0122 <173652916+Exclude0122@users.noreply.github.com> Date: Fri, 29 May 2026 23:51:05 -0400 Subject: [PATCH 06/13] rename address to hostname & remove port from config --- infra/conf/transport_finalmask.go | 14 ++------ .../internet/finalmask/minecraft/client.go | 25 ++++++++++---- .../internet/finalmask/minecraft/config.go | 2 +- .../internet/finalmask/minecraft/config.pb.go | 34 +++++++------------ .../internet/finalmask/minecraft/config.proto | 3 +- .../finalmask/minecraft/handshake_test.go | 4 +-- 6 files changed, 37 insertions(+), 45 deletions(-) diff --git a/infra/conf/transport_finalmask.go b/infra/conf/transport_finalmask.go index 8acfef3341d3..1b87431cd0c6 100644 --- a/infra/conf/transport_finalmask.go +++ b/infra/conf/transport_finalmask.go @@ -719,8 +719,7 @@ func (c *Xdns) Build() (proto.Message, error) { } type Minecraft struct { - Address string `json:"address"` - Port uint16 `json:"port"` + Hostname string `json:"hostname"` Usernames []string `json:"usernames"` Password string `json:"password"` } @@ -731,14 +730,6 @@ func (c *Minecraft) Build() (proto.Message, error) { c.Usernames = []string{"Dream"} } - if c.Port == 0 { - c.Port = 25565 - } - - if c.Address == "" { - c.Address = "localhost" - } - if c.Password == "" { return nil, fmt.Errorf("empty password") } @@ -756,8 +747,7 @@ func (c *Minecraft) Build() (proto.Message, error) { return &minecraft.Config{ Password: c.Password, Usernames: c.Usernames, - Addresss: c.Address, - Port: uint32(c.Port), + Hostname: c.Hostname, RsaPrivateKey: x509.MarshalPKCS1PrivateKey(rsaPrivateKey), RsaPublicKey: rsaPublicKey, }, nil diff --git a/transport/internet/finalmask/minecraft/client.go b/transport/internet/finalmask/minecraft/client.go index 77fe76cb9f55..a37f49183ad8 100644 --- a/transport/internet/finalmask/minecraft/client.go +++ b/transport/internet/finalmask/minecraft/client.go @@ -11,6 +11,7 @@ import ( "io" "math/big" "net" + "strconv" "sync" "time" ) @@ -26,8 +27,7 @@ type clientConn struct { usernames []string password string rsaPublicKey []byte - address string - port uint16 + hostname string } type clientState int @@ -37,7 +37,7 @@ var ( clientStateProxy clientState = 2 ) -func newClientConn(c net.Conn, usernames []string, password string, rsaPublicKey []byte, address string, port uint16) (*clientConn, error) { +func newClientConn(c net.Conn, usernames []string, password string, rsaPublicKey []byte, hostname string) (*clientConn, error) { if len(rsaPublicKey) == 0 { return nil, fmt.Errorf("empty rsa public key") } @@ -51,8 +51,7 @@ func newClientConn(c net.Conn, usernames []string, password string, rsaPublicKey usernames: usernames, password: password, rsaPublicKey: rsaPublicKey, - address: address, - port: port, + hostname: hostname, }, nil } @@ -73,11 +72,23 @@ func (c *clientConn) handshake() error { var ( protocolVersion Varint = Varint(775) - serverAddress String = String(c.address) - serverPort UnsignedShort = UnsignedShort(c.port) + serverAddress String = String(c.hostname) + serverPort UnsignedShort = UnsignedShort(25565) nextState Varint = Varint(2) ) + host, portString, err := net.SplitHostPort(c.c.RemoteAddr().String()) + if err == nil { + port, err := strconv.Atoi(portString) + if err == nil { + serverPort = UnsignedShort(port) + } + + if serverAddress == "" { + serverAddress = String(host) + } + } + err = writePacket(c.writer, 0x00, &protocolVersion, &serverAddress, &serverPort, &nextState) if err != nil { return fmt.Errorf("write handshake packet: %w", err) diff --git a/transport/internet/finalmask/minecraft/config.go b/transport/internet/finalmask/minecraft/config.go index a904d4079113..d7b5563e178a 100644 --- a/transport/internet/finalmask/minecraft/config.go +++ b/transport/internet/finalmask/minecraft/config.go @@ -9,7 +9,7 @@ func (c *Config) TCP() { } func (c *Config) WrapConnClient(conn net.Conn) (net.Conn, error) { - cc, err := newClientConn(conn, c.Usernames, c.Password, c.RsaPublicKey, c.Addresss, uint16(c.Port)) + cc, err := newClientConn(conn, c.Usernames, c.Password, c.RsaPublicKey, c.Hostname) if err != nil { return nil, fmt.Errorf("minecraft finalmask: %w", err) } diff --git a/transport/internet/finalmask/minecraft/config.pb.go b/transport/internet/finalmask/minecraft/config.pb.go index dac853ad889a..2d6e02df8f73 100644 --- a/transport/internet/finalmask/minecraft/config.pb.go +++ b/transport/internet/finalmask/minecraft/config.pb.go @@ -25,10 +25,9 @@ type Config struct { state protoimpl.MessageState `protogen:"open.v1"` Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` Usernames []string `protobuf:"bytes,2,rep,name=usernames,proto3" json:"usernames,omitempty"` - Addresss string `protobuf:"bytes,6,opt,name=addresss,proto3" json:"addresss,omitempty"` - Port uint32 `protobuf:"varint,7,opt,name=port,proto3" json:"port,omitempty"` RsaPrivateKey []byte `protobuf:"bytes,8,opt,name=rsa_private_key,json=rsaPrivateKey,proto3" json:"rsa_private_key,omitempty"` RsaPublicKey []byte `protobuf:"bytes,9,opt,name=rsa_public_key,json=rsaPublicKey,proto3" json:"rsa_public_key,omitempty"` + Hostname string `protobuf:"bytes,10,opt,name=hostname,proto3" json:"hostname,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -77,20 +76,6 @@ func (x *Config) GetUsernames() []string { return nil } -func (x *Config) GetAddresss() string { - if x != nil { - return x.Addresss - } - return "" -} - -func (x *Config) GetPort() uint32 { - if x != nil { - return x.Port - } - return 0 -} - func (x *Config) GetRsaPrivateKey() []byte { if x != nil { return x.RsaPrivateKey @@ -105,18 +90,25 @@ func (x *Config) GetRsaPublicKey() []byte { return nil } +func (x *Config) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + var File_config_proto protoreflect.FileDescriptor const file_config_proto_rawDesc = "" + "\n" + - "\fconfig.proto\x12+xray.transport.internet.finalmask.minecraft\"\xc0\x01\n" + + "\fconfig.proto\x12+xray.transport.internet.finalmask.minecraft\"\xac\x01\n" + "\x06Config\x12\x1a\n" + "\bpassword\x18\x01 \x01(\tR\bpassword\x12\x1c\n" + - "\tusernames\x18\x02 \x03(\tR\tusernames\x12\x1a\n" + - "\baddresss\x18\x06 \x01(\tR\baddresss\x12\x12\n" + - "\x04port\x18\a \x01(\rR\x04port\x12&\n" + + "\tusernames\x18\x02 \x03(\tR\tusernames\x12&\n" + "\x0frsa_private_key\x18\b \x01(\fR\rrsaPrivateKey\x12$\n" + - "\x0ersa_public_key\x18\t \x01(\fR\frsaPublicKeyB\xa3\x01\n" + + "\x0ersa_public_key\x18\t \x01(\fR\frsaPublicKey\x12\x1a\n" + + "\bhostname\x18\n" + + " \x01(\tR\bhostnameB\xa3\x01\n" + "/com.xray.transport.internet.finalmask.minecraftP\x01Z@github.com/xtls/xray-core/transport/internet/finalmask/minecraft\xaa\x02+Xray.Transport.Internet.Finalmask.Minecraftb\x06proto3" var ( diff --git a/transport/internet/finalmask/minecraft/config.proto b/transport/internet/finalmask/minecraft/config.proto index beb580c613e7..250afc2a5329 100644 --- a/transport/internet/finalmask/minecraft/config.proto +++ b/transport/internet/finalmask/minecraft/config.proto @@ -10,8 +10,7 @@ option java_multiple_files = true; message Config { string password = 1; repeated string usernames = 2; - string addresss = 6; - uint32 port = 7; bytes rsa_private_key = 8; bytes rsa_public_key = 9; + string hostname = 10; } diff --git a/transport/internet/finalmask/minecraft/handshake_test.go b/transport/internet/finalmask/minecraft/handshake_test.go index 9756e7e562e8..82ced87f66bd 100644 --- a/transport/internet/finalmask/minecraft/handshake_test.go +++ b/transport/internet/finalmask/minecraft/handshake_test.go @@ -73,7 +73,7 @@ func TestHandshakeSuccess(t *testing.T) { } defer clientRaw.Close() - client, err := newClientConn(clientRaw, usernames, password, publicKey, "localhost", uint16(ln.Addr().(*net.TCPAddr).Port)) + client, err := newClientConn(clientRaw, usernames, password, publicKey, "localhost") if err != nil { t.Fatalf("failed to create client: %v", err) } @@ -136,7 +136,7 @@ func TestHandshakePasswordMismatch(t *testing.T) { } defer clientRaw.Close() - client, err := newClientConn(clientRaw, usernames, clientPassword, clientPublicKey, "localhost", uint16(ln.Addr().(*net.TCPAddr).Port)) + client, err := newClientConn(clientRaw, usernames, clientPassword, clientPublicKey, "localhost") if err != nil { t.Fatalf("failed to create client: %v", err) } From 83c5701a54cd8a56a4f14984198f1aae14850dc5 Mon Sep 17 00:00:00 2001 From: Exclude0122 <173652916+Exclude0122@users.noreply.github.com> Date: Sat, 30 May 2026 00:01:06 -0400 Subject: [PATCH 07/13] remove minecraft command --- main/commands/all/commands.go | 1 - main/commands/all/minecraft_rsa.go | 60 ------------------------------ 2 files changed, 61 deletions(-) delete mode 100644 main/commands/all/minecraft_rsa.go diff --git a/main/commands/all/commands.go b/main/commands/all/commands.go index 70c4edd8dd4b..fba3a4b8bb43 100644 --- a/main/commands/all/commands.go +++ b/main/commands/all/commands.go @@ -19,6 +19,5 @@ func init() { cmdMLDSA65, cmdMLKEM768, cmdVLESSEnc, - cmdMinecraftRSA, ) } diff --git a/main/commands/all/minecraft_rsa.go b/main/commands/all/minecraft_rsa.go deleted file mode 100644 index 16958a9dbfd9..000000000000 --- a/main/commands/all/minecraft_rsa.go +++ /dev/null @@ -1,60 +0,0 @@ -package all - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/sha256" - "crypto/x509" - "encoding/hex" - "encoding/pem" - "fmt" - "strings" - - "github.com/xtls/xray-core/main/commands/base" -) - -var cmdMinecraftRSA = &base.Command{ - UsageLine: `{{.Exec}} minecraft`, - Short: `Generate RSA private key used in Minecraft finalmask`, - Long: ` -Generate RSA private key used in Minecraft finalmask. - -Output private key in PEM format and the SHA256 of the public key in PKIX format. -`, -} - -func init() { - cmdMinecraftRSA.Run = executeMinecraftRSA // break init loop -} - -func executeMinecraftRSA(cmd *base.Command, args []string) { - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - panic(err) - } - - pkcs8, err := x509.MarshalPKCS8PrivateKey(privateKey) - if err != nil { - panic(err) - } - - pkcs8pem := pem.EncodeToMemory(&pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: pkcs8, - }) - - fmt.Println("Private Key:") - fmt.Println(strings.ReplaceAll(string(pkcs8pem), "\n", "\\n")) - - rsaPublicKey, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) - if err != nil { - panic(err) - } - - h := sha256.New() - h.Write(rsaPublicKey) - hash := h.Sum([]byte{}) - - fmt.Println("Public Key SHA256:") - fmt.Println(hex.EncodeToString(hash)) -} From ac46cc9131910fa5f769b3261ef114ab4194f94f Mon Sep 17 00:00:00 2001 From: Exclude0122 <173652916+Exclude0122@users.noreply.github.com> Date: Fri, 5 Jun 2026 23:42:16 -0400 Subject: [PATCH 08/13] generate .pb.go with protoc v6.33.5 --- transport/internet/finalmask/minecraft/config.pb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport/internet/finalmask/minecraft/config.pb.go b/transport/internet/finalmask/minecraft/config.pb.go index 2d6e02df8f73..d6f2a1843470 100644 --- a/transport/internet/finalmask/minecraft/config.pb.go +++ b/transport/internet/finalmask/minecraft/config.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v3.19.6 +// protoc v6.33.5 // source: config.proto package minecraft From 8bad5db8ebd8ea77352e2ebd59d0a75bbb744e4b Mon Sep 17 00:00:00 2001 From: Exclude0122 <173652916+Exclude0122@users.noreply.github.com> Date: Fri, 5 Jun 2026 23:50:19 -0400 Subject: [PATCH 09/13] fmt --- infra/conf/transport_finalmask.go | 1 - transport/internet/finalmask/minecraft/protocol.go | 3 --- transport/internet/finalmask/minecraft/server.go | 2 +- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/infra/conf/transport_finalmask.go b/infra/conf/transport_finalmask.go index 1b87431cd0c6..8a8d92d93cd9 100644 --- a/infra/conf/transport_finalmask.go +++ b/infra/conf/transport_finalmask.go @@ -725,7 +725,6 @@ type Minecraft struct { } func (c *Minecraft) Build() (proto.Message, error) { - if len(c.Usernames) == 0 { c.Usernames = []string{"Dream"} } diff --git a/transport/internet/finalmask/minecraft/protocol.go b/transport/internet/finalmask/minecraft/protocol.go index bd3bb6df9afc..8c019dace56d 100644 --- a/transport/internet/finalmask/minecraft/protocol.go +++ b/transport/internet/finalmask/minecraft/protocol.go @@ -18,7 +18,6 @@ type mcPacket struct { } func readPacket(b io.Reader) (*mcPacket, error) { - var packetLength Varint err := packetLength.readFrom(b) if err != nil { @@ -49,7 +48,6 @@ func readPacket(b io.Reader) (*mcPacket, error) { } func (p *mcPacket) readFields(fields ...field) error { - r := bytes.NewReader(p.data) for _, field := range fields { @@ -251,7 +249,6 @@ func (v *UUID) writeTo(w io.Writer) error { type Bytes []byte func (v *Bytes) readFrom(r io.Reader) error { - var length Varint err := length.readFrom(r) if err != nil { diff --git a/transport/internet/finalmask/minecraft/server.go b/transport/internet/finalmask/minecraft/server.go index 25e2e776f87b..486bf30a6276 100644 --- a/transport/internet/finalmask/minecraft/server.go +++ b/transport/internet/finalmask/minecraft/server.go @@ -90,7 +90,7 @@ func (c *serverConn) handshake() error { switch pkt.packetID { case 0: // Status Request - err = writePacket(c.writer, 0, new(String((statusResponse)))) + err = writePacket(c.writer, 0, new(String(statusResponse))) if err != nil { return fmt.Errorf("write status response: %w", err) } From 7e11faf1a49d7fefe834a1898cd190f2342131d9 Mon Sep 17 00:00:00 2001 From: Exclude0122 <173652916+Exclude0122@users.noreply.github.com> Date: Mon, 8 Jun 2026 06:23:17 -0400 Subject: [PATCH 10/13] replace cfb8 implementation --- .../internet/finalmask/minecraft/cfb8.go | 166 +++++++++++++----- .../internet/finalmask/minecraft/stream.go | 4 +- 2 files changed, 122 insertions(+), 48 deletions(-) diff --git a/transport/internet/finalmask/minecraft/cfb8.go b/transport/internet/finalmask/minecraft/cfb8.go index d21fc94643fc..e8b2aa254194 100644 --- a/transport/internet/finalmask/minecraft/cfb8.go +++ b/transport/internet/finalmask/minecraft/cfb8.go @@ -1,64 +1,138 @@ package minecraft -// Source - https://stackoverflow.com/a/37234233 -// Posted by kostya, modified by community. See post 'Timeline' for change history -// Retrieved 2026-05-28, License - CC BY-SA 4.0 +// Copied from https://github.com/Tnze/go-mc/blob/539b4a3a7f030332eb58b8a946116ae7907630d2/net/CFB8/cfb8.go -import "crypto/cipher" +import ( + "crypto/cipher" + "crypto/subtle" + "unsafe" +) -// CFB stream with 8 bit segment size -// See http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf type cfb8 struct { - b cipher.Block + c cipher.Block blockSize int - in []byte - out []byte - - decrypt bool + ivPos int + iv []byte + de bool } -func (x *cfb8) XORKeyStream(dst, src []byte) { - for i := range src { - x.b.Encrypt(x.out, x.in) - copy(x.in[:x.blockSize-1], x.in[1:]) - if x.decrypt { - x.in[x.blockSize-1] = src[i] - } - dst[i] = src[i] ^ x.out[0] - if !x.decrypt { - x.in[x.blockSize-1] = dst[i] - } - } +func newCFB8Decrypt(c cipher.Block, iv []byte) *cfb8 { + return newCFB8(c, iv, true) } -// NewCFB8Encrypter returns a Stream which encrypts with cipher feedback mode -// (segment size = 8), using the given Block. The iv must be the same length as -// the Block's block size. -func newCFB8Encrypter(block cipher.Block, iv []byte) cipher.Stream { - return newCFB8(block, iv, false) +func newCFB8Encrypt(c cipher.Block, iv []byte) *cfb8 { + return newCFB8(c, iv, false) } -// NewCFB8Decrypter returns a Stream which decrypts with cipher feedback mode -// (segment size = 8), using the given Block. The iv must be the same length as -// the Block's block size. -func newCFB8Decrypter(block cipher.Block, iv []byte) cipher.Stream { - return newCFB8(block, iv, true) +func newCFB8(c cipher.Block, iv []byte, de bool) *cfb8 { + cp := make([]byte, len(iv)*3) + copy(cp, iv) + return &cfb8{ + c: c, + blockSize: c.BlockSize(), + iv: cp, + de: de, + } } -func newCFB8(block cipher.Block, iv []byte, decrypt bool) cipher.Stream { - blockSize := block.BlockSize() - if len(iv) != blockSize { - // stack trace will indicate whether it was de or encryption - panic("cipher.newCFB: IV length must equal block size") +func (cf *cfb8) XORKeyStream(dst, src []byte) { + if len(src) == 0 { + return } - x := &cfb8{ - b: block, - blockSize: blockSize, - out: make([]byte, blockSize), - in: make([]byte, blockSize), - decrypt: decrypt, + if len(dst) < len(src) { + panic("cfb8: output smaller than input") } - copy(x.in, iv) - return x + // If dst and src does not overlap in first block size, + // and the length of src is greater than 2*blockSize, + // we can use an optimized implementation. + if len(src) > cf.blockSize<<1 && + (uintptr(unsafe.Pointer(&dst[0]))+uintptr(cf.blockSize) <= uintptr(unsafe.Pointer(&src[0])) || + uintptr(unsafe.Pointer(&src[0]))+uintptr(len(src)) <= uintptr(unsafe.Pointer(&dst[0]))) { + // encrypt/decrypt first blockSize bytes + // After this, the IV will come to the same as + // the last blockSize of ciphertext, so + // we can reuse them without copy. + cf.xorKeyStream(dst, src[:cf.blockSize]) + var ciphertext []byte + if cf.de { + ciphertext = src + } else { + ciphertext = dst + } + dst = dst[cf.blockSize:] + src = src[cf.blockSize:] + iv := cf.iv + _ = iv[0] // bounds check hint to compiler; see golang.org/issue/14808 + var ( + i int + val byte + ) + dst = dst[:len(src)] + if cf.de && // and requires to be non-overlapping at all + uintptr(unsafe.Pointer(&dst[0])) <= uintptr(unsafe.Pointer(&src[len(src)-1])) && + uintptr(unsafe.Pointer(&src[0])) <= uintptr(unsafe.Pointer(&dst[len(dst)-1])) { + for i = 0; i < len(src)-cf.blockSize; i += 1 { + cf.c.Encrypt(dst[i:], ciphertext[i:]) + } + subtle.XORBytes(dst, src[:i], dst) + for ; i < len(src); i += 1 { + cf.c.Encrypt(iv, ciphertext[i:]) + dst[i] = src[i] ^ iv[0] + } + } else { + _ = ciphertext[len(src)] + for i, val = range src { + cf.c.Encrypt(iv, ciphertext[i:]) + dst[i] = val ^ iv[0] + } + // for-range does not increase i in the last loop, + // compared to the classic for clause + i += 1 + } + // copy the current IV for next operation + copy(iv, ciphertext[i:i+cf.blockSize]) + cf.ivPos = 0 + return + } + + cf.xorKeyStream(dst, src) +} + +func (cf *cfb8) xorKeyStream(dst, src []byte) { + dst = dst[:len(src)] // remove bounds check in loop + for i, val := range src { + posPlusBlockSize := cf.ivPos + cf.blockSize + // fast mod; 2*blockSize must be a non-negative integer power of 2 + tempPos := posPlusBlockSize & (cf.blockSize<<1 - 1) + // reuse space to store encrypted block + cf.c.Encrypt(cf.iv[tempPos:], cf.iv[cf.ivPos:]) + // Only the first byte of the encrypted block is used + // for encryption/decryption, other bytes are ignored. + val ^= cf.iv[tempPos] + + if cf.ivPos == cf.blockSize<<1 { + // bound reached; move to next round for next operation + // copy next block to the start of the ring buffer + copy(cf.iv, cf.iv[cf.ivPos+1:]) + // insert the encrypted byte to the end of IV + if cf.de { + cf.iv[cf.blockSize-1] = src[i] + } else { + cf.iv[cf.blockSize-1] = val + } + cf.ivPos = 0 + } else { + // insert the encrypted byte to the end of IV + if cf.de { + cf.iv[posPlusBlockSize] = src[i] + } else { + cf.iv[posPlusBlockSize] = val + } + // move to next block + cf.ivPos += 1 + } + + dst[i] = val + } } diff --git a/transport/internet/finalmask/minecraft/stream.go b/transport/internet/finalmask/minecraft/stream.go index bd41dced9a4a..055e58b93579 100644 --- a/transport/internet/finalmask/minecraft/stream.go +++ b/transport/internet/finalmask/minecraft/stream.go @@ -20,7 +20,7 @@ func newCryptoReader(r io.Reader, sharedSecret []byte) (*cryptoStream, error) { return nil, fmt.Errorf("new aes cipher: %w", err) } - stream := newCFB8Decrypter(blockCipher, sharedSecret) + stream := newCFB8Decrypt(blockCipher, sharedSecret) return &cryptoStream{stream: stream, r: r}, nil } @@ -46,7 +46,7 @@ func newCryptoWriter(w io.Writer, sharedSecret []byte) (*cryptoStream, error) { return nil, fmt.Errorf("new aes cipher: %w", err) } - stream := newCFB8Encrypter(blockCipher, sharedSecret) + stream := newCFB8Encrypt(blockCipher, sharedSecret) return &cryptoStream{stream: stream, w: w}, nil } From 59168a82df7b33d6b7a20b9541ba3199108df06c Mon Sep 17 00:00:00 2001 From: Exclude0122 <173652916+Exclude0122@users.noreply.github.com> Date: Fri, 10 Jul 2026 01:16:10 -0400 Subject: [PATCH 11/13] add to tcpmaskLoader --- infra/conf/transport_finalmask.go | 1 + 1 file changed, 1 insertion(+) diff --git a/infra/conf/transport_finalmask.go b/infra/conf/transport_finalmask.go index 8a8d92d93cd9..501aa850a174 100644 --- a/infra/conf/transport_finalmask.go +++ b/infra/conf/transport_finalmask.go @@ -70,6 +70,7 @@ var ( "header-custom": func() interface{} { return new(HeaderCustomTCP) }, "fragment": func() interface{} { return new(FragmentMask) }, "sudoku": func() interface{} { return new(Sudoku) }, + "xmc": func() interface{} { return new(Minecraft) }, }, "type", "settings") udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{ From 811fcbfdc5f1a5b40bc4094700aec65c2cfef873 Mon Sep 17 00:00:00 2001 From: Exclude0122 <173652916+Exclude0122@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:39:03 -0400 Subject: [PATCH 12/13] rename xmc --- infra/conf/transport_finalmask.go | 12 ++-- .../finalmask/{minecraft => xmc}/cfb8.go | 2 +- .../finalmask/{minecraft => xmc}/client.go | 2 +- .../finalmask/{minecraft => xmc}/config.go | 2 +- .../finalmask/{minecraft => xmc}/config.pb.go | 60 +++++++++---------- .../finalmask/{minecraft => xmc}/config.proto | 8 +-- .../{minecraft => xmc}/derivation.go | 2 +- .../{minecraft => xmc}/derivation_test.go | 2 +- .../{minecraft => xmc}/handshake_test.go | 2 +- .../finalmask/{minecraft => xmc}/protocol.go | 2 +- .../finalmask/{minecraft => xmc}/server.go | 2 +- .../finalmask/{minecraft => xmc}/stream.go | 2 +- 12 files changed, 49 insertions(+), 49 deletions(-) rename transport/internet/finalmask/{minecraft => xmc}/cfb8.go (99%) rename transport/internet/finalmask/{minecraft => xmc}/client.go (99%) rename transport/internet/finalmask/{minecraft => xmc}/config.go (96%) rename transport/internet/finalmask/{minecraft => xmc}/config.pb.go (56%) rename transport/internet/finalmask/{minecraft => xmc}/config.proto (56%) rename transport/internet/finalmask/{minecraft => xmc}/derivation.go (99%) rename transport/internet/finalmask/{minecraft => xmc}/derivation_test.go (98%) rename transport/internet/finalmask/{minecraft => xmc}/handshake_test.go (99%) rename transport/internet/finalmask/{minecraft => xmc}/protocol.go (99%) rename transport/internet/finalmask/{minecraft => xmc}/server.go (99%) rename transport/internet/finalmask/{minecraft => xmc}/stream.go (98%) diff --git a/infra/conf/transport_finalmask.go b/infra/conf/transport_finalmask.go index 501aa850a174..24c3f0afae8f 100644 --- a/infra/conf/transport_finalmask.go +++ b/infra/conf/transport_finalmask.go @@ -15,7 +15,6 @@ import ( "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/transport/internet/finalmask/fragment" "github.com/xtls/xray-core/transport/internet/finalmask/header/custom" - "github.com/xtls/xray-core/transport/internet/finalmask/minecraft" "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm" "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/header" "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/original" @@ -25,6 +24,7 @@ import ( "github.com/xtls/xray-core/transport/internet/finalmask/sudoku" "github.com/xtls/xray-core/transport/internet/finalmask/xdns" "github.com/xtls/xray-core/transport/internet/finalmask/xicmp" + "github.com/xtls/xray-core/transport/internet/finalmask/xmc" "github.com/xtls/xray-core/transport/internet/tls" "google.golang.org/protobuf/proto" ) @@ -70,7 +70,7 @@ var ( "header-custom": func() interface{} { return new(HeaderCustomTCP) }, "fragment": func() interface{} { return new(FragmentMask) }, "sudoku": func() interface{} { return new(Sudoku) }, - "xmc": func() interface{} { return new(Minecraft) }, + "xmc": func() interface{} { return new(XMC) }, }, "type", "settings") udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{ @@ -719,13 +719,13 @@ func (c *Xdns) Build() (proto.Message, error) { }, nil } -type Minecraft struct { +type XMC struct { Hostname string `json:"hostname"` Usernames []string `json:"usernames"` Password string `json:"password"` } -func (c *Minecraft) Build() (proto.Message, error) { +func (c *XMC) Build() (proto.Message, error) { if len(c.Usernames) == 0 { c.Usernames = []string{"Dream"} } @@ -734,7 +734,7 @@ func (c *Minecraft) Build() (proto.Message, error) { return nil, fmt.Errorf("empty password") } - rsaPrivateKey, err := minecraft.DeriveRSAKey(c.Password) + rsaPrivateKey, err := xmc.DeriveRSAKey(c.Password) if err != nil { return nil, fmt.Errorf("derive minecraft rsa key: %w", err) } @@ -744,7 +744,7 @@ func (c *Minecraft) Build() (proto.Message, error) { return nil, fmt.Errorf("marshal minecraft rsa public key: %w", err) } - return &minecraft.Config{ + return &xmc.Config{ Password: c.Password, Usernames: c.Usernames, Hostname: c.Hostname, diff --git a/transport/internet/finalmask/minecraft/cfb8.go b/transport/internet/finalmask/xmc/cfb8.go similarity index 99% rename from transport/internet/finalmask/minecraft/cfb8.go rename to transport/internet/finalmask/xmc/cfb8.go index e8b2aa254194..ed9cc1c53685 100644 --- a/transport/internet/finalmask/minecraft/cfb8.go +++ b/transport/internet/finalmask/xmc/cfb8.go @@ -1,4 +1,4 @@ -package minecraft +package xmc // Copied from https://github.com/Tnze/go-mc/blob/539b4a3a7f030332eb58b8a946116ae7907630d2/net/CFB8/cfb8.go diff --git a/transport/internet/finalmask/minecraft/client.go b/transport/internet/finalmask/xmc/client.go similarity index 99% rename from transport/internet/finalmask/minecraft/client.go rename to transport/internet/finalmask/xmc/client.go index a37f49183ad8..c0d1ee2b48c4 100644 --- a/transport/internet/finalmask/minecraft/client.go +++ b/transport/internet/finalmask/xmc/client.go @@ -1,4 +1,4 @@ -package minecraft +package xmc import ( "bufio" diff --git a/transport/internet/finalmask/minecraft/config.go b/transport/internet/finalmask/xmc/config.go similarity index 96% rename from transport/internet/finalmask/minecraft/config.go rename to transport/internet/finalmask/xmc/config.go index d7b5563e178a..d0f414a7c456 100644 --- a/transport/internet/finalmask/minecraft/config.go +++ b/transport/internet/finalmask/xmc/config.go @@ -1,4 +1,4 @@ -package minecraft +package xmc import ( "fmt" diff --git a/transport/internet/finalmask/minecraft/config.pb.go b/transport/internet/finalmask/xmc/config.pb.go similarity index 56% rename from transport/internet/finalmask/minecraft/config.pb.go rename to transport/internet/finalmask/xmc/config.pb.go index d6f2a1843470..f4c983b849c8 100644 --- a/transport/internet/finalmask/minecraft/config.pb.go +++ b/transport/internet/finalmask/xmc/config.pb.go @@ -2,9 +2,9 @@ // versions: // protoc-gen-go v1.36.11 // protoc v6.33.5 -// source: config.proto +// source: transport/internet/finalmask/xmc/config.proto -package minecraft +package xmc import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" @@ -34,7 +34,7 @@ type Config struct { func (x *Config) Reset() { *x = Config{} - mi := &file_config_proto_msgTypes[0] + mi := &file_transport_internet_finalmask_xmc_config_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -46,7 +46,7 @@ func (x *Config) String() string { func (*Config) ProtoMessage() {} func (x *Config) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[0] + mi := &file_transport_internet_finalmask_xmc_config_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -59,7 +59,7 @@ func (x *Config) ProtoReflect() protoreflect.Message { // Deprecated: Use Config.ProtoReflect.Descriptor instead. func (*Config) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{0} + return file_transport_internet_finalmask_xmc_config_proto_rawDescGZIP(), []int{0} } func (x *Config) GetPassword() string { @@ -97,37 +97,37 @@ func (x *Config) GetHostname() string { return "" } -var File_config_proto protoreflect.FileDescriptor +var File_transport_internet_finalmask_xmc_config_proto protoreflect.FileDescriptor -const file_config_proto_rawDesc = "" + +const file_transport_internet_finalmask_xmc_config_proto_rawDesc = "" + "\n" + - "\fconfig.proto\x12+xray.transport.internet.finalmask.minecraft\"\xac\x01\n" + + "-transport/internet/finalmask/xmc/config.proto\x12%xray.transport.internet.finalmask.xmc\"\xac\x01\n" + "\x06Config\x12\x1a\n" + "\bpassword\x18\x01 \x01(\tR\bpassword\x12\x1c\n" + "\tusernames\x18\x02 \x03(\tR\tusernames\x12&\n" + "\x0frsa_private_key\x18\b \x01(\fR\rrsaPrivateKey\x12$\n" + "\x0ersa_public_key\x18\t \x01(\fR\frsaPublicKey\x12\x1a\n" + "\bhostname\x18\n" + - " \x01(\tR\bhostnameB\xa3\x01\n" + - "/com.xray.transport.internet.finalmask.minecraftP\x01Z@github.com/xtls/xray-core/transport/internet/finalmask/minecraft\xaa\x02+Xray.Transport.Internet.Finalmask.Minecraftb\x06proto3" + " \x01(\tR\bhostnameB\x91\x01\n" + + ")com.xray.transport.internet.finalmask.xmcP\x01Z:github.com/xtls/xray-core/transport/internet/finalmask/xmc\xaa\x02%Xray.Transport.Internet.Finalmask.XMCb\x06proto3" var ( - file_config_proto_rawDescOnce sync.Once - file_config_proto_rawDescData []byte + file_transport_internet_finalmask_xmc_config_proto_rawDescOnce sync.Once + file_transport_internet_finalmask_xmc_config_proto_rawDescData []byte ) -func file_config_proto_rawDescGZIP() []byte { - file_config_proto_rawDescOnce.Do(func() { - file_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_config_proto_rawDesc), len(file_config_proto_rawDesc))) +func file_transport_internet_finalmask_xmc_config_proto_rawDescGZIP() []byte { + file_transport_internet_finalmask_xmc_config_proto_rawDescOnce.Do(func() { + file_transport_internet_finalmask_xmc_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_xmc_config_proto_rawDesc), len(file_transport_internet_finalmask_xmc_config_proto_rawDesc))) }) - return file_config_proto_rawDescData + return file_transport_internet_finalmask_xmc_config_proto_rawDescData } -var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_config_proto_goTypes = []any{ - (*Config)(nil), // 0: xray.transport.internet.finalmask.minecraft.Config +var file_transport_internet_finalmask_xmc_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_finalmask_xmc_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.finalmask.xmc.Config } -var file_config_proto_depIdxs = []int32{ +var file_transport_internet_finalmask_xmc_config_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type 0, // [0:0] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name @@ -135,26 +135,26 @@ var file_config_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for field type_name } -func init() { file_config_proto_init() } -func file_config_proto_init() { - if File_config_proto != nil { +func init() { file_transport_internet_finalmask_xmc_config_proto_init() } +func file_transport_internet_finalmask_xmc_config_proto_init() { + if File_transport_internet_finalmask_xmc_config_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_config_proto_rawDesc), len(file_config_proto_rawDesc)), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_xmc_config_proto_rawDesc), len(file_transport_internet_finalmask_xmc_config_proto_rawDesc)), NumEnums: 0, NumMessages: 1, NumExtensions: 0, NumServices: 0, }, - GoTypes: file_config_proto_goTypes, - DependencyIndexes: file_config_proto_depIdxs, - MessageInfos: file_config_proto_msgTypes, + GoTypes: file_transport_internet_finalmask_xmc_config_proto_goTypes, + DependencyIndexes: file_transport_internet_finalmask_xmc_config_proto_depIdxs, + MessageInfos: file_transport_internet_finalmask_xmc_config_proto_msgTypes, }.Build() - File_config_proto = out.File - file_config_proto_goTypes = nil - file_config_proto_depIdxs = nil + File_transport_internet_finalmask_xmc_config_proto = out.File + file_transport_internet_finalmask_xmc_config_proto_goTypes = nil + file_transport_internet_finalmask_xmc_config_proto_depIdxs = nil } diff --git a/transport/internet/finalmask/minecraft/config.proto b/transport/internet/finalmask/xmc/config.proto similarity index 56% rename from transport/internet/finalmask/minecraft/config.proto rename to transport/internet/finalmask/xmc/config.proto index 250afc2a5329..25fe29d4b24e 100644 --- a/transport/internet/finalmask/minecraft/config.proto +++ b/transport/internet/finalmask/xmc/config.proto @@ -1,9 +1,9 @@ syntax = "proto3"; -package xray.transport.internet.finalmask.minecraft; -option csharp_namespace = "Xray.Transport.Internet.Finalmask.Minecraft"; -option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/minecraft"; -option java_package = "com.xray.transport.internet.finalmask.minecraft"; +package xray.transport.internet.finalmask.xmc; +option csharp_namespace = "Xray.Transport.Internet.Finalmask.XMC"; +option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/xmc"; +option java_package = "com.xray.transport.internet.finalmask.xmc"; option java_multiple_files = true; diff --git a/transport/internet/finalmask/minecraft/derivation.go b/transport/internet/finalmask/xmc/derivation.go similarity index 99% rename from transport/internet/finalmask/minecraft/derivation.go rename to transport/internet/finalmask/xmc/derivation.go index bd1cbd595dfd..9dcb6a92a5e4 100644 --- a/transport/internet/finalmask/minecraft/derivation.go +++ b/transport/internet/finalmask/xmc/derivation.go @@ -1,4 +1,4 @@ -package minecraft +package xmc import ( "crypto/rsa" diff --git a/transport/internet/finalmask/minecraft/derivation_test.go b/transport/internet/finalmask/xmc/derivation_test.go similarity index 98% rename from transport/internet/finalmask/minecraft/derivation_test.go rename to transport/internet/finalmask/xmc/derivation_test.go index 2d16129baa06..b436d46dc5bc 100644 --- a/transport/internet/finalmask/minecraft/derivation_test.go +++ b/transport/internet/finalmask/xmc/derivation_test.go @@ -1,4 +1,4 @@ -package minecraft +package xmc import ( "crypto/sha256" diff --git a/transport/internet/finalmask/minecraft/handshake_test.go b/transport/internet/finalmask/xmc/handshake_test.go similarity index 99% rename from transport/internet/finalmask/minecraft/handshake_test.go rename to transport/internet/finalmask/xmc/handshake_test.go index 82ced87f66bd..8bb7577ae7dd 100644 --- a/transport/internet/finalmask/minecraft/handshake_test.go +++ b/transport/internet/finalmask/xmc/handshake_test.go @@ -1,4 +1,4 @@ -package minecraft +package xmc import ( "bytes" diff --git a/transport/internet/finalmask/minecraft/protocol.go b/transport/internet/finalmask/xmc/protocol.go similarity index 99% rename from transport/internet/finalmask/minecraft/protocol.go rename to transport/internet/finalmask/xmc/protocol.go index 8c019dace56d..68a4fb74df29 100644 --- a/transport/internet/finalmask/minecraft/protocol.go +++ b/transport/internet/finalmask/xmc/protocol.go @@ -1,5 +1,5 @@ // Minecraft protocol -package minecraft +package xmc import ( "bytes" diff --git a/transport/internet/finalmask/minecraft/server.go b/transport/internet/finalmask/xmc/server.go similarity index 99% rename from transport/internet/finalmask/minecraft/server.go rename to transport/internet/finalmask/xmc/server.go index 486bf30a6276..c79be767d8e7 100644 --- a/transport/internet/finalmask/minecraft/server.go +++ b/transport/internet/finalmask/xmc/server.go @@ -1,4 +1,4 @@ -package minecraft +package xmc import ( "bufio" diff --git a/transport/internet/finalmask/minecraft/stream.go b/transport/internet/finalmask/xmc/stream.go similarity index 98% rename from transport/internet/finalmask/minecraft/stream.go rename to transport/internet/finalmask/xmc/stream.go index 055e58b93579..2849a55eaf59 100644 --- a/transport/internet/finalmask/minecraft/stream.go +++ b/transport/internet/finalmask/xmc/stream.go @@ -1,5 +1,5 @@ // Minecraft stream cipher -package minecraft +package xmc import ( "crypto/aes" From f8b22087e8ff2166c912b66f5cd9dec29fcd4f9b Mon Sep 17 00:00:00 2001 From: Exclude0122 <173652916+Exclude0122@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:04:44 -0400 Subject: [PATCH 13/13] fix test --- .../internet/finalmask/xmc/handshake_test.go | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/transport/internet/finalmask/xmc/handshake_test.go b/transport/internet/finalmask/xmc/handshake_test.go index 8bb7577ae7dd..2e48c309c4d0 100644 --- a/transport/internet/finalmask/xmc/handshake_test.go +++ b/transport/internet/finalmask/xmc/handshake_test.go @@ -4,8 +4,8 @@ import ( "bytes" "crypto/x509" "net" + "sync" "testing" - "time" ) func deriveTestRSAKey(t *testing.T, password string) ([]byte, []byte) { @@ -105,9 +105,12 @@ func TestHandshakePasswordMismatch(t *testing.T) { serverPassword := "server-secret-456" usernames := []string{"test_user"} serverPrivateKey, serverPublicKey := deriveTestRSAKey(t, serverPassword) - _, clientPublicKey := deriveTestRSAKey(t, clientPassword) + var wg sync.WaitGroup + wg.Add(1) go func() { + defer wg.Done() + rawConn, err := ln.Accept() if err != nil { return @@ -136,21 +139,20 @@ func TestHandshakePasswordMismatch(t *testing.T) { } defer clientRaw.Close() - client, err := newClientConn(clientRaw, usernames, clientPassword, clientPublicKey, "localhost") + client, err := newClientConn(clientRaw, usernames, clientPassword, serverPublicKey, "localhost") if err != nil { t.Fatalf("failed to create client: %v", err) } - // Wait briefly or just perform write/read - _ = clientRaw.SetDeadline(time.Now().Add(100 * time.Millisecond)) - - _, err = client.Write([]byte("hello server")) - if err == nil { - // Try to read too - buf := make([]byte, 1024) - _, _ = client.Read(buf) + err = client.handshake() + if err != nil { + t.Fatalf("client handshake err: %v", err) } + _, _ = client.Write([]byte{0x1, 0x2, 0x3, 0x4}) + + wg.Wait() + // Check if we lost connection or received error t.Log("Handshake mismatch tested") }