Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions infra/conf/transport_internet.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package conf

import (
"context"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"math"
"net/netip"
"net/url"
Expand All @@ -24,6 +26,7 @@ import (
"github.com/xtls/xray-core/transport/internet"
"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"
Expand Down Expand Up @@ -1233,6 +1236,7 @@ var (
"header-custom": func() interface{} { return new(HeaderCustomTCP) },
"fragment": func() interface{} { return new(FragmentMask) },
"sudoku": func() interface{} { return new(Sudoku) },
"minecraft": func() interface{} { return new(Minecraft) },
}, "type", "settings")

udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
Expand Down Expand Up @@ -1436,6 +1440,41 @@ func (c *FragmentMask) Build() (proto.Message, error) {
return config, nil
}

type Minecraft struct {
Hostname string `json:"hostname"`
Usernames []string `json:"usernames"`
Password string `json:"password"`
}

func (c *Minecraft) Build() (proto.Message, error) {

if len(c.Usernames) == 0 {
c.Usernames = []string{"Dream"}
}

if c.Password == "" {
return nil, fmt.Errorf("empty password")
}

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{
Password: c.Password,
Usernames: c.Usernames,
Hostname: c.Hostname,
RsaPrivateKey: x509.MarshalPKCS1PrivateKey(rsaPrivateKey),
RsaPublicKey: rsaPublicKey,
}, nil
}

type NoiseItem struct {
Rand Int32Range `json:"rand"`
RandRange *Int32Range `json:"randRange"`
Expand Down
64 changes: 64 additions & 0 deletions transport/internet/finalmask/minecraft/cfb8.go
Original file line number Diff line number Diff line change
@@ -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
}
236 changes: 236 additions & 0 deletions transport/internet/finalmask/minecraft/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package minecraft

import (
"bufio"
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"fmt"
"io"
"math/big"
"net"
"strconv"
"sync"
"time"
)

type clientConn struct {
reader io.Reader
writer io.Writer
c net.Conn

state clientState

handshakeLock sync.Mutex
usernames []string
password string
rsaPublicKey []byte
hostname string
}

type clientState int

var (
clientStateHandshake clientState = 1
clientStateProxy clientState = 2
)

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")
}

return &clientConn{
reader: bufio.NewReader(c),
writer: c,
c: c,
state: clientStateHandshake,
handshakeLock: sync.Mutex{},
usernames: usernames,
password: password,
rsaPublicKey: rsaPublicKey,
hostname: hostname,
}, 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(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)
}

// 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)
}

if !bytes.Equal(publicKey, c.rsaPublicKey) {
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, []byte(c.password)...) // append pre-shared password

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
}
27 changes: 27 additions & 0 deletions transport/internet/finalmask/minecraft/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package minecraft

import (
"fmt"
"net"
)

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.Hostname)
if err != nil {
return nil, fmt.Errorf("minecraft finalmask: %w", err)
}

return cc, nil
}

func (c *Config) WrapConnServer(conn net.Conn) (net.Conn, error) {
cc, err := wrapConnServer(conn, c.Password, c.RsaPrivateKey, c.RsaPublicKey)
if err != nil {
return nil, fmt.Errorf("minecraft finalmask: %w", err)
}

return cc, nil
}
Loading
Loading