mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-09-15 22:10:26 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ca6f4b7d4 | ||
|
|
18e283909c | ||
|
|
6ab123bf8f | ||
|
|
4aba687dd3 | ||
|
|
5b1b41058e |
@@ -1,4 +1,4 @@
|
||||
//go:build darwin
|
||||
//go:build darwin && !ios
|
||||
|
||||
package net
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build darwin
|
||||
//go:build darwin && !ios
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build darwin
|
||||
//go:build darwin && !ios
|
||||
|
||||
package net
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build ios
|
||||
|
||||
package net
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (int, string, string, error) {
|
||||
return 0, "", "", errors.New("process lookup is not supported on this platform")
|
||||
}
|
||||
+1
-1
@@ -20,7 +20,7 @@ import (
|
||||
var (
|
||||
Version_x byte = 26
|
||||
Version_y byte = 7
|
||||
Version_z byte = 11
|
||||
Version_z byte = 28
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
googleuuid "github.com/google/uuid"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/fragment"
|
||||
@@ -720,14 +721,46 @@ func (c *Xdns) Build() (proto.Message, error) {
|
||||
}
|
||||
|
||||
type XMC struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Usernames []string `json:"usernames"`
|
||||
Password string `json:"password"`
|
||||
Hostname string `json:"hostname"`
|
||||
Profiles []XMCProfile `json:"profiles"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type XMCProfile struct {
|
||||
// Resolve the UUID by username, then request the session profile with
|
||||
// unsigned=false. Client and server must use the same signed profile.
|
||||
Username string `json:"username"`
|
||||
UUID string `json:"uuid"`
|
||||
TexturesValue string `json:"texturesValue"`
|
||||
TexturesSignature string `json:"texturesSignature"`
|
||||
}
|
||||
|
||||
var xmcUsernamePattern = regexp.MustCompile(`^[A-Za-z0-9_]{3,16}$`)
|
||||
|
||||
func (c *XMCProfile) Build() (*xmc.Profile, error) {
|
||||
if !xmcUsernamePattern.MatchString(c.Username) {
|
||||
return nil, fmt.Errorf("invalid minecraft profile username: %q", c.Username)
|
||||
}
|
||||
|
||||
profileUUID, err := googleuuid.Parse(c.UUID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid minecraft profile UUID: %w", err)
|
||||
}
|
||||
if c.TexturesValue == "" || c.TexturesSignature == "" {
|
||||
return nil, fmt.Errorf("incomplete minecraft profile textures")
|
||||
}
|
||||
|
||||
return &xmc.Profile{
|
||||
Username: c.Username,
|
||||
Uuid: append([]byte(nil), profileUUID[:]...),
|
||||
TexturesValue: c.TexturesValue,
|
||||
TexturesSignature: c.TexturesSignature,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *XMC) Build() (proto.Message, error) {
|
||||
if len(c.Usernames) == 0 {
|
||||
c.Usernames = []string{"Dream"}
|
||||
if len(c.Profiles) == 0 {
|
||||
return nil, fmt.Errorf("minecraft profiles are required")
|
||||
}
|
||||
|
||||
if c.Password == "" {
|
||||
@@ -744,12 +777,21 @@ func (c *XMC) Build() (proto.Message, error) {
|
||||
return nil, fmt.Errorf("marshal minecraft rsa public key: %w", err)
|
||||
}
|
||||
|
||||
profiles := make([]*xmc.Profile, 0, len(c.Profiles))
|
||||
for i := range c.Profiles {
|
||||
profile, err := c.Profiles[i].Build()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build minecraft profile %d: %w", i, err)
|
||||
}
|
||||
profiles = append(profiles, profile)
|
||||
}
|
||||
|
||||
return &xmc.Config{
|
||||
Password: c.Password,
|
||||
Usernames: c.Usernames,
|
||||
Hostname: c.Hostname,
|
||||
RsaPrivateKey: x509.MarshalPKCS1PrivateKey(rsaPrivateKey),
|
||||
RsaPublicKey: rsaPublicKey,
|
||||
Profiles: profiles,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/xmc"
|
||||
)
|
||||
|
||||
func TestXMCBuildProfile(t *testing.T) {
|
||||
built, err := (&XMC{
|
||||
Password: "test-password",
|
||||
Profiles: []XMCProfile{
|
||||
{
|
||||
Username: "TestUser",
|
||||
UUID: "00112233-4455-6677-8899-aabbccddeeff",
|
||||
TexturesValue: "textures-value",
|
||||
TexturesSignature: "textures-signature",
|
||||
},
|
||||
},
|
||||
}).Build()
|
||||
if err != nil {
|
||||
t.Fatalf("build XMC config: %v", err)
|
||||
}
|
||||
config := built.(*xmc.Config)
|
||||
if len(config.Profiles) != 1 || len(config.Profiles[0].Uuid) != 16 {
|
||||
t.Fatalf("unexpected profiles: %+v", config.Profiles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXMCBuildRequiresProfile(t *testing.T) {
|
||||
_, err := (&XMC{Password: "test-password"}).Build()
|
||||
if err == nil || !strings.Contains(err.Error(), "profiles are required") {
|
||||
t.Fatalf("expected required profiles error, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -450,8 +450,8 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
|
||||
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
|
||||
}
|
||||
if c.Xmux == (XmuxConfig{}) {
|
||||
c.Xmux.MaxConnections.From = 6
|
||||
c.Xmux.MaxConnections.To = 6
|
||||
c.Xmux.MaxConnections.From = 3
|
||||
c.Xmux.MaxConnections.To = 3
|
||||
c.Xmux.HMaxRequestTimes.From = 600
|
||||
c.Xmux.HMaxRequestTimes.To = 900
|
||||
c.Xmux.HMaxReusableSecs.From = 1800
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -23,11 +22,16 @@ type clientConn struct {
|
||||
|
||||
state clientState
|
||||
|
||||
handshakeLock sync.Mutex
|
||||
usernames []string
|
||||
password string
|
||||
rsaPublicKey []byte
|
||||
hostname string
|
||||
handshakeLock sync.Mutex
|
||||
lifecycleMu sync.Mutex
|
||||
closed bool
|
||||
profiles []loginProfile
|
||||
password string
|
||||
rsaPublicKey []byte
|
||||
hostname string
|
||||
paddingSchedule []paddingTurn
|
||||
packet *packetStream
|
||||
deadlines *connectionDeadlines
|
||||
}
|
||||
|
||||
type clientState int
|
||||
@@ -37,21 +41,29 @@ var (
|
||||
clientStateProxy clientState = 2
|
||||
)
|
||||
|
||||
func newClientConn(c net.Conn, usernames []string, password string, rsaPublicKey []byte, hostname string) (*clientConn, error) {
|
||||
func newClientConn(c net.Conn, profiles []loginProfile, password string, rsaPublicKey []byte, hostname string) (*clientConn, error) {
|
||||
if len(rsaPublicKey) == 0 {
|
||||
return nil, fmt.Errorf("empty rsa public key")
|
||||
}
|
||||
|
||||
if len(profiles) == 0 {
|
||||
return nil, fmt.Errorf("empty profiles")
|
||||
}
|
||||
paddingSchedule, err := newClientPaddingSchedule2612()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("select padding profile: %w", err)
|
||||
}
|
||||
return &clientConn{
|
||||
reader: bufio.NewReader(c),
|
||||
writer: c,
|
||||
c: c,
|
||||
state: clientStateHandshake,
|
||||
handshakeLock: sync.Mutex{},
|
||||
usernames: usernames,
|
||||
password: password,
|
||||
rsaPublicKey: rsaPublicKey,
|
||||
hostname: hostname,
|
||||
reader: bufio.NewReader(c),
|
||||
writer: c,
|
||||
c: c,
|
||||
state: clientStateHandshake,
|
||||
handshakeLock: sync.Mutex{},
|
||||
profiles: profiles,
|
||||
password: password,
|
||||
rsaPublicKey: rsaPublicKey,
|
||||
hostname: hostname,
|
||||
paddingSchedule: paddingSchedule,
|
||||
deadlines: newConnectionDeadlines(c),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -63,12 +75,10 @@ func (c *clientConn) handshake() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handshake timeout
|
||||
err := c.c.SetDeadline(time.Now().Add(time.Second * 30))
|
||||
if err != nil {
|
||||
if err := c.deadlines.beginHandshake(); err != nil {
|
||||
return fmt.Errorf("set deadline: %w", err)
|
||||
}
|
||||
defer c.c.SetDeadline(time.Time{})
|
||||
defer func() { _ = c.deadlines.endHandshake() }()
|
||||
|
||||
var (
|
||||
protocolVersion Varint = Varint(775)
|
||||
@@ -95,16 +105,14 @@ func (c *clientConn) handshake() error {
|
||||
}
|
||||
|
||||
// Login Start
|
||||
var (
|
||||
username string
|
||||
offlineUUID UUID
|
||||
)
|
||||
randomProfile, err := rand.Int(rand.Reader, big.NewInt(int64(len(c.profiles))))
|
||||
if err != nil {
|
||||
return fmt.Errorf("select profile: %w", err)
|
||||
}
|
||||
selectedProfile := c.profiles[randomProfile.Int64()]
|
||||
username := String(selectedProfile.Username)
|
||||
|
||||
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)
|
||||
err = writePacket(c.writer, 0x00, &username, &selectedProfile.UUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write login start: %w", err)
|
||||
}
|
||||
@@ -145,7 +153,9 @@ func (c *clientConn) handshake() error {
|
||||
}
|
||||
|
||||
sharedSecret := make([]byte, 16)
|
||||
rand.Read(sharedSecret)
|
||||
if _, err = rand.Read(sharedSecret); err != nil {
|
||||
return fmt.Errorf("generate shared secret: %w", err)
|
||||
}
|
||||
|
||||
encryptedSharedSecret, err := rsa.EncryptPKCS1v15(rand.Reader, rsaPublicKey, sharedSecret)
|
||||
if err != nil {
|
||||
@@ -181,7 +191,48 @@ func (c *clientConn) handshake() error {
|
||||
return fmt.Errorf("new crypto writer: %w", err)
|
||||
}
|
||||
|
||||
pkt, err = readPacket(c.reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read login finished: %w", err)
|
||||
}
|
||||
if pkt.packetID == 0x00 {
|
||||
var reason String
|
||||
if readErr := pkt.readFields(&reason); readErr != nil {
|
||||
return fmt.Errorf("authentication rejected")
|
||||
}
|
||||
return fmt.Errorf("authentication rejected: %s", reason)
|
||||
}
|
||||
if pkt.packetID != 0x02 {
|
||||
return fmt.Errorf("bad login finished packet id: %d", pkt.packetID)
|
||||
}
|
||||
|
||||
receivedProfile, err := readLoginSuccess(pkt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read login finished fields: %w", err)
|
||||
}
|
||||
if receivedProfile != selectedProfile {
|
||||
return fmt.Errorf("login profile mismatch")
|
||||
}
|
||||
loginAcknowledgedLength, err := writePacketWithLength(c.writer, 0x03)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write login acknowledged: %w", err)
|
||||
}
|
||||
if err = runPaddingSchedule(c.reader, c.writer, true, loginAcknowledgedLength, c.paddingSchedule); err != nil {
|
||||
return fmt.Errorf("run startup padding: %w", err)
|
||||
}
|
||||
|
||||
packet := newPacketStream(c.reader, c.writer, true)
|
||||
c.lifecycleMu.Lock()
|
||||
if c.closed {
|
||||
c.lifecycleMu.Unlock()
|
||||
packet.Stop()
|
||||
return net.ErrClosed
|
||||
}
|
||||
c.packet = packet
|
||||
c.reader = packet
|
||||
c.writer = packet
|
||||
c.state = clientStateProxy
|
||||
c.lifecycleMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -205,6 +256,13 @@ func (c *clientConn) Write(b []byte) (int, error) {
|
||||
}
|
||||
|
||||
func (c *clientConn) Close() error {
|
||||
c.lifecycleMu.Lock()
|
||||
c.closed = true
|
||||
packet := c.packet
|
||||
c.lifecycleMu.Unlock()
|
||||
if packet != nil {
|
||||
packet.Stop()
|
||||
}
|
||||
return c.c.Close()
|
||||
}
|
||||
|
||||
@@ -217,20 +275,13 @@ func (c *clientConn) RemoteAddr() net.Addr {
|
||||
}
|
||||
|
||||
func (c *clientConn) SetDeadline(t time.Time) error {
|
||||
return c.c.SetDeadline(t)
|
||||
return c.deadlines.setDeadline(t)
|
||||
}
|
||||
|
||||
func (c *clientConn) SetReadDeadline(t time.Time) error {
|
||||
return c.c.SetReadDeadline(t)
|
||||
return c.deadlines.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
|
||||
return c.deadlines.setWriteDeadline(t)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@ 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)
|
||||
profiles, err := profilesFromConfig(c.Profiles)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minecraft finalmask: %w", err)
|
||||
}
|
||||
cc, err := newClientConn(conn, profiles, c.Password, c.RsaPublicKey, c.Hostname)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minecraft finalmask: %w", err)
|
||||
}
|
||||
@@ -18,7 +22,11 @@ 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.Password, c.RsaPrivateKey, c.RsaPublicKey)
|
||||
profiles, err := profilesFromConfig(c.Profiles)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minecraft finalmask: %w", err)
|
||||
}
|
||||
cc, err := wrapConnServer(conn, profiles, c.Password, c.RsaPrivateKey, c.RsaPublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minecraft finalmask: %w", err)
|
||||
}
|
||||
|
||||
@@ -21,20 +21,91 @@ const (
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type Profile struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Resolve the UUID from https://api.mojang.com/users/profiles/minecraft/{username}.
|
||||
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
|
||||
Uuid []byte `protobuf:"bytes,2,opt,name=uuid,proto3" json:"uuid,omitempty"`
|
||||
// Copy the signed textures property returned by
|
||||
// https://sessionserver.mojang.com/session/minecraft/profile/{uuid}?unsigned=false.
|
||||
TexturesValue string `protobuf:"bytes,3,opt,name=textures_value,json=texturesValue,proto3" json:"textures_value,omitempty"`
|
||||
TexturesSignature string `protobuf:"bytes,4,opt,name=textures_signature,json=texturesSignature,proto3" json:"textures_signature,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Profile) Reset() {
|
||||
*x = Profile{}
|
||||
mi := &file_transport_internet_finalmask_xmc_config_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Profile) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Profile) ProtoMessage() {}
|
||||
|
||||
func (x *Profile) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_finalmask_xmc_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 Profile.ProtoReflect.Descriptor instead.
|
||||
func (*Profile) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_finalmask_xmc_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Profile) GetUsername() string {
|
||||
if x != nil {
|
||||
return x.Username
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Profile) GetUuid() []byte {
|
||||
if x != nil {
|
||||
return x.Uuid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Profile) GetTexturesValue() string {
|
||||
if x != nil {
|
||||
return x.TexturesValue
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Profile) GetTexturesSignature() string {
|
||||
if x != nil {
|
||||
return x.TexturesSignature
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
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"`
|
||||
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"`
|
||||
Profiles []*Profile `protobuf:"bytes,11,rep,name=profiles,proto3" json:"profiles,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Config) Reset() {
|
||||
*x = Config{}
|
||||
mi := &file_transport_internet_finalmask_xmc_config_proto_msgTypes[0]
|
||||
mi := &file_transport_internet_finalmask_xmc_config_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -46,7 +117,7 @@ func (x *Config) String() string {
|
||||
func (*Config) ProtoMessage() {}
|
||||
|
||||
func (x *Config) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_finalmask_xmc_config_proto_msgTypes[0]
|
||||
mi := &file_transport_internet_finalmask_xmc_config_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -59,7 +130,7 @@ func (x *Config) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
|
||||
func (*Config) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_finalmask_xmc_config_proto_rawDescGZIP(), []int{0}
|
||||
return file_transport_internet_finalmask_xmc_config_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *Config) GetPassword() string {
|
||||
@@ -69,13 +140,6 @@ func (x *Config) GetPassword() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetUsernames() []string {
|
||||
if x != nil {
|
||||
return x.Usernames
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetRsaPrivateKey() []byte {
|
||||
if x != nil {
|
||||
return x.RsaPrivateKey
|
||||
@@ -97,18 +161,30 @@ func (x *Config) GetHostname() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetProfiles() []*Profile {
|
||||
if x != nil {
|
||||
return x.Profiles
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_transport_internet_finalmask_xmc_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_transport_internet_finalmask_xmc_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"-transport/internet/finalmask/xmc/config.proto\x12%xray.transport.internet.finalmask.xmc\"\xac\x01\n" +
|
||||
"-transport/internet/finalmask/xmc/config.proto\x12%xray.transport.internet.finalmask.xmc\"\x8f\x01\n" +
|
||||
"\aProfile\x12\x1a\n" +
|
||||
"\busername\x18\x01 \x01(\tR\busername\x12\x12\n" +
|
||||
"\x04uuid\x18\x02 \x01(\fR\x04uuid\x12%\n" +
|
||||
"\x0etextures_value\x18\x03 \x01(\tR\rtexturesValue\x12-\n" +
|
||||
"\x12textures_signature\x18\x04 \x01(\tR\x11texturesSignature\"\xe0\x01\n" +
|
||||
"\x06Config\x12\x1a\n" +
|
||||
"\bpassword\x18\x01 \x01(\tR\bpassword\x12\x1c\n" +
|
||||
"\tusernames\x18\x02 \x03(\tR\tusernames\x12&\n" +
|
||||
"\bpassword\x18\x01 \x01(\tR\bpassword\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\x91\x01\n" +
|
||||
" \x01(\tR\bhostname\x12J\n" +
|
||||
"\bprofiles\x18\v \x03(\v2..xray.transport.internet.finalmask.xmc.ProfileR\bprofilesJ\x04\b\x02\x10\x03B\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 (
|
||||
@@ -123,16 +199,18 @@ func file_transport_internet_finalmask_xmc_config_proto_rawDescGZIP() []byte {
|
||||
return file_transport_internet_finalmask_xmc_config_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_transport_internet_finalmask_xmc_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_transport_internet_finalmask_xmc_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_transport_internet_finalmask_xmc_config_proto_goTypes = []any{
|
||||
(*Config)(nil), // 0: xray.transport.internet.finalmask.xmc.Config
|
||||
(*Profile)(nil), // 0: xray.transport.internet.finalmask.xmc.Profile
|
||||
(*Config)(nil), // 1: xray.transport.internet.finalmask.xmc.Config
|
||||
}
|
||||
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
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
0, // 0: xray.transport.internet.finalmask.xmc.Config.profiles:type_name -> xray.transport.internet.finalmask.xmc.Profile
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_transport_internet_finalmask_xmc_config_proto_init() }
|
||||
@@ -146,7 +224,7 @@ func file_transport_internet_finalmask_xmc_config_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
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,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
@@ -6,11 +6,21 @@ 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;
|
||||
|
||||
message Profile {
|
||||
// Resolve the UUID from https://api.mojang.com/users/profiles/minecraft/{username}.
|
||||
string username = 1;
|
||||
bytes uuid = 2;
|
||||
// Copy the signed textures property returned by
|
||||
// https://sessionserver.mojang.com/session/minecraft/profile/{uuid}?unsigned=false.
|
||||
string textures_value = 3;
|
||||
string textures_signature = 4;
|
||||
}
|
||||
|
||||
message Config {
|
||||
string password = 1;
|
||||
repeated string usernames = 2;
|
||||
reserved 2;
|
||||
bytes rsa_private_key = 8;
|
||||
bytes rsa_public_key = 9;
|
||||
string hostname = 10;
|
||||
repeated Profile profiles = 11;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const handshakeTimeout = 2 * time.Minute
|
||||
|
||||
type connectionDeadlines struct {
|
||||
mu sync.Mutex
|
||||
c net.Conn
|
||||
|
||||
read time.Time
|
||||
write time.Time
|
||||
handshake time.Time
|
||||
}
|
||||
|
||||
func newConnectionDeadlines(c net.Conn) *connectionDeadlines {
|
||||
return &connectionDeadlines{c: c}
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) beginHandshake() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.handshake = time.Now().Add(handshakeTimeout)
|
||||
if err := d.applyLocked(); err != nil {
|
||||
d.handshake = time.Time{}
|
||||
_ = d.applyLocked()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) endHandshake() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.handshake = time.Time{}
|
||||
return d.applyLocked()
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) setDeadline(t time.Time) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.read = t
|
||||
d.write = t
|
||||
return d.applyLocked()
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) setReadDeadline(t time.Time) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.read = t
|
||||
return d.c.SetReadDeadline(earlierDeadline(d.read, d.handshake))
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) setWriteDeadline(t time.Time) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.write = t
|
||||
return d.c.SetWriteDeadline(earlierDeadline(d.write, d.handshake))
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) applyLocked() error {
|
||||
if err := d.c.SetReadDeadline(earlierDeadline(d.read, d.handshake)); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.c.SetWriteDeadline(earlierDeadline(d.write, d.handshake))
|
||||
}
|
||||
|
||||
func earlierDeadline(user, internal time.Time) time.Time {
|
||||
if internal.IsZero() {
|
||||
return user
|
||||
}
|
||||
if user.IsZero() || internal.Before(user) {
|
||||
return internal
|
||||
}
|
||||
return user
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConnectionDeadlinesRestoreCallerValues(t *testing.T) {
|
||||
client, server := net.Pipe()
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
|
||||
recording := &deadlineRecordingConn{Conn: client}
|
||||
deadlines := newConnectionDeadlines(recording)
|
||||
callerDeadline := time.Now().Add(10 * time.Minute)
|
||||
if err := deadlines.setDeadline(callerDeadline); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := deadlines.beginHandshake(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
read, write := recording.currentDeadlines()
|
||||
if !read.Before(callerDeadline) || !write.Before(callerDeadline) {
|
||||
t.Fatalf("handshake deadlines = %s/%s, caller = %s", read, write, callerDeadline)
|
||||
}
|
||||
|
||||
shortReadDeadline := time.Now().Add(time.Second)
|
||||
if err := deadlines.setReadDeadline(shortReadDeadline); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
read, _ = recording.currentDeadlines()
|
||||
if !read.Equal(shortReadDeadline) {
|
||||
t.Fatalf("read deadline = %s, want %s", read, shortReadDeadline)
|
||||
}
|
||||
|
||||
if err := deadlines.endHandshake(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
read, write = recording.currentDeadlines()
|
||||
if !read.Equal(shortReadDeadline) || !write.Equal(callerDeadline) {
|
||||
t.Fatalf("restored deadlines = %s/%s, want %s/%s", read, write, shortReadDeadline, callerDeadline)
|
||||
}
|
||||
}
|
||||
|
||||
type deadlineRecordingConn struct {
|
||||
net.Conn
|
||||
mu sync.Mutex
|
||||
read time.Time
|
||||
write time.Time
|
||||
}
|
||||
|
||||
func (c *deadlineRecordingConn) SetDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
c.read = t
|
||||
c.write = t
|
||||
c.mu.Unlock()
|
||||
return c.Conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *deadlineRecordingConn) SetReadDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
c.read = t
|
||||
c.mu.Unlock()
|
||||
return c.Conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *deadlineRecordingConn) SetWriteDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
c.write = t
|
||||
c.mu.Unlock()
|
||||
return c.Conn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (c *deadlineRecordingConn) currentDeadlines() (time.Time, time.Time) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.read, c.write
|
||||
}
|
||||
@@ -2,10 +2,16 @@ package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func deriveTestRSAKey(t *testing.T, password string) ([]byte, []byte) {
|
||||
@@ -24,6 +30,19 @@ func deriveTestRSAKey(t *testing.T, password string) ([]byte, []byte) {
|
||||
return x509.MarshalPKCS1PrivateKey(key), publicKey
|
||||
}
|
||||
|
||||
func testLoginProfile(username string) loginProfile {
|
||||
profile := loginProfile{
|
||||
Username: username,
|
||||
TexturesValue: strings.Repeat("texture-value-", 40),
|
||||
TexturesSignature: strings.Repeat("texture-signature-", 24),
|
||||
}
|
||||
digest := sha256.Sum256([]byte(username))
|
||||
copy(profile.UUID[:], digest[:16])
|
||||
profile.UUID[6] = (profile.UUID[6] & 0x0f) | 0x40
|
||||
profile.UUID[8] = (profile.UUID[8] & 0x3f) | 0x80
|
||||
return profile
|
||||
}
|
||||
|
||||
func TestHandshakeSuccess(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
@@ -32,7 +51,7 @@ func TestHandshakeSuccess(t *testing.T) {
|
||||
defer ln.Close()
|
||||
|
||||
password := "super-secure-shared-key-12345"
|
||||
usernames := []string{"test_user"}
|
||||
profiles := []loginProfile{testLoginProfile("test_user")}
|
||||
privateKey, publicKey := deriveTestRSAKey(t, password)
|
||||
|
||||
go func() {
|
||||
@@ -42,7 +61,7 @@ func TestHandshakeSuccess(t *testing.T) {
|
||||
}
|
||||
defer rawConn.Close()
|
||||
|
||||
server, err := wrapConnServer(rawConn, password, privateKey, publicKey)
|
||||
server, err := wrapConnServer(rawConn, profiles, password, privateKey, publicKey)
|
||||
if err != nil {
|
||||
t.Errorf("failed to wrap server: %v", err)
|
||||
return
|
||||
@@ -73,7 +92,7 @@ func TestHandshakeSuccess(t *testing.T) {
|
||||
}
|
||||
defer clientRaw.Close()
|
||||
|
||||
client, err := newClientConn(clientRaw, usernames, password, publicKey, "localhost")
|
||||
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create client: %v", err)
|
||||
}
|
||||
@@ -103,7 +122,7 @@ func TestHandshakePasswordMismatch(t *testing.T) {
|
||||
|
||||
clientPassword := "client-secret-123"
|
||||
serverPassword := "server-secret-456"
|
||||
usernames := []string{"test_user"}
|
||||
profiles := []loginProfile{testLoginProfile("test_user")}
|
||||
serverPrivateKey, serverPublicKey := deriveTestRSAKey(t, serverPassword)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
@@ -117,7 +136,7 @@ func TestHandshakePasswordMismatch(t *testing.T) {
|
||||
}
|
||||
defer rawConn.Close()
|
||||
|
||||
server, err := wrapConnServer(rawConn, serverPassword, serverPrivateKey, serverPublicKey)
|
||||
server, err := wrapConnServer(rawConn, profiles, 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)
|
||||
@@ -139,20 +158,233 @@ func TestHandshakePasswordMismatch(t *testing.T) {
|
||||
}
|
||||
defer clientRaw.Close()
|
||||
|
||||
client, err := newClientConn(clientRaw, usernames, clientPassword, serverPublicKey, "localhost")
|
||||
client, err := newClientConn(clientRaw, profiles, clientPassword, serverPublicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create client: %v", err)
|
||||
}
|
||||
|
||||
err = client.handshake()
|
||||
if err != nil {
|
||||
t.Fatalf("client handshake err: %v", err)
|
||||
if err == nil {
|
||||
t.Fatal("expected client handshake to fail due to password mismatch")
|
||||
}
|
||||
|
||||
_, _ = client.Write([]byte{0x1, 0x2, 0x3, 0x4})
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Check if we lost connection or received error
|
||||
t.Log("Handshake mismatch tested")
|
||||
}
|
||||
|
||||
func TestHandshakeNetPipeWithKeepAlive(t *testing.T) {
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
const password = "net-pipe-shared-key"
|
||||
profiles := []loginProfile{testLoginProfile("pipe_user")}
|
||||
privateKey, publicKey := deriveTestRSAKey(t, password)
|
||||
serverDone := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
server, err := wrapConnServer(serverRaw, profiles, password, privateKey, publicKey)
|
||||
if err != nil {
|
||||
serverDone <- err
|
||||
return
|
||||
}
|
||||
|
||||
request := make([]byte, len("hello server"))
|
||||
if _, err = io.ReadFull(server, request); err != nil {
|
||||
serverDone <- fmt.Errorf("read request: %w", err)
|
||||
return
|
||||
}
|
||||
if string(request) != "hello server" {
|
||||
serverDone <- fmt.Errorf("unexpected request: %q", request)
|
||||
return
|
||||
}
|
||||
|
||||
followupDone := make(chan error, 1)
|
||||
go func() {
|
||||
followup := make([]byte, len("after keepalive"))
|
||||
_, readErr := io.ReadFull(server, followup)
|
||||
if readErr == nil && string(followup) != "after keepalive" {
|
||||
readErr = fmt.Errorf("unexpected followup: %q", followup)
|
||||
}
|
||||
followupDone <- readErr
|
||||
}()
|
||||
|
||||
if err = server.packet.writeKeepAlive(Long(42)); err != nil {
|
||||
serverDone <- fmt.Errorf("write keep-alive: %w", err)
|
||||
return
|
||||
}
|
||||
if _, err = server.Write([]byte("hello client")); err != nil {
|
||||
serverDone <- fmt.Errorf("write response: %w", err)
|
||||
return
|
||||
}
|
||||
serverDone <- <-followupDone
|
||||
}()
|
||||
|
||||
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = client.Write([]byte("hello server")); err != nil {
|
||||
t.Fatalf("write request: %v", err)
|
||||
}
|
||||
response := make([]byte, len("hello client"))
|
||||
if _, err = io.ReadFull(client, response); err != nil {
|
||||
t.Fatalf("read response: %v", err)
|
||||
}
|
||||
if string(response) != "hello client" {
|
||||
t.Fatalf("unexpected response: %q", response)
|
||||
}
|
||||
if _, err = client.Write([]byte("after keepalive")); err != nil {
|
||||
t.Fatalf("write followup: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case err = <-serverDone:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("net.Pipe handshake timed out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusQueryUnaffected(t *testing.T) {
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
const password = "status-shared-key"
|
||||
profiles := []loginProfile{testLoginProfile("status_user")}
|
||||
privateKey, publicKey := deriveTestRSAKey(t, password)
|
||||
serverDone := make(chan error, 1)
|
||||
go func() {
|
||||
server, err := wrapConnServer(serverRaw, profiles, password, privateKey, publicKey)
|
||||
if err == nil {
|
||||
err = server.handshake()
|
||||
}
|
||||
serverDone <- err
|
||||
}()
|
||||
|
||||
protocolVersion := Varint(775)
|
||||
serverAddress := String("localhost")
|
||||
serverPort := UnsignedShort(25565)
|
||||
nextState := Varint(1)
|
||||
if err := writePacket(clientRaw, 0x00, &protocolVersion, &serverAddress, &serverPort, &nextState); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writePacket(clientRaw, 0x00); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response, err := readPacket(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.packetID != 0x00 {
|
||||
t.Fatalf("status packet id = %d", response.packetID)
|
||||
}
|
||||
var responseJSON String
|
||||
if err = response.readFields(&responseJSON); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(responseJSON) != statusResponse {
|
||||
t.Fatalf("status response = %q", responseJSON)
|
||||
}
|
||||
|
||||
payload := Long(0x0102030405060708)
|
||||
if err = writePacket(clientRaw, 0x01, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pong, err := readPacket(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var receivedPayload Long
|
||||
if pong.packetID != 0x01 {
|
||||
t.Fatalf("pong packet id = %d", pong.packetID)
|
||||
}
|
||||
if err = pong.readFields(&receivedPayload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if receivedPayload != payload {
|
||||
t.Fatalf("pong payload = %x", receivedPayload)
|
||||
}
|
||||
|
||||
select {
|
||||
case err = <-serverDone:
|
||||
if err == nil || !strings.Contains(err.Error(), "ping") {
|
||||
t.Fatalf("server error = %v", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("status handshake timed out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientHandshakeHonorsCallerDeadline(t *testing.T) {
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
const password = "deadline-shared-key"
|
||||
profiles := []loginProfile{testLoginProfile("deadline_user")}
|
||||
_, publicKey := deriveTestRSAKey(t, password)
|
||||
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = client.SetDeadline(time.Now().Add(30 * time.Millisecond)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
started := time.Now()
|
||||
_, err = client.Write([]byte("blocked"))
|
||||
var netErr net.Error
|
||||
if !errors.As(err, &netErr) || !netErr.Timeout() {
|
||||
t.Fatalf("error = %v, want network timeout", err)
|
||||
}
|
||||
if elapsed := time.Since(started); elapsed > 500*time.Millisecond {
|
||||
t.Fatalf("caller deadline took %s", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientCloseInterruptsHandshake(t *testing.T) {
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer serverRaw.Close()
|
||||
|
||||
const password = "close-shared-key"
|
||||
profiles := []loginProfile{testLoginProfile("close_user")}
|
||||
_, publicKey := deriveTestRSAKey(t, password)
|
||||
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, writeErr := client.Write([]byte("blocked"))
|
||||
done <- writeErr
|
||||
}()
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
if err = client.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
select {
|
||||
case err = <-done:
|
||||
if err == nil {
|
||||
t.Fatal("handshake unexpectedly succeeded after close")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("close did not interrupt handshake")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLoginAcknowledgedPacketRejectsData(t *testing.T) {
|
||||
if err := validateLoginAcknowledgedPacket(&mcPacket{packetID: 0x03}); err != nil {
|
||||
t.Fatalf("valid login acknowledged packet: %v", err)
|
||||
}
|
||||
if err := validateLoginAcknowledgedPacket(&mcPacket{packetID: 0x03, data: []byte{0x00}}); err == nil {
|
||||
t.Fatal("login acknowledged packet with trailing data was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
configurationClientboundCustomPayload = 0x01
|
||||
configurationServerboundCustomPayload = 0x02
|
||||
configurationKeepAlive = 0x04
|
||||
|
||||
packetChannel = "xmc:data"
|
||||
maxPacketData = 24 * 1024
|
||||
keepAlivePeriod = 15 * time.Second
|
||||
)
|
||||
|
||||
// packetStream carries the raw proxy byte stream in Minecraft configuration
|
||||
// custom payload packets. The configuration state provides bidirectional
|
||||
// payload packets and keep-alives without requiring version-specific world data.
|
||||
type packetStream struct {
|
||||
reader io.Reader
|
||||
writer io.Writer
|
||||
isClient bool
|
||||
|
||||
readMu sync.Mutex
|
||||
writeMu sync.Mutex
|
||||
pending []byte
|
||||
|
||||
keepAliveID atomic.Int64
|
||||
done chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
func newPacketStream(reader io.Reader, writer io.Writer, isClient bool) *packetStream {
|
||||
s := &packetStream{
|
||||
reader: reader,
|
||||
writer: writer,
|
||||
isClient: isClient,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
if !isClient {
|
||||
go s.keepAliveLoop()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *packetStream) Read(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
s.readMu.Lock()
|
||||
defer s.readMu.Unlock()
|
||||
|
||||
if len(s.pending) > 0 {
|
||||
n := copy(p, s.pending)
|
||||
s.pending = s.pending[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
for {
|
||||
packet, err := readPacket(s.reader)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("read minecraft packet stream: %w", err)
|
||||
}
|
||||
|
||||
if packet.packetID == s.remoteCustomPayloadID() {
|
||||
payload, ok, err := parseCustomPayload(packet)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !ok || len(payload) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
n := copy(p, payload)
|
||||
if n < len(payload) {
|
||||
s.pending = append(s.pending[:0], payload[n:]...)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
if packet.packetID == configurationKeepAlive {
|
||||
var id Long
|
||||
if err := packet.readFields(&id); err != nil {
|
||||
return 0, fmt.Errorf("read minecraft keep-alive: %w", err)
|
||||
}
|
||||
if s.isClient {
|
||||
if err := s.writeKeepAlive(id); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *packetStream) Write(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
s.writeMu.Lock()
|
||||
defer s.writeMu.Unlock()
|
||||
|
||||
written := 0
|
||||
for written < len(p) {
|
||||
end := written + maxPacketData
|
||||
if end > len(p) {
|
||||
end = len(p)
|
||||
}
|
||||
channel := String(packetChannel)
|
||||
payload := RestBytes(p[written:end])
|
||||
if err := writePacket(s.writer, s.localCustomPayloadID(), &channel, &payload); err != nil {
|
||||
return written, fmt.Errorf("write minecraft custom payload: %w", err)
|
||||
}
|
||||
written = end
|
||||
}
|
||||
|
||||
return written, nil
|
||||
}
|
||||
|
||||
func (s *packetStream) Stop() {
|
||||
s.stopOnce.Do(func() { close(s.done) })
|
||||
}
|
||||
|
||||
func (s *packetStream) localCustomPayloadID() int {
|
||||
if s.isClient {
|
||||
return configurationServerboundCustomPayload
|
||||
}
|
||||
return configurationClientboundCustomPayload
|
||||
}
|
||||
|
||||
func (s *packetStream) remoteCustomPayloadID() int {
|
||||
if s.isClient {
|
||||
return configurationClientboundCustomPayload
|
||||
}
|
||||
return configurationServerboundCustomPayload
|
||||
}
|
||||
|
||||
func parseCustomPayload(packet *mcPacket) ([]byte, bool, error) {
|
||||
r := bytes.NewReader(packet.data)
|
||||
var channel String
|
||||
if err := channel.readFrom(r); err != nil {
|
||||
return nil, false, fmt.Errorf("read minecraft custom payload channel: %w", err)
|
||||
}
|
||||
if string(channel) != packetChannel {
|
||||
return nil, false, nil
|
||||
}
|
||||
payload := make([]byte, r.Len())
|
||||
if _, err := io.ReadFull(r, payload); err != nil {
|
||||
return nil, false, fmt.Errorf("read minecraft custom payload data: %w", err)
|
||||
}
|
||||
return payload, true, nil
|
||||
}
|
||||
|
||||
func (s *packetStream) writeKeepAlive(id Long) error {
|
||||
s.writeMu.Lock()
|
||||
defer s.writeMu.Unlock()
|
||||
if err := writePacket(s.writer, configurationKeepAlive, &id); err != nil {
|
||||
return fmt.Errorf("write minecraft keep-alive: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *packetStream) keepAliveLoop() {
|
||||
ticker := time.NewTicker(keepAlivePeriod)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
id := Long(s.keepAliveID.Add(1))
|
||||
if err := s.writeKeepAlive(id); err != nil {
|
||||
return
|
||||
}
|
||||
case <-s.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPacketStreamUsesPlainFraming(t *testing.T) {
|
||||
payload := []byte("hello")
|
||||
var wire bytes.Buffer
|
||||
stream := newPacketStream(bytes.NewReader(nil), &wire, true)
|
||||
|
||||
written, err := stream.Write(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("write payload: %v", err)
|
||||
}
|
||||
if written != len(payload) {
|
||||
t.Fatalf("written = %d, want %d", written, len(payload))
|
||||
}
|
||||
wantOutbound := []byte{0x0f, 0x02, 0x08, 'x', 'm', 'c', ':', 'd', 'a', 't', 'a', 'h', 'e', 'l', 'l', 'o'}
|
||||
if !bytes.Equal(wire.Bytes(), wantOutbound) {
|
||||
t.Fatalf("wire frame = %x, want %x", wire.Bytes(), wantOutbound)
|
||||
}
|
||||
|
||||
wantInbound := append([]byte(nil), wantOutbound...)
|
||||
wantInbound[1] = configurationClientboundCustomPayload
|
||||
reader := newPacketStream(bytes.NewReader(wantInbound), io.Discard, true)
|
||||
got := make([]byte, len(payload))
|
||||
if _, err = io.ReadFull(reader, got); err != nil {
|
||||
t.Fatalf("read payload: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, payload) {
|
||||
t.Fatalf("payload = %q, want %q", got, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketStreamRoundTrip(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
const password = "packet-stream-shared-key"
|
||||
privateKey, publicKey := deriveTestRSAKey(t, password)
|
||||
profiles := []loginProfile{testLoginProfile("packet_user")}
|
||||
clientPayload := bytes.Repeat([]byte("client-payload-"), 5000)
|
||||
serverPayload := bytes.Repeat([]byte("server-payload-"), 5000)
|
||||
serverDone := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
rawConn, acceptErr := ln.Accept()
|
||||
if acceptErr != nil {
|
||||
serverDone <- acceptErr
|
||||
return
|
||||
}
|
||||
defer rawConn.Close()
|
||||
|
||||
server, wrapErr := wrapConnServer(rawConn, profiles, password, privateKey, publicKey)
|
||||
if wrapErr != nil {
|
||||
serverDone <- wrapErr
|
||||
return
|
||||
}
|
||||
got := make([]byte, len(clientPayload))
|
||||
if _, readErr := io.ReadFull(server, got); readErr != nil {
|
||||
serverDone <- readErr
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(got, clientPayload) {
|
||||
serverDone <- io.ErrUnexpectedEOF
|
||||
return
|
||||
}
|
||||
_, writeErr := server.Write(serverPayload)
|
||||
serverDone <- writeErr
|
||||
}()
|
||||
|
||||
rawClient, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rawClient.Close()
|
||||
|
||||
client, err := newClientConn(rawClient, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = client.Write(clientPayload); err != nil {
|
||||
t.Fatalf("write payload: %v", err)
|
||||
}
|
||||
got := make([]byte, len(serverPayload))
|
||||
if _, err = io.ReadFull(client, got); err != nil {
|
||||
t.Fatalf("read payload: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, serverPayload) {
|
||||
t.Fatal("server payload mismatch")
|
||||
}
|
||||
if err = <-serverDone; err != nil {
|
||||
t.Fatalf("server: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"time"
|
||||
)
|
||||
|
||||
type paddingDirection uint8
|
||||
|
||||
const (
|
||||
paddingClientToServer paddingDirection = iota + 1
|
||||
paddingServerToClient
|
||||
|
||||
paddingBufferLength = 16 * 1024
|
||||
maxPaddingChunkLength = 48 * 1024
|
||||
maxPaddingTurnLength = 8 * 1024 * 1024
|
||||
)
|
||||
|
||||
type paddingVariant struct {
|
||||
chunks []int
|
||||
delays []paddingDelayRange
|
||||
}
|
||||
|
||||
type paddingDelayRange struct {
|
||||
min time.Duration
|
||||
max time.Duration
|
||||
}
|
||||
|
||||
type paddingTurn struct {
|
||||
direction paddingDirection
|
||||
minLength int
|
||||
maxLength int
|
||||
variants []paddingVariant
|
||||
startDelay paddingDelayRange
|
||||
chunkDelay paddingDelayRange
|
||||
writeChunkMinLength int
|
||||
writeChunkLength int
|
||||
sendMinLength int
|
||||
sendMaxLength int
|
||||
sendVariants []int
|
||||
}
|
||||
|
||||
func runPaddingSchedule(reader io.Reader, writer io.Writer, isClient bool, firstTurnPrefixLength int, schedule []paddingTurn) error {
|
||||
if err := validatePaddingSchedule(schedule, firstTurnPrefixLength); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var writeBuffer []byte
|
||||
for i, turn := range schedule {
|
||||
prefixLength := 0
|
||||
if i == 0 {
|
||||
prefixLength = firstTurnPrefixLength
|
||||
}
|
||||
|
||||
localSends := isClient == (turn.direction == paddingClientToServer)
|
||||
if localSends {
|
||||
if err := writePaddingTurnWithBuffer(writer, turn, prefixLength, time.Sleep, &writeBuffer); err != nil {
|
||||
return fmt.Errorf("write padding turn %d: %w", i, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := readPaddingTurn(reader, turn, prefixLength); err != nil {
|
||||
return fmt.Errorf("read padding turn %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePaddingSchedule(schedule []paddingTurn, firstTurnPrefixLength int) error {
|
||||
if len(schedule) == 0 {
|
||||
return fmt.Errorf("empty padding schedule")
|
||||
}
|
||||
if firstTurnPrefixLength < 0 {
|
||||
return fmt.Errorf("negative first turn prefix length: %d", firstTurnPrefixLength)
|
||||
}
|
||||
if firstTurnPrefixLength > 0 && schedule[0].direction != paddingClientToServer {
|
||||
return fmt.Errorf("first prefixed padding turn is not client-to-server")
|
||||
}
|
||||
|
||||
for i, turn := range schedule {
|
||||
if turn.direction != paddingClientToServer && turn.direction != paddingServerToClient {
|
||||
return fmt.Errorf("padding turn %d has invalid direction: %d", i, turn.direction)
|
||||
}
|
||||
if err := validatePaddingDelayRange(turn.startDelay); err != nil {
|
||||
return fmt.Errorf("padding turn %d has an invalid start delay: %w", i, err)
|
||||
}
|
||||
if err := validatePaddingDelayRange(turn.chunkDelay); err != nil {
|
||||
return fmt.Errorf("padding turn %d has an invalid chunk delay: %w", i, err)
|
||||
}
|
||||
if turn.writeChunkMinLength < 0 || turn.writeChunkLength < turn.writeChunkMinLength || turn.writeChunkLength > maxPaddingChunkLength {
|
||||
return fmt.Errorf("padding turn %d has an invalid write chunk range: %d-%d", i, turn.writeChunkMinLength, turn.writeChunkLength)
|
||||
}
|
||||
if len(turn.variants) > 0 && turn.writeChunkLength != 0 {
|
||||
return fmt.Errorf("padding turn %d combines variants with generated write chunks", i)
|
||||
}
|
||||
|
||||
minLength, maxLength, err := paddingTurnBounds(turn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("padding turn %d: %w", i, err)
|
||||
}
|
||||
hasSendRange := turn.sendMinLength != 0 || turn.sendMaxLength != 0
|
||||
if hasSendRange {
|
||||
if len(turn.variants) > 0 {
|
||||
return fmt.Errorf("padding turn %d combines variants with a send range", i)
|
||||
}
|
||||
if turn.sendMinLength < minLength || turn.sendMaxLength < turn.sendMinLength || turn.sendMaxLength > maxLength {
|
||||
return fmt.Errorf("padding turn %d has an invalid send range: %d-%d", i, turn.sendMinLength, turn.sendMaxLength)
|
||||
}
|
||||
}
|
||||
if i == 0 && minLength-firstTurnPrefixLength < 1 {
|
||||
return fmt.Errorf("padding turn 0 is too short for %d prefix bytes", firstTurnPrefixLength)
|
||||
}
|
||||
if i == 0 && len(turn.variants) > 0 {
|
||||
for j, variant := range turn.variants {
|
||||
if _, _, err = trimPaddingPrefix(variant, firstTurnPrefixLength); err != nil {
|
||||
return fmt.Errorf("padding turn 0 variant %d: %w", j, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if i > 0 && turn.direction == schedule[i-1].direction {
|
||||
return fmt.Errorf("padding turns %d and %d have the same direction", i-1, i)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writePaddingTurn(w io.Writer, turn paddingTurn, prefixLength int) error {
|
||||
return writePaddingTurnWithSleep(w, turn, prefixLength, time.Sleep)
|
||||
}
|
||||
|
||||
func writePaddingTurnWithSleep(w io.Writer, turn paddingTurn, prefixLength int, sleep func(time.Duration)) error {
|
||||
return writePaddingTurnWithBuffer(w, turn, prefixLength, sleep, nil)
|
||||
}
|
||||
|
||||
func writePaddingTurnWithBuffer(w io.Writer, turn paddingTurn, prefixLength int, sleep func(time.Duration), reusableBuffer *[]byte) error {
|
||||
startDelay, err := randomPaddingDelay(turn.startDelay)
|
||||
if err != nil {
|
||||
return fmt.Errorf("select padding start delay: %w", err)
|
||||
}
|
||||
if startDelay > 0 {
|
||||
sleep(startDelay)
|
||||
}
|
||||
|
||||
targetLength, chunks, delays, err := selectPaddingVariant(turn, prefixLength)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recordLength := targetLength - prefixLength
|
||||
if recordLength < 1 {
|
||||
return fmt.Errorf("target length %d leaves an invalid record length %d", targetLength, recordLength)
|
||||
}
|
||||
|
||||
encodedLength := Varint(recordLength)
|
||||
var header bytes.Buffer
|
||||
if err = encodedLength.writeTo(&header); err != nil {
|
||||
return fmt.Errorf("write padding header: %w", err)
|
||||
}
|
||||
if len(chunks) == 0 {
|
||||
writeChunkLength := turn.writeChunkLength
|
||||
if writeChunkLength == 0 {
|
||||
writeChunkLength = paddingBufferLength
|
||||
} else if turn.writeChunkMinLength > 0 {
|
||||
writeChunkLength, err = randomPaddingTarget(turn.writeChunkMinLength, writeChunkLength)
|
||||
if err != nil {
|
||||
return fmt.Errorf("select padding write chunk length: %w", err)
|
||||
}
|
||||
}
|
||||
chunks = defaultPaddingChunks(recordLength, writeChunkLength)
|
||||
delays = make([]paddingDelayRange, len(chunks))
|
||||
for i := 1; i < len(delays); i++ {
|
||||
delays[i] = turn.chunkDelay
|
||||
}
|
||||
}
|
||||
if chunks[0] < header.Len() {
|
||||
return fmt.Errorf("first padding chunk %d is shorter than header %d", chunks[0], header.Len())
|
||||
}
|
||||
|
||||
maxChunkLength := 0
|
||||
for _, chunkLength := range chunks {
|
||||
if chunkLength < 1 || chunkLength > maxPaddingChunkLength {
|
||||
return fmt.Errorf("invalid padding chunk length: %d", chunkLength)
|
||||
}
|
||||
maxChunkLength = max(maxChunkLength, chunkLength)
|
||||
}
|
||||
var buffer []byte
|
||||
if reusableBuffer == nil {
|
||||
buffer = make([]byte, maxChunkLength)
|
||||
} else {
|
||||
if cap(*reusableBuffer) < maxChunkLength {
|
||||
*reusableBuffer = make([]byte, maxChunkLength)
|
||||
}
|
||||
buffer = (*reusableBuffer)[:maxChunkLength]
|
||||
clear(buffer)
|
||||
}
|
||||
copy(buffer, header.Bytes())
|
||||
written := 0
|
||||
for i, chunkLength := range chunks {
|
||||
if i < len(delays) {
|
||||
delay, delayErr := randomPaddingDelay(delays[i])
|
||||
if delayErr != nil {
|
||||
return fmt.Errorf("select padding chunk %d delay: %w", i, delayErr)
|
||||
}
|
||||
if delay > 0 {
|
||||
sleep(delay)
|
||||
}
|
||||
}
|
||||
if err = writeFull(w, buffer[:chunkLength]); err != nil {
|
||||
return fmt.Errorf("write padding chunk %d: %w", i, err)
|
||||
}
|
||||
written += chunkLength
|
||||
if i == 0 {
|
||||
clear(buffer[:header.Len()])
|
||||
}
|
||||
}
|
||||
if written != recordLength {
|
||||
return fmt.Errorf("padding chunks total %d, want %d", written, recordLength)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readPaddingTurn(r io.Reader, turn paddingTurn, prefixLength int) error {
|
||||
encodedLength, headerLength, err := readVarintWithLength(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read padding header: %w", err)
|
||||
}
|
||||
recordLength := int(encodedLength)
|
||||
if recordLength < headerLength || recordLength > maxPaddingTurnLength {
|
||||
return fmt.Errorf("invalid padding record length: %d", recordLength)
|
||||
}
|
||||
totalLength := prefixLength + recordLength
|
||||
if !paddingTurnAcceptsLength(turn, totalLength) {
|
||||
if len(turn.variants) > 0 {
|
||||
return fmt.Errorf("padding turn length %d is not an allowed variant", totalLength)
|
||||
}
|
||||
return fmt.Errorf("padding turn length %d is outside %d-%d", totalLength, turn.minLength, turn.maxLength)
|
||||
}
|
||||
|
||||
var buffer [paddingBufferLength]byte
|
||||
remaining := recordLength - headerLength
|
||||
for remaining > 0 {
|
||||
chunkLength := min(remaining, len(buffer))
|
||||
if _, err := io.ReadFull(r, buffer[:chunkLength]); err != nil {
|
||||
return fmt.Errorf("read padding body: %w", err)
|
||||
}
|
||||
remaining -= chunkLength
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func selectPaddingVariant(turn paddingTurn, prefixLength int) (int, []int, []paddingDelayRange, error) {
|
||||
if len(turn.variants) == 0 {
|
||||
minimum, maximum := turn.minLength, turn.maxLength
|
||||
if turn.sendMinLength != 0 || turn.sendMaxLength != 0 {
|
||||
minimum, maximum = turn.sendMinLength, turn.sendMaxLength
|
||||
}
|
||||
targetLength, err := randomPaddingTarget(minimum, maximum)
|
||||
return targetLength, nil, nil, err
|
||||
}
|
||||
|
||||
indices := turn.sendVariants
|
||||
if len(indices) == 0 {
|
||||
indices = make([]int, len(turn.variants))
|
||||
for i := range indices {
|
||||
indices[i] = i
|
||||
}
|
||||
}
|
||||
selected, err := randomPaddingIndex(len(indices))
|
||||
if err != nil {
|
||||
return 0, nil, nil, err
|
||||
}
|
||||
variantIndex := indices[selected]
|
||||
if variantIndex < 0 || variantIndex >= len(turn.variants) {
|
||||
return 0, nil, nil, fmt.Errorf("invalid send variant index: %d", variantIndex)
|
||||
}
|
||||
variant := turn.variants[variantIndex]
|
||||
targetLength := paddingVariantLength(variant)
|
||||
chunks, delays, err := trimPaddingPrefix(variant, prefixLength)
|
||||
if err != nil {
|
||||
return 0, nil, nil, err
|
||||
}
|
||||
return targetLength, chunks, delays, nil
|
||||
}
|
||||
|
||||
func trimPaddingPrefix(variant paddingVariant, prefixLength int) ([]int, []paddingDelayRange, error) {
|
||||
remainingPrefix := prefixLength
|
||||
firstChunk := 0
|
||||
for firstChunk < len(variant.chunks) && remainingPrefix > 0 {
|
||||
chunkLength := variant.chunks[firstChunk]
|
||||
if remainingPrefix < chunkLength {
|
||||
return nil, nil, fmt.Errorf("prefix length %d splits chunk %d", prefixLength, firstChunk)
|
||||
}
|
||||
remainingPrefix -= chunkLength
|
||||
firstChunk++
|
||||
}
|
||||
if remainingPrefix != 0 || firstChunk == len(variant.chunks) {
|
||||
return nil, nil, fmt.Errorf("prefix length %d leaves no padding record", prefixLength)
|
||||
}
|
||||
|
||||
chunks := append([]int(nil), variant.chunks[firstChunk:]...)
|
||||
delays := make([]paddingDelayRange, len(chunks))
|
||||
if len(variant.delays) > 0 {
|
||||
copy(delays, variant.delays[firstChunk:])
|
||||
}
|
||||
return chunks, delays, nil
|
||||
}
|
||||
|
||||
func defaultPaddingChunks(recordLength, writeChunkLength int) []int {
|
||||
chunks := make([]int, 0, (recordLength+writeChunkLength-1)/writeChunkLength)
|
||||
for remaining := recordLength; remaining > 0; {
|
||||
chunkLength := min(remaining, writeChunkLength)
|
||||
chunks = append(chunks, chunkLength)
|
||||
remaining -= chunkLength
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
func paddingTurnBounds(turn paddingTurn) (int, int, error) {
|
||||
if len(turn.variants) == 0 {
|
||||
if turn.minLength < 1 || turn.maxLength < turn.minLength || turn.maxLength > maxPaddingTurnLength {
|
||||
return 0, 0, fmt.Errorf("invalid range: %d-%d", turn.minLength, turn.maxLength)
|
||||
}
|
||||
return turn.minLength, turn.maxLength, nil
|
||||
}
|
||||
if turn.minLength != 0 || turn.maxLength != 0 {
|
||||
return 0, 0, fmt.Errorf("variants cannot be combined with a length range")
|
||||
}
|
||||
|
||||
minLength := maxPaddingTurnLength + 1
|
||||
maxLength := 0
|
||||
for i, variant := range turn.variants {
|
||||
if len(variant.chunks) == 0 {
|
||||
return 0, 0, fmt.Errorf("variant %d has no chunks", i)
|
||||
}
|
||||
if len(variant.delays) != 0 && len(variant.delays) != len(variant.chunks) {
|
||||
return 0, 0, fmt.Errorf("variant %d has %d chunks and %d delays", i, len(variant.chunks), len(variant.delays))
|
||||
}
|
||||
for j, chunkLength := range variant.chunks {
|
||||
if chunkLength < 1 || chunkLength > maxPaddingChunkLength {
|
||||
return 0, 0, fmt.Errorf("variant %d chunk %d has invalid length: %d", i, j, chunkLength)
|
||||
}
|
||||
if len(variant.delays) > 0 {
|
||||
if err := validatePaddingDelayRange(variant.delays[j]); err != nil {
|
||||
return 0, 0, fmt.Errorf("variant %d chunk %d has an invalid delay: %w", i, j, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
length := paddingVariantLength(variant)
|
||||
if length > maxPaddingTurnLength {
|
||||
return 0, 0, fmt.Errorf("variant %d is too long: %d", i, length)
|
||||
}
|
||||
minLength = min(minLength, length)
|
||||
maxLength = max(maxLength, length)
|
||||
}
|
||||
for _, index := range turn.sendVariants {
|
||||
if index < 0 || index >= len(turn.variants) {
|
||||
return 0, 0, fmt.Errorf("invalid send variant index: %d", index)
|
||||
}
|
||||
}
|
||||
return minLength, maxLength, nil
|
||||
}
|
||||
|
||||
func paddingTurnAcceptsLength(turn paddingTurn, length int) bool {
|
||||
if len(turn.variants) == 0 {
|
||||
return length >= turn.minLength && length <= turn.maxLength
|
||||
}
|
||||
for _, variant := range turn.variants {
|
||||
if paddingVariantLength(variant) == length {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func paddingVariantLength(variant paddingVariant) int {
|
||||
total := 0
|
||||
for _, chunkLength := range variant.chunks {
|
||||
total += chunkLength
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func validatePaddingDelayRange(delay paddingDelayRange) error {
|
||||
if delay.min < 0 || delay.max < delay.min {
|
||||
return fmt.Errorf("invalid range: %s-%s", delay.min, delay.max)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func randomPaddingDelay(delay paddingDelayRange) (time.Duration, error) {
|
||||
if err := validatePaddingDelayRange(delay); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if delay.min == delay.max {
|
||||
return delay.min, nil
|
||||
}
|
||||
span := int64(delay.max-delay.min) + 1
|
||||
offset, err := rand.Int(rand.Reader, big.NewInt(span))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("select padding delay: %w", err)
|
||||
}
|
||||
return delay.min + time.Duration(offset.Int64()), nil
|
||||
}
|
||||
|
||||
func randomPaddingIndex(length int) (int, error) {
|
||||
if length < 1 {
|
||||
return 0, fmt.Errorf("select from empty padding choices")
|
||||
}
|
||||
if length == 1 {
|
||||
return 0, nil
|
||||
}
|
||||
index, err := rand.Int(rand.Reader, big.NewInt(int64(length)))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("select padding choice: %w", err)
|
||||
}
|
||||
return int(index.Int64()), nil
|
||||
}
|
||||
|
||||
func randomPaddingTarget(minLength, maxLength int) (int, error) {
|
||||
if minLength == maxLength {
|
||||
return minLength, nil
|
||||
}
|
||||
span := int64(maxLength-minLength) + 1
|
||||
offset, err := rand.Int(rand.Reader, big.NewInt(span))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("select padding length: %w", err)
|
||||
}
|
||||
return minLength + int(offset.Int64()), nil
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Length and write-boundary templates come from controlled Minecraft 26.1.2
|
||||
// logins. Timing deliberately uses broad random bands that preserve only the
|
||||
// rough ordering of short and long phases; it does not replay captured delays.
|
||||
var startupPaddingSchedule2612 = []paddingTurn{
|
||||
{
|
||||
direction: paddingClientToServer,
|
||||
variants: []paddingVariant{
|
||||
paddingVariantFromChunks(2, 26, 16),
|
||||
},
|
||||
},
|
||||
{
|
||||
direction: paddingServerToClient,
|
||||
variants: []paddingVariant{
|
||||
paddingVariantFromChunks(26, 21, 25),
|
||||
},
|
||||
startDelay: millisecondRange(0, 20),
|
||||
},
|
||||
{
|
||||
direction: paddingClientToServer,
|
||||
variants: []paddingVariant{
|
||||
paddingVariantFromChunks(25),
|
||||
},
|
||||
startDelay: millisecondRange(2, 22),
|
||||
},
|
||||
{
|
||||
direction: paddingServerToClient,
|
||||
variants: []paddingVariant{
|
||||
registryPaddingVariant(),
|
||||
},
|
||||
startDelay: millisecondRange(20, 50),
|
||||
},
|
||||
{
|
||||
direction: paddingClientToServer,
|
||||
variants: []paddingVariant{
|
||||
paddingVariantFromChunks(2),
|
||||
},
|
||||
startDelay: millisecondRange(10, 35),
|
||||
},
|
||||
{
|
||||
direction: paddingServerToClient,
|
||||
variants: []paddingVariant{
|
||||
playStartPaddingVariant(4941, 252, 259, 267, 268, 251, 303, 259, 264, 54, 346),
|
||||
playStartPaddingVariant(4941, 262, 284, 272, 260, 260, 313, 264, 151, 224, 207, 215, 224, 390),
|
||||
playStartPaddingVariant(4941, 257, 272, 275, 260, 260, 313, 283, 274, 226, 207, 230, 215, 204, 221, 352),
|
||||
playStartPaddingVariant(4941, 259, 272, 288, 260, 260, 311, 270, 70, 236, 223, 201, 210, 352),
|
||||
playStartPaddingVariant(4941, 255, 269, 277, 263, 260, 136, 207, 210, 232, 325),
|
||||
playStartPaddingVariant(4941, 259, 270, 274, 263, 258, 327, 170, 210, 375),
|
||||
playStartPaddingVariant(4941, 257, 275, 291, 260, 260, 325, 269, 70, 230, 226, 207, 221, 352),
|
||||
playStartPaddingVariant(4941, 252, 273, 262, 252, 254, 306, 93),
|
||||
playStartPaddingVariant(4941, 273, 270, 269, 258, 256, 322, 221, 207, 215, 438),
|
||||
playStartPaddingVariant(4941, 259, 275, 274, 250, 258, 308, 267, 154, 233, 209, 207, 213, 393),
|
||||
playStartPaddingVariant(4941, 254, 267, 272, 260, 253, 311, 167, 204, 232, 207, 481, 8),
|
||||
playStartPaddingVariant(4941, 259, 269, 272, 261, 313, 207, 213, 500, 19),
|
||||
playStartPaddingVariant(4941, 262, 269, 274, 263, 274, 311, 270, 242, 210, 229, 221, 210, 431),
|
||||
playStartPaddingVariant(4941, 259, 265, 277, 263, 277, 316, 269, 156, 204, 210, 226, 207, 413),
|
||||
playStartPaddingVariant(4941, 215, 251, 249, 317, 260, 270, 249, 52),
|
||||
playStartPaddingVariant(4941, 224, 263, 277, 316, 267, 272, 260, 138, 230, 226, 207, 204, 352),
|
||||
playStartPaddingVariant(4941, 221, 258, 263, 319, 269, 288, 263, 136, 204, 210, 220, 207, 378),
|
||||
playStartPaddingVariant(4941, 221, 258, 260, 316, 273, 291, 226, 204, 229, 213, 489, 8),
|
||||
playStartPaddingVariant(4941, 238, 260, 261, 306, 272, 277, 260, 224, 241, 212, 207, 204, 393),
|
||||
playStartPaddingVariant(4941, 224, 260, 260, 309, 272, 277, 277, 138, 207, 207, 212, 241, 352),
|
||||
},
|
||||
startDelay: millisecondRange(35, 50),
|
||||
},
|
||||
}
|
||||
|
||||
// These turns cover the finite Play-state tail through the client's
|
||||
// player_loaded packet. Bounds are the observed per-turn minima and maxima
|
||||
// across 20 controlled 26.1.2 logins; payload bytes remain opaque padding.
|
||||
var playJoinPaddingSchedule2612 = []paddingTurn{
|
||||
clientPlayPaddingTurn(6, 883),
|
||||
serverPlayPaddingTurn(346, 58638),
|
||||
clientPlayPaddingTurn(6, 887),
|
||||
serverPlayPaddingTurn(388, 61077),
|
||||
clientPlayPaddingTurn(2, 50),
|
||||
serverPlayPaddingTurn(575, 65584),
|
||||
clientPlayPaddingTurn(6, 45),
|
||||
serverPlayPaddingTurn(86, 63563),
|
||||
clientPlayPaddingTurn(2, 44),
|
||||
serverPlayPaddingTurn(42, 51983),
|
||||
clientPlayPaddingTurn(2, 851),
|
||||
serverPlayPaddingTurn(309, 25083),
|
||||
clientPlayPaddingTurn(2, 19),
|
||||
serverPlayPaddingTurn(74, 63885),
|
||||
clientPlayPaddingTurn(8, 24),
|
||||
serverPlayPaddingTurn(30, 66128),
|
||||
clientPlayPaddingTurn(2, 19),
|
||||
serverPlayPaddingTurn(26, 35818),
|
||||
clientPlayPaddingTurn(6, 19),
|
||||
serverPlayPaddingTurn(35, 59407),
|
||||
clientPlayPaddingTurn(6, 19),
|
||||
serverPlayPaddingTurn(37, 65328),
|
||||
clientPlayPaddingTurn(2, 19),
|
||||
serverPlayPaddingTurn(26, 60622),
|
||||
clientPlayPaddingTurn(6, 19),
|
||||
serverPlayPaddingTurn(11, 60808),
|
||||
clientPlayPaddingTurn(8, 43),
|
||||
serverPlayPaddingTurn(55, 62027),
|
||||
clientPlayPaddingTurn(2, 19),
|
||||
serverPlayPaddingTurn(427, 65622),
|
||||
clientPlayPaddingTurn(5, 19),
|
||||
serverPlayPaddingTurn(35, 59401),
|
||||
clientPlayPaddingTurn(6, 19),
|
||||
}
|
||||
|
||||
type paddingLengthRange2612 struct {
|
||||
minimum int
|
||||
maximum int
|
||||
}
|
||||
|
||||
type serverPlayLengthBranches2612 struct {
|
||||
small paddingLengthRange2612
|
||||
large paddingLengthRange2612
|
||||
}
|
||||
|
||||
var serverPlayBranches2612 = []serverPlayLengthBranches2612{
|
||||
{small: paddingLengthRange2612{346, 18812}, large: paddingLengthRange2612{51702, 58638}},
|
||||
{small: paddingLengthRange2612{388, 20689}, large: paddingLengthRange2612{51445, 61077}},
|
||||
{small: paddingLengthRange2612{575, 20915}, large: paddingLengthRange2612{41428, 65584}},
|
||||
{small: paddingLengthRange2612{86, 2772}, large: paddingLengthRange2612{41428, 63563}},
|
||||
{small: paddingLengthRange2612{42, 26813}, large: paddingLengthRange2612{51983, 51983}},
|
||||
{small: paddingLengthRange2612{309, 19484}, large: paddingLengthRange2612{24837, 25083}},
|
||||
{small: paddingLengthRange2612{74, 40686}, large: paddingLengthRange2612{63885, 63885}},
|
||||
{small: paddingLengthRange2612{30, 44114}, large: paddingLengthRange2612{66128, 66128}},
|
||||
{small: paddingLengthRange2612{26, 1464}, large: paddingLengthRange2612{9941, 35818}},
|
||||
{small: paddingLengthRange2612{35, 42885}, large: paddingLengthRange2612{52194, 59407}},
|
||||
{small: paddingLengthRange2612{37, 47553}, large: paddingLengthRange2612{61765, 65328}},
|
||||
{small: paddingLengthRange2612{26, 1121}, large: paddingLengthRange2612{16162, 60622}},
|
||||
{small: paddingLengthRange2612{11, 45629}, large: paddingLengthRange2612{60808, 60808}},
|
||||
{small: paddingLengthRange2612{55, 10035}, large: paddingLengthRange2612{30237, 62027}},
|
||||
{small: paddingLengthRange2612{427, 52536}, large: paddingLengthRange2612{64014, 65622}},
|
||||
{small: paddingLengthRange2612{35, 22708}, large: paddingLengthRange2612{38987, 59401}},
|
||||
}
|
||||
|
||||
// Each mask preserves only the small/large branch order from one baseline
|
||||
// login. Actual lengths and timing are selected randomly inside each branch.
|
||||
var serverPlayBranchMasks2612 = []uint32{
|
||||
0x011c, 0x090a, 0x0821, 0xe921, 0x2102,
|
||||
0x0844, 0xa101, 0x1106, 0x2e00, 0xab01,
|
||||
0xe900, 0xac01, 0xab01, 0x8b80, 0x0808,
|
||||
0x2001, 0x0901, 0x000a, 0x2c01, 0x0801,
|
||||
}
|
||||
|
||||
type clientPlayBurst2612 struct {
|
||||
playIndex int
|
||||
regular paddingLengthRange2612
|
||||
burst paddingLengthRange2612
|
||||
}
|
||||
|
||||
var clientPlayBursts2612 = []clientPlayBurst2612{
|
||||
{playIndex: 0, regular: paddingLengthRange2612{6, 44}, burst: paddingLengthRange2612{877, 883}},
|
||||
{playIndex: 2, regular: paddingLengthRange2612{6, 45}, burst: paddingLengthRange2612{884, 887}},
|
||||
{playIndex: 10, regular: paddingLengthRange2612{2, 19}, burst: paddingLengthRange2612{851, 851}},
|
||||
}
|
||||
|
||||
// The 20 samples placed the one client initialization burst in these slots.
|
||||
var clientPlayBurstChoices2612 = []int{
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 1,
|
||||
2,
|
||||
}
|
||||
|
||||
var paddingSchedule2612 = buildPaddingSchedule2612()
|
||||
|
||||
func buildPaddingSchedule2612() []paddingTurn {
|
||||
schedule := make([]paddingTurn, 0, len(startupPaddingSchedule2612)+len(playJoinPaddingSchedule2612))
|
||||
schedule = append(schedule, startupPaddingSchedule2612...)
|
||||
schedule = append(schedule, playJoinPaddingSchedule2612...)
|
||||
return schedule
|
||||
}
|
||||
|
||||
func clientPlayPaddingTurn(minimum, maximum int) paddingTurn {
|
||||
return paddingTurn{
|
||||
direction: paddingClientToServer,
|
||||
minLength: minimum,
|
||||
maxLength: maximum,
|
||||
startDelay: millisecondRange(1, 30),
|
||||
writeChunkLength: 1024,
|
||||
}
|
||||
}
|
||||
|
||||
func serverPlayPaddingTurn(minimum, maximum int) paddingTurn {
|
||||
return paddingTurn{
|
||||
direction: paddingServerToClient,
|
||||
minLength: minimum,
|
||||
maxLength: maximum,
|
||||
startDelay: millisecondRange(1, 45),
|
||||
chunkDelay: millisecondRange(1, 4),
|
||||
writeChunkMinLength: 32 * 1024,
|
||||
writeChunkLength: maxPaddingChunkLength,
|
||||
}
|
||||
}
|
||||
|
||||
func newClientPaddingSchedule2612() ([]paddingTurn, error) {
|
||||
choice, err := randomPaddingIndex(len(clientPlayBurstChoices2612))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selectedBurst := clientPlayBurstChoices2612[choice]
|
||||
schedule := append([]paddingTurn(nil), paddingSchedule2612...)
|
||||
for i, burst := range clientPlayBursts2612 {
|
||||
lengthRange := burst.regular
|
||||
if i == selectedBurst {
|
||||
lengthRange = burst.burst
|
||||
}
|
||||
turn := &schedule[len(startupPaddingSchedule2612)+burst.playIndex]
|
||||
turn.sendMinLength = lengthRange.minimum
|
||||
turn.sendMaxLength = lengthRange.maximum
|
||||
}
|
||||
return schedule, nil
|
||||
}
|
||||
|
||||
type paddingPause struct {
|
||||
chunk int
|
||||
delay paddingDelayRange
|
||||
}
|
||||
|
||||
func paddingVariantFromChunks(chunks ...int) paddingVariant {
|
||||
return paddingVariant{chunks: chunks}
|
||||
}
|
||||
|
||||
func pacedPaddingVariant(chunks []int, pauses ...paddingPause) paddingVariant {
|
||||
delays := make([]paddingDelayRange, len(chunks))
|
||||
for _, pause := range pauses {
|
||||
if pause.chunk < 0 || pause.chunk >= len(delays) {
|
||||
panic("xmc: padding pause index is outside its chunk template")
|
||||
}
|
||||
delays[pause.chunk] = pause.delay
|
||||
}
|
||||
return paddingVariant{chunks: chunks, delays: delays}
|
||||
}
|
||||
|
||||
func registryPaddingVariant() paddingVariant {
|
||||
return pacedPaddingVariant(
|
||||
[]int{1590, 226, 329, 229, 186, 151, 78, 81, 79, 235, 67, 67, 78, 71, 82, 74, 982, 117, 1118, 1038, 970, 400, 239, 49, 50, 95, 65, 104, 32320, 2},
|
||||
paddingPause{28, millisecondRange(1, 4)},
|
||||
paddingPause{29, millisecondRange(44, 61)},
|
||||
)
|
||||
}
|
||||
|
||||
func playStartPaddingVariant(chunks ...int) paddingVariant {
|
||||
if len(chunks) < 2 {
|
||||
panic("xmc: play start padding variant needs at least two chunks")
|
||||
}
|
||||
return pacedPaddingVariant(
|
||||
chunks,
|
||||
paddingPause{len(chunks) / 2, millisecondRange(1, 5)},
|
||||
paddingPause{len(chunks) - 1, millisecondRange(9, 20)},
|
||||
)
|
||||
}
|
||||
|
||||
func millisecondRange(minimum, maximum int) paddingDelayRange {
|
||||
return paddingDelayRange{
|
||||
min: time.Duration(minimum) * time.Millisecond,
|
||||
max: time.Duration(maximum) * time.Millisecond,
|
||||
}
|
||||
}
|
||||
|
||||
func newServerPaddingSchedule2612() ([]paddingTurn, error) {
|
||||
schedule := append([]paddingTurn(nil), paddingSchedule2612...)
|
||||
profileIndex, err := randomPaddingIndex(len(serverPlayBranchMasks2612))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile := serverPlayBranchMasks2612[profileIndex]
|
||||
for i, branches := range serverPlayBranches2612 {
|
||||
lengthRange := branches.small
|
||||
if profile&(1<<i) != 0 {
|
||||
lengthRange = branches.large
|
||||
}
|
||||
turn := &schedule[len(startupPaddingSchedule2612)+1+i*2]
|
||||
turn.sendMinLength = lengthRange.minimum
|
||||
turn.sendMaxLength = lengthRange.maximum
|
||||
}
|
||||
return schedule, nil
|
||||
}
|
||||
@@ -0,0 +1,618 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPaddingTurnReachesFinalTargetLength(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 128, maxLength: 128}
|
||||
const prefixLength = 3
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, prefixLength); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := prefixLength + encoded.Len(); got != turn.minLength {
|
||||
t.Fatalf("total turn length = %d, want %d", got, turn.minLength)
|
||||
}
|
||||
encodedReader := bytes.NewReader(encoded.Bytes())
|
||||
var recordLength Varint
|
||||
if err := recordLength.readFrom(encodedReader); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := int(recordLength); got != encoded.Len() {
|
||||
t.Fatalf("record length = %d, encoded = %d", got, encoded.Len())
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, prefixLength); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingTurnSupportsThreeByteTarget(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 3, maxLength: 3}
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := encoded.Len(); got != 3 {
|
||||
t.Fatalf("padding length = %d, want 3", got)
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingTurnVarintBoundaries(t *testing.T) {
|
||||
for _, targetLength := range []int{127, 128, 16383, 16384} {
|
||||
t.Run(strconv.Itoa(targetLength), func(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: targetLength, maxLength: targetLength}
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if encoded.Len() != targetLength {
|
||||
t.Fatalf("padding length = %d, want %d", encoded.Len(), targetLength)
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingTurnRandomRange(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingServerToClient, minLength: 127, maxLength: 129}
|
||||
seen := make(map[int]bool)
|
||||
for range 100 {
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if encoded.Len() < turn.minLength || encoded.Len() > turn.maxLength {
|
||||
t.Fatalf("padding length = %d", encoded.Len())
|
||||
}
|
||||
seen[encoded.Len()] = true
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("padding range did not vary: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingTurnUsesRestrictedSendRange(t *testing.T) {
|
||||
turn := paddingTurn{
|
||||
direction: paddingServerToClient,
|
||||
minLength: 3,
|
||||
maxLength: 100,
|
||||
sendMinLength: 90,
|
||||
sendMaxLength: 100,
|
||||
}
|
||||
seen := make(map[int]bool)
|
||||
for range 100 {
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if encoded.Len() < turn.sendMinLength || encoded.Len() > turn.sendMaxLength {
|
||||
t.Fatalf("padding length = %d", encoded.Len())
|
||||
}
|
||||
seen[encoded.Len()] = true
|
||||
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("restricted send range did not vary: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingScheduleSynchronizesDirections(t *testing.T) {
|
||||
client, server := net.Pipe()
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
|
||||
schedule := []paddingTurn{
|
||||
{direction: paddingClientToServer, minLength: 33, maxLength: 33},
|
||||
{direction: paddingServerToClient, minLength: 4097, maxLength: 4097},
|
||||
{direction: paddingClientToServer, minLength: 16385, maxLength: 16385},
|
||||
}
|
||||
serverDone := make(chan error, 1)
|
||||
go func() {
|
||||
serverDone <- runPaddingSchedule(server, server, false, 3, schedule)
|
||||
}()
|
||||
if err := runPaddingSchedule(client, client, true, 3, schedule); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case err := <-serverDone:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("server padding schedule did not complete")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnHandlesFragmentedInput(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 1024, maxLength: 1024}
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := readPaddingTurn(&oneByteReader{reader: bytes.NewReader(encoded.Bytes())}, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnRejectsInvalidLength(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 64, maxLength: 96}
|
||||
data := encodePaddingLength(t, 63)
|
||||
if err := readPaddingTurn(bytes.NewReader(data), turn, 0); err == nil || !strings.Contains(err.Error(), "outside") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnRejectsNonCanonicalHeader(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 3, maxLength: 3}
|
||||
err := readPaddingTurn(bytes.NewReader([]byte{0x83, 0x00, 0x00}), turn, 0)
|
||||
if err == nil || !strings.Contains(err.Error(), "non-canonical") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnRejectsTruncatedBody(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 64, maxLength: 64}
|
||||
data := encodePaddingLength(t, 64)
|
||||
if err := readPaddingTurn(bytes.NewReader(data), turn, 0); err == nil || !strings.Contains(err.Error(), "body") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnHonorsConnectionTimeout(t *testing.T) {
|
||||
client, server := net.Pipe()
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
|
||||
if err := server.SetReadDeadline(time.Now().Add(20 * time.Millisecond)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 64, maxLength: 64}
|
||||
err := readPaddingTurn(server, turn, 0)
|
||||
var netErr net.Error
|
||||
if !errors.As(err, &netErr) || !netErr.Timeout() {
|
||||
t.Fatalf("error = %v, want network timeout", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePaddingSchedule(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schedule []paddingTurn
|
||||
prefix int
|
||||
}{
|
||||
{name: "empty"},
|
||||
{name: "bad direction", schedule: []paddingTurn{{direction: 99, minLength: 4, maxLength: 4}}},
|
||||
{name: "too small", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 0, maxLength: 4}}},
|
||||
{name: "reversed range", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 7}}},
|
||||
{name: "wrong first direction", prefix: 3, schedule: []paddingTurn{{direction: paddingServerToClient, minLength: 8, maxLength: 8}}},
|
||||
{name: "prefix leaves no header", prefix: 8, schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8}}},
|
||||
{name: "same direction", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8}, {direction: paddingClientToServer, minLength: 8, maxLength: 8}}},
|
||||
{name: "range with variants", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, variants: []paddingVariant{paddingVariantFromChunks(8)}}}},
|
||||
{name: "empty variant", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{{}}}}},
|
||||
{name: "bad chunk", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(maxPaddingChunkLength + 1)}}}},
|
||||
{
|
||||
name: "delay mismatch",
|
||||
schedule: []paddingTurn{{
|
||||
direction: paddingClientToServer,
|
||||
variants: []paddingVariant{{
|
||||
chunks: []int{4, 4},
|
||||
delays: []paddingDelayRange{{min: time.Millisecond, max: time.Millisecond}},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
{name: "reversed start delay", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, startDelay: paddingDelayRange{min: 2 * time.Millisecond, max: time.Millisecond}}}},
|
||||
{name: "reversed generated chunk delay", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, chunkDelay: paddingDelayRange{min: 2 * time.Millisecond, max: time.Millisecond}}}},
|
||||
{name: "oversized generated chunk", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, writeChunkLength: maxPaddingChunkLength + 1}}},
|
||||
{name: "reversed generated chunk range", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, writeChunkMinLength: 9, writeChunkLength: 8}}},
|
||||
{name: "variant with generated chunks", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8)}, writeChunkLength: 8}}},
|
||||
{name: "send range outside accepted range", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 16, sendMinLength: 7, sendMaxLength: 12}}},
|
||||
{name: "variant with send range", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8)}, sendMinLength: 8, sendMaxLength: 8}}},
|
||||
{name: "negative chunk delay", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{{chunks: []int{8}, delays: []paddingDelayRange{{min: -time.Millisecond}}}}}}},
|
||||
{name: "bad send variant", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8)}, sendVariants: []int{1}}}},
|
||||
{name: "prefix splits chunk", prefix: 3, schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8, 4)}}}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if err := validatePaddingSchedule(test.schedule, test.prefix); err == nil {
|
||||
t.Fatal("expected invalid padding schedule")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingSchedule2612MatchesCapturedTemplates(t *testing.T) {
|
||||
wantDirections := []paddingDirection{
|
||||
paddingClientToServer,
|
||||
paddingServerToClient,
|
||||
paddingClientToServer,
|
||||
paddingServerToClient,
|
||||
paddingClientToServer,
|
||||
paddingServerToClient,
|
||||
}
|
||||
wantLengths := [][]int{
|
||||
{44},
|
||||
{72},
|
||||
{25},
|
||||
{41172},
|
||||
{2},
|
||||
{7464, 8267, 8790, 8153, 7375, 7347, 8184, 6633, 7670, 8241, 7857, 7254, 8407, 8283, 6804, 8177, 8177, 7929, 8296, 8177},
|
||||
}
|
||||
if len(paddingSchedule2612) != len(wantDirections)+33 {
|
||||
t.Fatalf("padding schedule has %d turns, want %d", len(paddingSchedule2612), len(wantDirections)+33)
|
||||
}
|
||||
for i, turn := range paddingSchedule2612[:len(wantDirections)] {
|
||||
if turn.direction != wantDirections[i] {
|
||||
t.Fatalf("padding turn %d direction = %d, want %d", i, turn.direction, wantDirections[i])
|
||||
}
|
||||
if len(turn.variants) != len(wantLengths[i]) {
|
||||
t.Fatalf("padding turn %d has %d variants, want %d", i, len(turn.variants), len(wantLengths[i]))
|
||||
}
|
||||
for j, variant := range turn.variants {
|
||||
if got := paddingVariantLength(variant); got != wantLengths[i][j] {
|
||||
t.Fatalf("padding turn %d variant %d length = %d, want %d", i, j, got, wantLengths[i][j])
|
||||
}
|
||||
}
|
||||
}
|
||||
wantPlayBounds := [][2]int{
|
||||
{6, 883},
|
||||
{346, 58638},
|
||||
{6, 887},
|
||||
{388, 61077},
|
||||
{2, 50},
|
||||
{575, 65584},
|
||||
{6, 45},
|
||||
{86, 63563},
|
||||
{2, 44},
|
||||
{42, 51983},
|
||||
{2, 851},
|
||||
{309, 25083},
|
||||
{2, 19},
|
||||
{74, 63885},
|
||||
{8, 24},
|
||||
{30, 66128},
|
||||
{2, 19},
|
||||
{26, 35818},
|
||||
{6, 19},
|
||||
{35, 59407},
|
||||
{6, 19},
|
||||
{37, 65328},
|
||||
{2, 19},
|
||||
{26, 60622},
|
||||
{6, 19},
|
||||
{11, 60808},
|
||||
{8, 43},
|
||||
{55, 62027},
|
||||
{2, 19},
|
||||
{427, 65622},
|
||||
{5, 19},
|
||||
{35, 59401},
|
||||
{6, 19},
|
||||
}
|
||||
for i, want := range wantPlayBounds {
|
||||
turn := paddingSchedule2612[len(wantDirections)+i]
|
||||
wantDirection := paddingClientToServer
|
||||
if i%2 == 1 {
|
||||
wantDirection = paddingServerToClient
|
||||
}
|
||||
if turn.direction != wantDirection {
|
||||
t.Fatalf("play turn %d direction = %d, want %d", i, turn.direction, wantDirection)
|
||||
}
|
||||
if turn.minLength != want[0] || turn.maxLength != want[1] {
|
||||
t.Fatalf("play turn %d bounds = %d-%d, want %d-%d", i, turn.minLength, turn.maxLength, want[0], want[1])
|
||||
}
|
||||
if len(turn.variants) != 0 {
|
||||
t.Fatalf("play turn %d unexpectedly has captured variants", i)
|
||||
}
|
||||
}
|
||||
if got := len(paddingSchedule2612[3].variants[0].chunks); got != 30 {
|
||||
t.Fatalf("registry turn chunks = %d, want 30", got)
|
||||
}
|
||||
minimumPlayStart := maxPaddingTurnLength
|
||||
maximumPlayStart := 0
|
||||
for _, variant := range paddingSchedule2612[5].variants {
|
||||
length := paddingVariantLength(variant)
|
||||
minimumPlayStart = min(minimumPlayStart, length)
|
||||
maximumPlayStart = max(maximumPlayStart, length)
|
||||
if variant.chunks[0] != 4941 {
|
||||
t.Fatalf("play start first chunk = %d, want 4941", variant.chunks[0])
|
||||
}
|
||||
}
|
||||
if minimumPlayStart != 6633 || maximumPlayStart != 8790 {
|
||||
t.Fatalf("play start bounds = %d-%d, want 6633-8790", minimumPlayStart, maximumPlayStart)
|
||||
}
|
||||
if err := validatePaddingSchedule(paddingSchedule2612, 2); err != nil {
|
||||
t.Fatalf("captured schedule is invalid: %v", err)
|
||||
}
|
||||
|
||||
serverSchedule, err := newServerPaddingSchedule2612()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = validatePaddingSchedule(serverSchedule, 2); err != nil {
|
||||
t.Fatalf("server schedule is invalid: %v", err)
|
||||
}
|
||||
for i, branches := range serverPlayBranches2612 {
|
||||
turn := serverSchedule[len(startupPaddingSchedule2612)+1+i*2]
|
||||
got := paddingLengthRange2612{turn.sendMinLength, turn.sendMaxLength}
|
||||
if got != branches.small && got != branches.large {
|
||||
t.Fatalf("server play turn %d send range = %v, want %v or %v", i, got, branches.small, branches.large)
|
||||
}
|
||||
}
|
||||
|
||||
for range 20 {
|
||||
clientSchedule, clientErr := newClientPaddingSchedule2612()
|
||||
if clientErr != nil {
|
||||
t.Fatal(clientErr)
|
||||
}
|
||||
if clientErr = validatePaddingSchedule(clientSchedule, 2); clientErr != nil {
|
||||
t.Fatalf("client schedule is invalid: %v", clientErr)
|
||||
}
|
||||
burstCount := 0
|
||||
for _, burst := range clientPlayBursts2612 {
|
||||
turn := clientSchedule[len(startupPaddingSchedule2612)+burst.playIndex]
|
||||
got := paddingLengthRange2612{turn.sendMinLength, turn.sendMaxLength}
|
||||
switch got {
|
||||
case burst.regular:
|
||||
case burst.burst:
|
||||
burstCount++
|
||||
default:
|
||||
t.Fatalf("client play turn %d send range = %v", burst.playIndex, got)
|
||||
}
|
||||
}
|
||||
if burstCount != 1 {
|
||||
t.Fatalf("client schedule has %d initialization bursts, want 1", burstCount)
|
||||
}
|
||||
}
|
||||
|
||||
for variantIndex := range paddingSchedule2612[3].variants {
|
||||
turn := paddingSchedule2612[3]
|
||||
turn.sendVariants = []int{variantIndex}
|
||||
var encoded bytes.Buffer
|
||||
if err = writePaddingTurnWithSleep(&encoded, turn, 0, func(time.Duration) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = readPaddingTurn(bytes.NewReader(encoded.Bytes()), paddingSchedule2612[3], 0); err != nil {
|
||||
t.Fatalf("registry variant %d was rejected: %v", variantIndex, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingVariantPreservesWriteBoundaries(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
turn paddingTurn
|
||||
prefix int
|
||||
want []int
|
||||
}{
|
||||
{name: "login acknowledged turn", turn: paddingSchedule2612[0], prefix: 2, want: []int{26, 16}},
|
||||
{name: "server response turn", turn: paddingSchedule2612[1], want: []int{26, 21, 25}},
|
||||
{name: "single packet turn", turn: paddingSchedule2612[2], want: []int{25}},
|
||||
{name: "fixed registry profile", turn: paddingSchedule2612[3], want: paddingSchedule2612[3].variants[0].chunks},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var writer recordingWriter
|
||||
if err := writePaddingTurnWithSleep(&writer, test.turn, test.prefix, func(time.Duration) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(writer.writes) != len(test.want) {
|
||||
t.Fatalf("writes = %v, want %v", writer.writes, test.want)
|
||||
}
|
||||
for i := range test.want {
|
||||
if writer.writes[i] != test.want[i] {
|
||||
t.Fatalf("writes = %v, want %v", writer.writes, test.want)
|
||||
}
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(writer.Bytes()), test.turn, test.prefix); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingVariantAppliesPacing(t *testing.T) {
|
||||
turn := paddingTurn{
|
||||
direction: paddingClientToServer,
|
||||
startDelay: paddingDelayRange{min: 3 * time.Millisecond, max: 3 * time.Millisecond},
|
||||
variants: []paddingVariant{{
|
||||
chunks: []int{3, 5, 7},
|
||||
delays: []paddingDelayRange{
|
||||
{},
|
||||
{min: 2 * time.Millisecond, max: 2 * time.Millisecond},
|
||||
{min: 4 * time.Millisecond, max: 4 * time.Millisecond},
|
||||
},
|
||||
}},
|
||||
}
|
||||
var slept []time.Duration
|
||||
var writer recordingWriter
|
||||
if err := writePaddingTurnWithSleep(&writer, turn, 3, func(delay time.Duration) {
|
||||
slept = append(slept, delay)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []time.Duration{3 * time.Millisecond, 2 * time.Millisecond, 4 * time.Millisecond}
|
||||
if len(slept) != len(want) {
|
||||
t.Fatalf("delays = %v, want %v", slept, want)
|
||||
}
|
||||
for i := range want {
|
||||
if slept[i] != want[i] {
|
||||
t.Fatalf("delays = %v, want %v", slept, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratedPaddingChunksApplyPacing(t *testing.T) {
|
||||
turn := paddingTurn{
|
||||
direction: paddingServerToClient,
|
||||
minLength: 100,
|
||||
maxLength: 100,
|
||||
writeChunkLength: 32,
|
||||
chunkDelay: paddingDelayRange{min: 2 * time.Millisecond, max: 2 * time.Millisecond},
|
||||
}
|
||||
var slept []time.Duration
|
||||
var writer recordingWriter
|
||||
if err := writePaddingTurnWithSleep(&writer, turn, 0, func(delay time.Duration) {
|
||||
slept = append(slept, delay)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantWrites := []int{32, 32, 32, 4}
|
||||
if !slicesEqual(writer.writes, wantWrites) {
|
||||
t.Fatalf("writes = %v, want %v", writer.writes, wantWrites)
|
||||
}
|
||||
wantSleeps := []time.Duration{2 * time.Millisecond, 2 * time.Millisecond, 2 * time.Millisecond}
|
||||
if !slicesEqual(slept, wantSleeps) {
|
||||
t.Fatalf("delays = %v, want %v", slept, wantSleeps)
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(writer.Bytes()), turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratedPaddingChunkLengthIsRandomized(t *testing.T) {
|
||||
turn := paddingTurn{
|
||||
direction: paddingServerToClient,
|
||||
minLength: 100,
|
||||
maxLength: 100,
|
||||
writeChunkMinLength: 16,
|
||||
writeChunkLength: 32,
|
||||
}
|
||||
seen := make(map[int]bool)
|
||||
for range 100 {
|
||||
var writer recordingWriter
|
||||
if err := writePaddingTurnWithSleep(&writer, turn, 0, func(time.Duration) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
firstWrite := writer.writes[0]
|
||||
if firstWrite < turn.writeChunkMinLength || firstWrite > turn.writeChunkLength {
|
||||
t.Fatalf("first write = %d", firstWrite)
|
||||
}
|
||||
seen[firstWrite] = true
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("generated write chunk length did not vary: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingDelayRangeIsRandomized(t *testing.T) {
|
||||
delayRange := millisecondRange(25, 40)
|
||||
seen := make(map[time.Duration]bool)
|
||||
for range 100 {
|
||||
delay, err := randomPaddingDelay(delayRange)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if delay < delayRange.min || delay > delayRange.max {
|
||||
t.Fatalf("delay = %s, want %s-%s", delay, delayRange.min, delayRange.max)
|
||||
}
|
||||
seen[delay] = true
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("padding delay did not vary: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingSchedule2612UsesCoarseTimingBands(t *testing.T) {
|
||||
assertDelayRange(t, "turn 3 to 4", paddingSchedule2612[3].startDelay, 20*time.Millisecond, 50*time.Millisecond)
|
||||
assertDelayRange(t, "turn 5 to 6", paddingSchedule2612[5].startDelay, 35*time.Millisecond, 50*time.Millisecond)
|
||||
assertDelayRange(t, "first play client turn", paddingSchedule2612[6].startDelay, time.Millisecond, 30*time.Millisecond)
|
||||
assertDelayRange(t, "first play server turn", paddingSchedule2612[7].startDelay, time.Millisecond, 45*time.Millisecond)
|
||||
assertDelayRange(t, "play server chunk pacing", paddingSchedule2612[7].chunkDelay, time.Millisecond, 4*time.Millisecond)
|
||||
if paddingSchedule2612[6].writeChunkLength != 1024 {
|
||||
t.Fatalf("play client write chunk = %d, want 1024", paddingSchedule2612[6].writeChunkLength)
|
||||
}
|
||||
if paddingSchedule2612[7].writeChunkLength != maxPaddingChunkLength {
|
||||
t.Fatalf("play server write chunk = %d, want %d", paddingSchedule2612[7].writeChunkLength, maxPaddingChunkLength)
|
||||
}
|
||||
if paddingSchedule2612[7].writeChunkMinLength != 32*1024 {
|
||||
t.Fatalf("play server minimum write chunk = %d, want %d", paddingSchedule2612[7].writeChunkMinLength, 32*1024)
|
||||
}
|
||||
|
||||
for i, variant := range paddingSchedule2612[3].variants {
|
||||
minimum, maximum := paddingVariantDelayBounds(variant)
|
||||
if minimum != 45*time.Millisecond || maximum != 65*time.Millisecond {
|
||||
t.Fatalf("turn 4 variant %d duration = %s-%s, want 45ms-65ms", i, minimum, maximum)
|
||||
}
|
||||
}
|
||||
for i, variant := range paddingSchedule2612[5].variants {
|
||||
minimum, maximum := paddingVariantDelayBounds(variant)
|
||||
if minimum != 10*time.Millisecond || maximum != 25*time.Millisecond {
|
||||
t.Fatalf("turn 6 variant %d duration = %s-%s, want 10ms-25ms", i, minimum, maximum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertDelayRange(t *testing.T, name string, got paddingDelayRange, minimum, maximum time.Duration) {
|
||||
t.Helper()
|
||||
if got.min != minimum || got.max != maximum {
|
||||
t.Fatalf("%s delay = %s-%s, want %s-%s", name, got.min, got.max, minimum, maximum)
|
||||
}
|
||||
}
|
||||
|
||||
func paddingVariantDelayBounds(variant paddingVariant) (time.Duration, time.Duration) {
|
||||
var minimum time.Duration
|
||||
var maximum time.Duration
|
||||
for _, delay := range variant.delays {
|
||||
minimum += delay.min
|
||||
maximum += delay.max
|
||||
}
|
||||
return minimum, maximum
|
||||
}
|
||||
|
||||
func slicesEqual[T comparable](left, right []T) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for i := range left {
|
||||
if left[i] != right[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func encodePaddingLength(t *testing.T, length int) []byte {
|
||||
t.Helper()
|
||||
var encoded bytes.Buffer
|
||||
value := Varint(length)
|
||||
if err := value.writeTo(&encoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return encoded.Bytes()
|
||||
}
|
||||
|
||||
type oneByteReader struct {
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
func (r *oneByteReader) Read(p []byte) (int, error) {
|
||||
if len(p) > 1 {
|
||||
p = p[:1]
|
||||
}
|
||||
return r.reader.Read(p)
|
||||
}
|
||||
|
||||
type recordingWriter struct {
|
||||
bytes.Buffer
|
||||
writes []int
|
||||
}
|
||||
|
||||
func (w *recordingWriter) Write(p []byte) (int, error) {
|
||||
w.writes = append(w.writes, len(p))
|
||||
return w.Buffer.Write(p)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package xmc
|
||||
|
||||
import "fmt"
|
||||
|
||||
type loginProfile struct {
|
||||
Username string
|
||||
UUID UUID
|
||||
TexturesValue string
|
||||
TexturesSignature string
|
||||
}
|
||||
|
||||
func profilesFromConfig(configured []*Profile) ([]loginProfile, error) {
|
||||
if len(configured) == 0 {
|
||||
return nil, fmt.Errorf("empty profiles")
|
||||
}
|
||||
|
||||
profiles := make([]loginProfile, 0, len(configured))
|
||||
for _, configuredProfile := range configured {
|
||||
if configuredProfile == nil || configuredProfile.Username == "" {
|
||||
return nil, fmt.Errorf("invalid profile")
|
||||
}
|
||||
if len(configuredProfile.Uuid) != len(UUID{}) {
|
||||
return nil, fmt.Errorf("bad profile UUID length: %d", len(configuredProfile.Uuid))
|
||||
}
|
||||
if configuredProfile.TexturesValue == "" || configuredProfile.TexturesSignature == "" {
|
||||
return nil, fmt.Errorf("incomplete profile textures")
|
||||
}
|
||||
|
||||
profile := loginProfile{
|
||||
Username: configuredProfile.Username,
|
||||
TexturesValue: configuredProfile.TexturesValue,
|
||||
TexturesSignature: configuredProfile.TexturesSignature,
|
||||
}
|
||||
copy(profile.UUID[:], configuredProfile.Uuid)
|
||||
profiles = append(profiles, profile)
|
||||
}
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
func findProfile(profiles []loginProfile, username string, uuid UUID) (loginProfile, bool) {
|
||||
for _, profile := range profiles {
|
||||
if profile.Username == username && profile.UUID == uuid {
|
||||
return profile, true
|
||||
}
|
||||
}
|
||||
return loginProfile{}, false
|
||||
}
|
||||
|
||||
func readLoginSuccess(packet *mcPacket) (loginProfile, error) {
|
||||
var (
|
||||
profile loginProfile
|
||||
username String
|
||||
propertyCount Varint
|
||||
propertyName String
|
||||
value String
|
||||
signed Boolean
|
||||
signature String
|
||||
)
|
||||
if err := packet.readFields(&profile.UUID, &username, &propertyCount, &propertyName, &value, &signed, &signature); err != nil {
|
||||
return loginProfile{}, err
|
||||
}
|
||||
if propertyCount != 1 || propertyName != "textures" || !signed {
|
||||
return loginProfile{}, fmt.Errorf("invalid login profile properties")
|
||||
}
|
||||
profile.Username = string(username)
|
||||
profile.TexturesValue = string(value)
|
||||
profile.TexturesSignature = string(signature)
|
||||
return profile, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProfilesFromConfigRejectsEmpty(t *testing.T) {
|
||||
if _, err := profilesFromConfig(nil); err == nil {
|
||||
t.Fatal("expected empty profiles error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfilesFromConfig(t *testing.T) {
|
||||
uuid := bytes.Repeat([]byte{0x2a}, 16)
|
||||
profiles, err := profilesFromConfig([]*Profile{
|
||||
{
|
||||
Username: "SignedUser",
|
||||
Uuid: uuid,
|
||||
TexturesValue: "textures-value",
|
||||
TexturesSignature: "textures-signature",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("build explicit profile: %v", err)
|
||||
}
|
||||
if len(profiles) != 1 || profiles[0].Username != "SignedUser" {
|
||||
t.Fatalf("unexpected profile: %+v", profiles)
|
||||
}
|
||||
if profiles[0].TexturesValue != "textures-value" || profiles[0].TexturesSignature != "textures-signature" {
|
||||
t.Fatalf("textures were not preserved: %+v", profiles[0])
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,11 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
maxPacketDataLength = 32 * 1024
|
||||
maxPacketBodyLength = maxPacketDataLength + 5
|
||||
)
|
||||
|
||||
type field interface {
|
||||
readFrom(r io.Reader) error
|
||||
writeTo(w io.Writer) error
|
||||
@@ -18,25 +23,38 @@ type mcPacket struct {
|
||||
}
|
||||
|
||||
func readPacket(b io.Reader) (*mcPacket, error) {
|
||||
var packetLength Varint
|
||||
err := packetLength.readFrom(b)
|
||||
packet, _, err := readPacketWithLength(b)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
func readPacketWithLength(b io.Reader) (*mcPacket, int, error) {
|
||||
packetData, wireLength, err := readFrame(b, maxPacketBodyLength)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read packet length: %w", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
packet, err := decodePacketBody(packetData)
|
||||
return packet, wireLength, err
|
||||
}
|
||||
|
||||
func decodePacketBody(packetData []byte) (*mcPacket, error) {
|
||||
if len(packetData) < 1 || len(packetData) > maxPacketBodyLength {
|
||||
return nil, fmt.Errorf("read packet: bad length: %d", len(packetData))
|
||||
}
|
||||
|
||||
body := bytes.NewReader(packetData)
|
||||
var packetID Varint
|
||||
err = packetID.readFrom(b)
|
||||
err := packetID.readFrom(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read packet ID: %w", err)
|
||||
}
|
||||
|
||||
dataLength := int(packetLength) - varintSize(packetID)
|
||||
if dataLength < 0 || dataLength > 1024*32 {
|
||||
dataLength := body.Len()
|
||||
if dataLength > maxPacketDataLength {
|
||||
return nil, fmt.Errorf("read packet: bad length: %d", dataLength)
|
||||
}
|
||||
|
||||
data := make([]byte, dataLength)
|
||||
_, err = io.ReadFull(b, data)
|
||||
_, err = io.ReadFull(body, data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read packet data: %w", err)
|
||||
}
|
||||
@@ -47,9 +65,24 @@ func readPacket(b io.Reader) (*mcPacket, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readFrame(r io.Reader, maxLength int) ([]byte, int, error) {
|
||||
frameLength, prefixLength, err := readVarintWithLength(r)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("read packet length: %w", err)
|
||||
}
|
||||
if frameLength < 1 || int(frameLength) > maxLength {
|
||||
return nil, 0, fmt.Errorf("read packet: bad length: %d", frameLength)
|
||||
}
|
||||
|
||||
frame := make([]byte, int(frameLength))
|
||||
if _, err := io.ReadFull(r, frame); err != nil {
|
||||
return nil, 0, fmt.Errorf("read packet data: %w", err)
|
||||
}
|
||||
return frame, prefixLength + len(frame), nil
|
||||
}
|
||||
|
||||
func (p *mcPacket) readFields(fields ...field) error {
|
||||
r := bytes.NewReader(p.data)
|
||||
|
||||
for _, field := range fields {
|
||||
err := field.readFrom(r)
|
||||
if err != nil {
|
||||
@@ -62,47 +95,49 @@ func (p *mcPacket) readFields(fields ...field) error {
|
||||
|
||||
type Varint int32
|
||||
|
||||
const (
|
||||
SEGMENT_BITS = 0x7F
|
||||
CONTINUE_BIT = 0x80
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
value, _, err := readVarintWithLength(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*v = Varint(value)
|
||||
|
||||
*v = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Varint) writeTo(w io.Writer) error {
|
||||
SEGMENT_BITS := byte(0x7F)
|
||||
CONTINUE_BIT := byte(0x80)
|
||||
func readVarintWithLength(r io.Reader) (Varint, int, error) {
|
||||
var value int32
|
||||
for index := 0; index < 5; index++ {
|
||||
currentByte, err := readByte(r)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("read varint: %w", err)
|
||||
}
|
||||
if index == 4 && currentByte&0xf0 != 0 {
|
||||
return 0, 0, fmt.Errorf("read varint: too large")
|
||||
}
|
||||
value |= int32(currentByte&SEGMENT_BITS) << (7 * index)
|
||||
|
||||
value := int32(*v)
|
||||
if currentByte&CONTINUE_BIT == 0 {
|
||||
parsed := Varint(value)
|
||||
length := index + 1
|
||||
if length != varintSize(parsed) {
|
||||
return 0, 0, fmt.Errorf("read varint: non-canonical encoding")
|
||||
}
|
||||
return parsed, length, nil
|
||||
}
|
||||
}
|
||||
return 0, 0, fmt.Errorf("read varint: too large")
|
||||
}
|
||||
|
||||
func (v *Varint) writeTo(w io.Writer) error {
|
||||
value := uint32(*v)
|
||||
|
||||
for {
|
||||
currentByte := byte(value & int32(SEGMENT_BITS))
|
||||
currentByte := byte(value & SEGMENT_BITS)
|
||||
value >>= 7
|
||||
if value != 0 {
|
||||
currentByte |= CONTINUE_BIT
|
||||
@@ -122,11 +157,12 @@ func (v *Varint) writeTo(w io.Writer) error {
|
||||
}
|
||||
|
||||
func varintSize(value Varint) int {
|
||||
uintValue := uint32(value)
|
||||
size := 0
|
||||
for {
|
||||
for range 5 {
|
||||
size++
|
||||
value >>= 7
|
||||
if value == 0 {
|
||||
uintValue >>= 7
|
||||
if uintValue == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -238,6 +274,31 @@ func (v *UUID) readFrom(r io.Reader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type Boolean bool
|
||||
|
||||
func (v *Boolean) readFrom(r io.Reader) error {
|
||||
b, err := readByte(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read boolean: %w", err)
|
||||
}
|
||||
if b > 1 {
|
||||
return fmt.Errorf("read boolean: invalid value: %d", b)
|
||||
}
|
||||
*v = b == 1
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Boolean) writeTo(w io.Writer) error {
|
||||
value := byte(0)
|
||||
if *v {
|
||||
value = 1
|
||||
}
|
||||
if _, err := w.Write([]byte{value}); err != nil {
|
||||
return fmt.Errorf("write boolean: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *UUID) writeTo(w io.Writer) error {
|
||||
_, err := w.Write(v[:])
|
||||
if err != nil {
|
||||
@@ -256,7 +317,7 @@ func (v *Bytes) readFrom(r io.Reader) error {
|
||||
}
|
||||
|
||||
if length < 0 || length >= 1024 {
|
||||
return fmt.Errorf("read bytes: invalid size: %d", err)
|
||||
return fmt.Errorf("read bytes: invalid size: %d", length)
|
||||
}
|
||||
|
||||
buf := make([]byte, length)
|
||||
@@ -271,6 +332,24 @@ func (v *Bytes) readFrom(r io.Reader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type RestBytes []byte
|
||||
|
||||
func (v *RestBytes) readFrom(r io.Reader) error {
|
||||
buf, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read remaining bytes: %w", err)
|
||||
}
|
||||
*v = append((*v)[:0], buf...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *RestBytes) writeTo(w io.Writer) error {
|
||||
if _, err := w.Write(*v); err != nil {
|
||||
return fmt.Errorf("write remaining bytes: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Bytes) writeTo(w io.Writer) error {
|
||||
length := Varint(len(*v))
|
||||
err := length.writeTo(w)
|
||||
@@ -297,36 +376,66 @@ func readByte(r io.Reader) (byte, error) {
|
||||
}
|
||||
|
||||
func writePacket(w io.Writer, packetID int, fields ...field) error {
|
||||
_, err := writePacketWithLength(w, packetID, fields...)
|
||||
return err
|
||||
}
|
||||
|
||||
func writePacketWithLength(w io.Writer, packetID int, fields ...field) (int, error) {
|
||||
frame, err := encodePacket(packetID, fields...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err = writeFull(w, frame); err != nil {
|
||||
return 0, fmt.Errorf("write packet data: %w", err)
|
||||
}
|
||||
return len(frame), nil
|
||||
}
|
||||
|
||||
func encodePacket(packetID int, fields ...field) ([]byte, error) {
|
||||
var dataBuf bytes.Buffer
|
||||
|
||||
for _, field := range fields {
|
||||
err := field.writeTo(&dataBuf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write packet field: %w", err)
|
||||
return nil, 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)
|
||||
if dataBuf.Len() > maxPacketDataLength {
|
||||
return nil, fmt.Errorf("write packet: bad length: %d", dataBuf.Len())
|
||||
}
|
||||
|
||||
var packetIDVarint Varint = Varint(packetID)
|
||||
err = packetIDVarint.writeTo(&buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write packet ID: %w", err)
|
||||
packetIDVarint := Varint(packetID)
|
||||
bodyLength := varintSize(packetIDVarint) + dataBuf.Len()
|
||||
if bodyLength > maxPacketBodyLength {
|
||||
return nil, fmt.Errorf("write packet: bad length: %d", bodyLength)
|
||||
}
|
||||
|
||||
buf.Write(dataBuf.Bytes())
|
||||
|
||||
_, err = w.Write(buf.Bytes())
|
||||
if err != nil {
|
||||
return fmt.Errorf("write packet data: %w", err)
|
||||
var frame bytes.Buffer
|
||||
frame.Grow(varintSize(Varint(bodyLength)) + bodyLength)
|
||||
frameLength := Varint(bodyLength)
|
||||
if err := frameLength.writeTo(&frame); err != nil {
|
||||
return nil, fmt.Errorf("write packet length: %w", err)
|
||||
}
|
||||
if err := packetIDVarint.writeTo(&frame); err != nil {
|
||||
return nil, fmt.Errorf("write packet ID: %w", err)
|
||||
}
|
||||
frame.Write(dataBuf.Bytes())
|
||||
return frame.Bytes(), nil
|
||||
}
|
||||
|
||||
func writeFull(w io.Writer, p []byte) error {
|
||||
for len(p) > 0 {
|
||||
n, err := w.Write(p)
|
||||
if n > 0 {
|
||||
p = p[n:]
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReadPacketDoesNotConsumeFollowingPacket(t *testing.T) {
|
||||
data := []byte{0x01, 0x80, 0x01, 0x00}
|
||||
r := bytes.NewReader(data)
|
||||
if _, err := readPacket(r); err == nil {
|
||||
t.Fatal("expected truncated packet ID to fail")
|
||||
}
|
||||
pkt, err := readPacket(r)
|
||||
if err != nil {
|
||||
t.Fatalf("read following packet: %v", err)
|
||||
}
|
||||
if pkt.packetID != 0 {
|
||||
t.Fatalf("packet ID = %d", pkt.packetID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketWithLengthReportsWireBytes(t *testing.T) {
|
||||
var wire bytes.Buffer
|
||||
written, err := writePacketWithLength(&wire, 0x03)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if written != 2 || !bytes.Equal(wire.Bytes(), []byte{0x01, 0x03}) {
|
||||
t.Fatalf("wire = %x, length = %d", wire.Bytes(), written)
|
||||
}
|
||||
|
||||
packet, read, err := readPacketWithLength(bytes.NewReader(wire.Bytes()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if packet.packetID != 0x03 || read != written {
|
||||
t.Fatalf("packet ID = %d, read = %d, written = %d", packet.packetID, read, written)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPacketRejectsNonCanonicalLengthVarint(t *testing.T) {
|
||||
_, _, err := readPacketWithLength(bytes.NewReader([]byte{0x81, 0x00, 0x03}))
|
||||
if err == nil || !strings.Contains(err.Error(), "non-canonical") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVarintRejectsOversizedFifthByte(t *testing.T) {
|
||||
var value Varint
|
||||
err := value.readFrom(bytes.NewReader([]byte{0xff, 0xff, 0xff, 0xff, 0x1f}))
|
||||
if err == nil || !strings.Contains(err.Error(), "too large") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -31,10 +31,16 @@ type serverConn struct {
|
||||
|
||||
state serverState
|
||||
|
||||
handshakeLock sync.Mutex
|
||||
password string
|
||||
rsaPrivateKey *rsa.PrivateKey
|
||||
rsaPublicKey []byte
|
||||
handshakeLock sync.Mutex
|
||||
lifecycleMu sync.Mutex
|
||||
closed bool
|
||||
profiles []loginProfile
|
||||
password string
|
||||
rsaPrivateKey *rsa.PrivateKey
|
||||
rsaPublicKey []byte
|
||||
paddingSchedule []paddingTurn
|
||||
packet *packetStream
|
||||
deadlines *connectionDeadlines
|
||||
}
|
||||
|
||||
func (c *serverConn) handshake() error {
|
||||
@@ -45,12 +51,10 @@ func (c *serverConn) handshake() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// handshake timeout
|
||||
err := c.c.SetDeadline(time.Now().Add(time.Second * 30))
|
||||
if err != nil {
|
||||
if err := c.deadlines.beginHandshake(); err != nil {
|
||||
return fmt.Errorf("set deadline: %w", err)
|
||||
}
|
||||
defer c.c.SetDeadline(time.Time{})
|
||||
defer func() { _ = c.deadlines.endHandshake() }()
|
||||
|
||||
var (
|
||||
protocolVersion Varint
|
||||
@@ -138,17 +142,20 @@ func (c *serverConn) handshake() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("read login start packet: %w", err)
|
||||
}
|
||||
profile, found := findProfile(c.profiles, string(username), uuid)
|
||||
|
||||
// encrypt request
|
||||
|
||||
var (
|
||||
serverId String = String("")
|
||||
publicKey Bytes = Bytes(c.rsaPublicKey)
|
||||
verifyToken Bytes = Bytes(make([]byte, 4))
|
||||
shouldAuthenticate Varint = Varint(1)
|
||||
serverId String = String("")
|
||||
publicKey Bytes = Bytes(c.rsaPublicKey)
|
||||
verifyToken Bytes = Bytes(make([]byte, 4))
|
||||
shouldAuthenticate Boolean = true
|
||||
)
|
||||
|
||||
rand.Read(verifyToken)
|
||||
if _, err = rand.Read(verifyToken); err != nil {
|
||||
return fmt.Errorf("generate verify token: %w", err)
|
||||
}
|
||||
|
||||
err = writePacket(c.writer, 0x01, &serverId, &publicKey, &verifyToken, &shouldAuthenticate)
|
||||
if err != nil {
|
||||
@@ -183,6 +190,9 @@ func (c *serverConn) handshake() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("decrypt shared secret: %w", err)
|
||||
}
|
||||
if len(sharedSecret) != 16 {
|
||||
return fmt.Errorf("bad shared secret length: %d", len(sharedSecret))
|
||||
}
|
||||
|
||||
decryptedVerifyToken, err = rsa.DecryptPKCS1v15(rand.Reader, c.rsaPrivateKey, encryptedVerifyToken)
|
||||
if err != nil {
|
||||
@@ -210,8 +220,47 @@ func (c *serverConn) handshake() error {
|
||||
writeDisconnectPacket(c.writer, `{"type":"translatable","translate":"multiplayer.disconnect.authservers_down"}`)
|
||||
return fmt.Errorf("bad password")
|
||||
}
|
||||
if !found {
|
||||
if err = writeDisconnectPacket(c.writer, `{"text":"You are not white-listed on this server!"}`); err != nil {
|
||||
return fmt.Errorf("write unknown login profile disconnect: %w", err)
|
||||
}
|
||||
return fmt.Errorf("unknown login profile")
|
||||
}
|
||||
|
||||
loginName := String(profile.Username)
|
||||
propertyCount := Varint(1)
|
||||
propertyName := String("textures")
|
||||
texturesValue := String(profile.TexturesValue)
|
||||
signed := Boolean(true)
|
||||
texturesSignature := String(profile.TexturesSignature)
|
||||
if err = writePacket(c.writer, 0x02, &profile.UUID, &loginName, &propertyCount, &propertyName, &texturesValue, &signed, &texturesSignature); err != nil {
|
||||
return fmt.Errorf("write login finished: %w", err)
|
||||
}
|
||||
|
||||
var loginAcknowledgedLength int
|
||||
pkt, loginAcknowledgedLength, err = readPacketWithLength(c.reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read login acknowledged: %w", err)
|
||||
}
|
||||
if err = validateLoginAcknowledgedPacket(pkt); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = runPaddingSchedule(c.reader, c.writer, false, loginAcknowledgedLength, c.paddingSchedule); err != nil {
|
||||
return fmt.Errorf("run startup padding: %w", err)
|
||||
}
|
||||
|
||||
packet := newPacketStream(c.reader, c.writer, false)
|
||||
c.lifecycleMu.Lock()
|
||||
if c.closed {
|
||||
c.lifecycleMu.Unlock()
|
||||
packet.Stop()
|
||||
return net.ErrClosed
|
||||
}
|
||||
c.packet = packet
|
||||
c.reader = packet
|
||||
c.writer = packet
|
||||
c.state = serverStateProxy
|
||||
c.lifecycleMu.Unlock()
|
||||
|
||||
return nil
|
||||
|
||||
@@ -220,6 +269,16 @@ func (c *serverConn) handshake() error {
|
||||
}
|
||||
}
|
||||
|
||||
func validateLoginAcknowledgedPacket(pkt *mcPacket) error {
|
||||
if pkt.packetID != 0x03 {
|
||||
return fmt.Errorf("bad login acknowledged packet id: %d", pkt.packetID)
|
||||
}
|
||||
if len(pkt.data) != 0 {
|
||||
return fmt.Errorf("bad login acknowledged packet data length: %d", len(pkt.data))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *serverConn) Read(b []byte) (int, error) {
|
||||
err := c.handshake()
|
||||
if err != nil {
|
||||
@@ -239,6 +298,13 @@ func (c *serverConn) Write(b []byte) (int, error) {
|
||||
}
|
||||
|
||||
func (c *serverConn) Close() error {
|
||||
c.lifecycleMu.Lock()
|
||||
c.closed = true
|
||||
packet := c.packet
|
||||
c.lifecycleMu.Unlock()
|
||||
if packet != nil {
|
||||
packet.Stop()
|
||||
}
|
||||
return c.c.Close()
|
||||
}
|
||||
|
||||
@@ -251,38 +317,47 @@ func (c *serverConn) RemoteAddr() net.Addr {
|
||||
}
|
||||
|
||||
func (c *serverConn) SetDeadline(t time.Time) error {
|
||||
return c.c.SetDeadline(t)
|
||||
return c.deadlines.setDeadline(t)
|
||||
}
|
||||
|
||||
func (c *serverConn) SetReadDeadline(t time.Time) error {
|
||||
return c.c.SetReadDeadline(t)
|
||||
return c.deadlines.setReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *serverConn) SetWriteDeadline(t time.Time) error {
|
||||
return c.c.SetWriteDeadline(t)
|
||||
return c.deadlines.setWriteDeadline(t)
|
||||
}
|
||||
|
||||
func wrapConnServer(c net.Conn, password string, rsaPrivateKeyDER []byte, rsaPublicKey []byte) (*serverConn, error) {
|
||||
func wrapConnServer(c net.Conn, profiles []loginProfile, password string, rsaPrivateKeyDER []byte, rsaPublicKey []byte) (*serverConn, error) {
|
||||
if len(profiles) == 0 {
|
||||
return nil, fmt.Errorf("empty profiles")
|
||||
}
|
||||
if len(rsaPrivateKeyDER) == 0 {
|
||||
return nil, fmt.Errorf("empty rsa private key")
|
||||
}
|
||||
if len(rsaPublicKey) == 0 {
|
||||
return nil, fmt.Errorf("empty rsa public key")
|
||||
}
|
||||
|
||||
rsaPrivateKey, err := x509.ParsePKCS1PrivateKey(rsaPrivateKeyDER)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse rsa private key: %w", err)
|
||||
}
|
||||
paddingSchedule, err := newServerPaddingSchedule2612()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("select padding profile: %w", err)
|
||||
}
|
||||
|
||||
s := &serverConn{
|
||||
reader: bufio.NewReader(c),
|
||||
writer: c,
|
||||
c: c,
|
||||
state: serverStateHandshake,
|
||||
password: password,
|
||||
rsaPrivateKey: rsaPrivateKey,
|
||||
rsaPublicKey: rsaPublicKey,
|
||||
reader: bufio.NewReader(c),
|
||||
writer: c,
|
||||
c: c,
|
||||
state: serverStateHandshake,
|
||||
profiles: profiles,
|
||||
password: password,
|
||||
rsaPrivateKey: rsaPrivateKey,
|
||||
rsaPublicKey: rsaPublicKey,
|
||||
paddingSchedule: paddingSchedule,
|
||||
deadlines: newConnectionDeadlines(c),
|
||||
}
|
||||
|
||||
return s, nil
|
||||
|
||||
@@ -6,12 +6,14 @@ import (
|
||||
"crypto/cipher"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type cryptoStream struct {
|
||||
stream cipher.Stream
|
||||
r io.Reader
|
||||
w io.Writer
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newCryptoReader(r io.Reader, sharedSecret []byte) (*cryptoStream, error) {
|
||||
@@ -30,12 +32,19 @@ func (c *cryptoStream) Read(b []byte) (int, error) {
|
||||
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.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.stream.XORKeyStream(b[:n], b[:n])
|
||||
n, err := c.r.Read(b)
|
||||
if n > 0 {
|
||||
c.stream.XORKeyStream(b[:n], b[:n])
|
||||
}
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return n, io.EOF
|
||||
}
|
||||
return n, fmt.Errorf("crypto reader: read: %w", err)
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
@@ -56,13 +65,15 @@ func (c *cryptoStream) Write(b []byte) (int, error) {
|
||||
panic("write on a read-only crypto stream")
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
encrypted := make([]byte, len(b))
|
||||
c.stream.XORKeyStream(encrypted, b)
|
||||
|
||||
n, err := c.w.Write(encrypted)
|
||||
if err != nil {
|
||||
if err := writeFull(c.w, encrypted); err != nil {
|
||||
return 0, fmt.Errorf("crypto writer: write: %w", err)
|
||||
}
|
||||
|
||||
return n, nil
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type dataAndEOFReader struct {
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (r *dataAndEOFReader) Read(p []byte) (int, error) {
|
||||
if len(r.data) == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := copy(p, r.data)
|
||||
r.data = r.data[n:]
|
||||
return n, io.EOF
|
||||
}
|
||||
|
||||
type shortWriter struct {
|
||||
bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *shortWriter) Write(p []byte) (int, error) {
|
||||
if len(p) > 1 {
|
||||
p = p[:len(p)/2]
|
||||
}
|
||||
return w.Buffer.Write(p)
|
||||
}
|
||||
|
||||
func TestCryptoReaderPreservesDataReturnedWithEOF(t *testing.T) {
|
||||
secret := []byte("0123456789abcdef")
|
||||
plaintext := []byte("payload returned with EOF")
|
||||
var encrypted bytes.Buffer
|
||||
writer, err := newCryptoWriter(&encrypted, secret)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = writer.Write(plaintext); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reader, err := newCryptoReader(&dataAndEOFReader{data: encrypted.Bytes()}, secret)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := make([]byte, len(plaintext))
|
||||
n, err := reader.Read(got)
|
||||
if err == nil || n != len(plaintext) {
|
||||
t.Fatalf("Read = %d, %v", n, err)
|
||||
}
|
||||
if !bytes.Equal(got[:n], plaintext) {
|
||||
t.Fatalf("plaintext = %q", got[:n])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCryptoWriterHandlesShortWrites(t *testing.T) {
|
||||
secret := []byte("0123456789abcdef")
|
||||
plaintext := bytes.Repeat([]byte("short-write"), 100)
|
||||
var dst shortWriter
|
||||
writer, err := newCryptoWriter(&dst, secret)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n, err := writer.Write(plaintext); err != nil || n != len(plaintext) {
|
||||
t.Fatalf("Write = %d, %v", n, err)
|
||||
}
|
||||
reader, err := newCryptoReader(bytes.NewReader(dst.Bytes()), secret)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got, plaintext) {
|
||||
t.Fatal("decrypted payload mismatch")
|
||||
}
|
||||
}
|
||||
@@ -38,12 +38,14 @@ func NewHunkReadWriter(hc HunkConn, cancel context.CancelFunc) *HunkReaderWriter
|
||||
|
||||
func NewHunkConn(hc HunkConn, cancel context.CancelFunc, trustedXForwardedFor []string) net.Conn {
|
||||
rAddr := remoteAddrFromContext(hc.Context(), trustedXForwardedFor)
|
||||
lAddr := localAddrFromContext(hc.Context())
|
||||
wrc := NewHunkReadWriter(hc, cancel)
|
||||
return cnc.NewConnection(
|
||||
cnc.ConnectionInput(wrc),
|
||||
cnc.ConnectionOutput(wrc),
|
||||
cnc.ConnectionOnClose(wrc),
|
||||
cnc.ConnectionRemoteAddr(rAddr),
|
||||
cnc.ConnectionLocalAddr(lAddr),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,12 +33,14 @@ func NewMultiHunkReadWriter(hc MultiHunkConn, cancel context.CancelFunc) *MultiH
|
||||
|
||||
func NewMultiHunkConn(hc MultiHunkConn, cancel context.CancelFunc, trustedXForwardedFor []string) net.Conn {
|
||||
rAddr := remoteAddrFromContext(hc.Context(), trustedXForwardedFor)
|
||||
lAddr := localAddrFromContext(hc.Context())
|
||||
wrc := NewMultiHunkReadWriter(hc, cancel)
|
||||
return cnc.NewConnection(
|
||||
cnc.ConnectionInputMulti(wrc),
|
||||
cnc.ConnectionOutputMulti(wrc),
|
||||
cnc.ConnectionOnClose(wrc),
|
||||
cnc.ConnectionRemoteAddr(rAddr),
|
||||
cnc.ConnectionLocalAddr(lAddr),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -56,3 +56,17 @@ func parseTrustedXForwardedFor(md metadata.MD, trusted []string, remoteAddr net.
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func localAddrFromContext(ctx context.Context) net.Addr {
|
||||
var localAddr net.Addr
|
||||
if pr, ok := peer.FromContext(ctx); ok {
|
||||
localAddr = pr.LocalAddr
|
||||
}
|
||||
if localAddr == nil {
|
||||
localAddr = &net.TCPAddr{
|
||||
IP: []byte{0, 0, 0, 0},
|
||||
Port: 0,
|
||||
}
|
||||
}
|
||||
return localAddr
|
||||
}
|
||||
|
||||
@@ -373,11 +373,15 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
|
||||
Reader: request.Body,
|
||||
ResponseWriter: writer,
|
||||
}
|
||||
localAddr := h.localAddr
|
||||
if la, ok := request.Context().Value(http.LocalAddrContextKey).(net.Addr); ok && la != nil {
|
||||
localAddr = la
|
||||
}
|
||||
conn := splitConn{
|
||||
writer: httpSC,
|
||||
reader: httpSC,
|
||||
remoteAddr: remoteAddr,
|
||||
localAddr: h.localAddr,
|
||||
localAddr: localAddr,
|
||||
}
|
||||
if sessionId != "" { // if not stream-one
|
||||
conn.reader = currentSession.uploadQueue
|
||||
|
||||
Reference in New Issue
Block a user