Compare commits

..
3 Commits
Author SHA1 Message Date
Fangliding 1deae07504 Add grpc local addr 2026-07-21 22:02:44 +08:00
风扇滑翔翼andGitHub 1e9962116f Remove ai comment 2026-07-12 03:49:20 +08:00
echoowall d5dcc4d6fe splithttp: bind sendThrough=origin to the real per-connection local IP
The XHTTP inbound set every accepted connection's LocalAddr to the
listener's address (h.localAddr = l.listener.Addr()). On a wildcard
listener that is the unspecified address ("[::]" / "0.0.0.0").

sendThrough "origin" derives the outbound gateway from inbound.Local,
which comes from conn.LocalAddr(). So with XHTTP every connection's
egress was bound to the wildcard, collapsing all entry IPs onto one
(often IPv6) source address and breaking source-in-source-out on
multi-IP hosts (and failing outright when that address has no route).

Read the concrete per-connection local address from the request context
(http.LocalAddrContextKey), which net/http populates with the address
the client actually connected to. Fall back to the listener address when
the key is absent (e.g. HTTP/3, where net/http does not set it).
2026-07-12 03:39:36 +08:00
39 changed files with 203 additions and 2977 deletions
+1 -1
View File
@@ -92,7 +92,7 @@ jobs:
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
- name: Set up Go
uses: actions/setup-go@v7
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
+1 -1
View File
@@ -193,7 +193,7 @@ jobs:
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
- name: Set up Go
uses: actions/setup-go@v7
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
+2 -2
View File
@@ -61,7 +61,7 @@ jobs:
- name: Checkout codebase
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v7
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
@@ -85,7 +85,7 @@ jobs:
- name: Checkout codebase
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v7
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build darwin && !ios
//go:build darwin
package net
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build darwin && !ios
//go:build darwin
#include "textflag.h"
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build darwin && !ios
//go:build darwin
package net
-11
View File
@@ -1,11 +0,0 @@
//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
View File
@@ -20,7 +20,7 @@ import (
var (
Version_x byte = 26
Version_y byte = 7
Version_z byte = 28
Version_z byte = 11
)
var (
+1 -1
View File
@@ -31,7 +31,7 @@ require (
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/windows v1.0.1
google.golang.org/grpc v1.82.1
google.golang.org/grpc v1.82.0
google.golang.org/protobuf v1.36.11
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
h12.io/socks v1.0.3
+2 -2
View File
@@ -149,8 +149,8 @@ gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+6 -48
View File
@@ -11,7 +11,6 @@ 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"
@@ -721,46 +720,14 @@ func (c *Xdns) Build() (proto.Message, error) {
}
type XMC struct {
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
Hostname string `json:"hostname"`
Usernames []string `json:"usernames"`
Password string `json:"password"`
}
func (c *XMC) Build() (proto.Message, error) {
if len(c.Profiles) == 0 {
return nil, fmt.Errorf("minecraft profiles are required")
if len(c.Usernames) == 0 {
c.Usernames = []string{"Dream"}
}
if c.Password == "" {
@@ -777,21 +744,12 @@ 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
}
@@ -1,36 +0,0 @@
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)
}
}
+2 -2
View File
@@ -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 = 3
c.Xmux.MaxConnections.To = 3
c.Xmux.MaxConnections.From = 6
c.Xmux.MaxConnections.To = 6
c.Xmux.HMaxRequestTimes.From = 600
c.Xmux.HMaxRequestTimes.To = 900
c.Xmux.HMaxReusableSecs.From = 1800
+3 -7
View File
@@ -113,10 +113,8 @@ func (c *REALITYConfig) Build() (proto.Message, error) {
config.MinClientVer[i] = byte(u)
}
}
errors.LogWarning(context.Background(), `REALITY: Changing "minClientVer" will increase the likelihood of your server's IP being blocked by the GFW`)
} else {
config.MinClientVer = []byte{26, 3, 27} // change it at your own risk: https://github.com/XTLS/Xray-core/commit/af7eb68028732a8ee3c0e5d6ab2b8a657bb2e770
errors.LogWarning(context.Background(), `REALITY: The default minimal client version is Xray-core v26.3.27, other clients may be refused to connect`)
config.MinClientVer = []byte{26, 3, 27} // change it at your own risk: https://github.com/XTLS/Xray-core/pull/6181#issuecomment-4567373533
}
if c.MaxClientVer != "" {
config.MaxClientVer = make([]byte, 3)
@@ -161,10 +159,8 @@ func (c *REALITYConfig) Build() (proto.Message, error) {
}
for _, sn := range config.ServerNames {
sn = strings.ToLower(sn)
if strings.HasSuffix(sn, ".ru") || strings.HasSuffix(sn, ".ir") || strings.HasSuffix(sn, ".cn") ||
strings.Contains(sn, "apple") || strings.Contains(sn, "icloud") || strings.Contains(sn, "microsoft") {
errors.LogWarning(context.Background(), `REALITY: Choosing "`, sn, `" as the target will increase the likelihood of your server's IP being blocked by the GFW`)
if strings.Contains(sn, "apple") || strings.Contains(sn, "icloud") {
errors.LogWarning(context.Background(), `REALITY: Choosing apple, icloud, etc. as the target may get your IP blocked by the GFW`)
}
}
+1 -70
View File
@@ -1,19 +1,12 @@
package conf
import (
"crypto/rand"
"fmt"
"math/big"
"net"
"strconv"
"github.com/xtls/xray-core/proxy/tun"
"google.golang.org/protobuf/proto"
)
type TunConfig struct {
Name string `json:"name"`
Desc string `json:"desc"`
MTU uint32 `json:"mtu"`
Gateway []string `json:"gateway"`
DNS []string `json:"dns"`
@@ -25,7 +18,6 @@ type TunConfig struct {
func (v *TunConfig) Build() (proto.Message, error) {
config := &tun.Config{
Name: v.Name,
Desc: v.Desc,
MTU: v.MTU,
Gateway: v.Gateway,
DNS: v.DNS,
@@ -40,71 +32,10 @@ func (v *TunConfig) Build() (proto.Message, error) {
}
if config.Name == "" {
name, err := GetAvailableTunName()
if err != nil {
return nil, err
}
config.Name = name
}
if config.Desc == "" {
config.Desc = "Wintun"
config.Name = "xray0"
}
if config.MTU == 0 {
config.MTU = 1500
}
return config, nil
}
const (
tunNamePrefix = "utun"
minTunIndex = 10
maxTunIndex = 1024
)
func GetAvailableTunName() (string, error) {
interfaces, err := net.Interfaces()
if err != nil {
return "", fmt.Errorf("fail to get system interface information: %w", err)
}
usedNames := make(map[string]struct{}, len(interfaces))
for _, iface := range interfaces {
usedNames[iface.Name] = struct{}{}
}
startIndex, err := randomInt(minTunIndex, maxTunIndex)
if err != nil {
return "", fmt.Errorf("fail to generate valid tun name: %w", err)
}
rangeSize := maxTunIndex - minTunIndex + 1
for offset := 0; offset < rangeSize; offset++ {
index := minTunIndex + (startIndex-minTunIndex+offset)%rangeSize
name := tunNamePrefix + strconv.Itoa(index)
if _, exists := usedNames[name]; !exists {
return name, nil
}
}
return "", fmt.Errorf(
"no available TUN interface name in range %s%d-%s%d",
tunNamePrefix,
minTunIndex,
tunNamePrefix,
maxTunIndex,
)
}
func randomInt(min, max int) (int, error) {
value, err := rand.Int(
rand.Reader,
big.NewInt(int64(max-min+1)),
)
if err != nil {
return 0, err
}
return min + int(value.Int64()), nil
}
+1 -1
View File
@@ -176,7 +176,7 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
receiverSettings.StreamSettings = ss
if strings.Contains(ss.SecurityType, "reality") && (receiverSettings.PortList == nil ||
len(receiverSettings.PortList.Ports()) != 1 || receiverSettings.PortList.Ports()[0] != 443) {
errors.LogWarning(context.Background(), `REALITY: Listening on non-443 ports will increase the likelihood of your server's IP being blocked by the GFW`)
errors.LogWarning(context.Background(), `REALITY: Listening on non-443 ports may get your IP blocked by the GFW`)
}
}
if c.SniffingConfig != nil {
+2 -6
View File
@@ -31,17 +31,13 @@ Here is simple Xray config snippet to enable the inbound:
"port": 0,
"protocol": "tun",
"settings": {
"name": "utun10",
"desc": "Wintun",
"mtu": 1500
"name": "xray0",
"MTU": 1492
}
}
],
```
`desc` sets the Windows Wintun adapter tunnel type and defaults to `Wintun`.
It is ignored on other platforms.
## SUPPORTED FEATURES
- IPv4 and IPv6
+4 -14
View File
@@ -7,12 +7,11 @@
package tun
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
)
const (
@@ -31,7 +30,6 @@ type Config struct {
UserLevel uint32 `protobuf:"varint,5,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
AutoSystemRoutingTable []string `protobuf:"bytes,6,rep,name=auto_system_routing_table,json=autoSystemRoutingTable,proto3" json:"auto_system_routing_table,omitempty"`
AutoOutboundsInterface string `protobuf:"bytes,7,opt,name=auto_outbounds_interface,json=autoOutboundsInterface,proto3" json:"auto_outbounds_interface,omitempty"`
Desc string `protobuf:"bytes,8,opt,name=desc,proto3" json:"desc,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -115,18 +113,11 @@ func (x *Config) GetAutoOutboundsInterface() string {
return ""
}
func (x *Config) GetDesc() string {
if x != nil {
return x.Desc
}
return ""
}
var File_proxy_tun_config_proto protoreflect.FileDescriptor
const file_proxy_tun_config_proto_rawDesc = "" +
"\n" +
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\x82\x02\n" +
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\xee\x01\n" +
"\x06Config\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
"\x03MTU\x18\x02 \x01(\rR\x03MTU\x12\x18\n" +
@@ -135,8 +126,7 @@ const file_proxy_tun_config_proto_rawDesc = "" +
"\n" +
"user_level\x18\x05 \x01(\rR\tuserLevel\x129\n" +
"\x19auto_system_routing_table\x18\x06 \x03(\tR\x16autoSystemRoutingTable\x128\n" +
"\x18auto_outbounds_interface\x18\a \x01(\tR\x16autoOutboundsInterface\x12\x12\n" +
"\x04desc\x18\b \x01(\tR\x04descBL\n" +
"\x18auto_outbounds_interface\x18\a \x01(\tR\x16autoOutboundsInterfaceBL\n" +
"\x12com.xray.proxy.tunP\x01Z#github.com/xtls/xray-core/proxy/tun\xaa\x02\x0eXray.Proxy.Tunb\x06proto3"
var (
-1
View File
@@ -14,5 +14,4 @@ message Config {
uint32 user_level = 5;
repeated string auto_system_routing_table = 6;
string auto_outbounds_interface = 7;
string desc = 8;
}
+3 -3
View File
@@ -50,7 +50,7 @@ var _ GVisorDevice = (*WindowsTun)(nil)
// interface with the same name exist, it tried to be reused.
func NewTun(options *Config) (Tun, error) {
// instantiate wintun adapter
adapter, err := open(options.Name, options.Desc)
adapter, err := open(options.Name)
if err != nil {
return nil, err
}
@@ -73,12 +73,12 @@ func NewTun(options *Config) (Tun, error) {
return tun, nil
}
func open(name, desc string) (*wintun.Adapter, error) {
func open(name string) (*wintun.Adapter, error) {
// generate a deterministic GUID from the adapter name
id := md5.Sum([]byte(name))
guid := (*windows.GUID)(unsafe.Pointer(&id[0]))
// try to create adapter anew
adapter, err := wintun.CreateAdapter(name, desc, guid)
adapter, err := wintun.CreateAdapter(name, "Xray", guid)
if err == nil {
return adapter, nil
}
+41 -92
View File
@@ -5,6 +5,7 @@ import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"fmt"
"io"
@@ -22,16 +23,11 @@ type clientConn struct {
state clientState
handshakeLock sync.Mutex
lifecycleMu sync.Mutex
closed bool
profiles []loginProfile
password string
rsaPublicKey []byte
hostname string
paddingSchedule []paddingTurn
packet *packetStream
deadlines *connectionDeadlines
handshakeLock sync.Mutex
usernames []string
password string
rsaPublicKey []byte
hostname string
}
type clientState int
@@ -41,29 +37,21 @@ var (
clientStateProxy clientState = 2
)
func newClientConn(c net.Conn, profiles []loginProfile, password string, rsaPublicKey []byte, hostname string) (*clientConn, error) {
func newClientConn(c net.Conn, usernames []string, password string, rsaPublicKey []byte, hostname string) (*clientConn, error) {
if len(rsaPublicKey) == 0 {
return nil, fmt.Errorf("empty rsa public key")
}
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{},
profiles: profiles,
password: password,
rsaPublicKey: rsaPublicKey,
hostname: hostname,
paddingSchedule: paddingSchedule,
deadlines: newConnectionDeadlines(c),
reader: bufio.NewReader(c),
writer: c,
c: c,
state: clientStateHandshake,
handshakeLock: sync.Mutex{},
usernames: usernames,
password: password,
rsaPublicKey: rsaPublicKey,
hostname: hostname,
}, nil
}
@@ -75,10 +63,12 @@ func (c *clientConn) handshake() error {
return nil
}
if err := c.deadlines.beginHandshake(); err != nil {
// Handshake timeout
err := c.c.SetDeadline(time.Now().Add(time.Second * 30))
if err != nil {
return fmt.Errorf("set deadline: %w", err)
}
defer func() { _ = c.deadlines.endHandshake() }()
defer c.c.SetDeadline(time.Time{})
var (
protocolVersion Varint = Varint(775)
@@ -105,14 +95,16 @@ func (c *clientConn) handshake() error {
}
// Login Start
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)
var (
username string
offlineUUID UUID
)
err = writePacket(c.writer, 0x00, &username, &selectedProfile.UUID)
randomUsername, _ := rand.Int(rand.Reader, big.NewInt(int64(len(c.usernames))))
username = c.usernames[randomUsername.Int64()]
generateOfflineUUID(&offlineUUID, string(username))
err = writePacket(c.writer, 0x00, new(String(username)), &offlineUUID)
if err != nil {
return fmt.Errorf("write login start: %w", err)
}
@@ -153,9 +145,7 @@ func (c *clientConn) handshake() error {
}
sharedSecret := make([]byte, 16)
if _, err = rand.Read(sharedSecret); err != nil {
return fmt.Errorf("generate shared secret: %w", err)
}
rand.Read(sharedSecret)
encryptedSharedSecret, err := rsa.EncryptPKCS1v15(rand.Reader, rsaPublicKey, sharedSecret)
if err != nil {
@@ -191,48 +181,7 @@ 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
}
@@ -256,13 +205,6 @@ 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()
}
@@ -275,13 +217,20 @@ func (c *clientConn) RemoteAddr() net.Addr {
}
func (c *clientConn) SetDeadline(t time.Time) error {
return c.deadlines.setDeadline(t)
return c.c.SetDeadline(t)
}
func (c *clientConn) SetReadDeadline(t time.Time) error {
return c.deadlines.setReadDeadline(t)
return c.c.SetReadDeadline(t)
}
func (c *clientConn) SetWriteDeadline(t time.Time) error {
return c.deadlines.setWriteDeadline(t)
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
}
+2 -10
View File
@@ -9,11 +9,7 @@ func (c *Config) TCP() {
}
func (c *Config) WrapConnClient(conn net.Conn) (net.Conn, error) {
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)
cc, err := newClientConn(conn, c.Usernames, c.Password, c.RsaPublicKey, c.Hostname)
if err != nil {
return nil, fmt.Errorf("minecraft finalmask: %w", err)
}
@@ -22,11 +18,7 @@ func (c *Config) WrapConnClient(conn net.Conn) (net.Conn, error) {
}
func (c *Config) WrapConnServer(conn net.Conn) (net.Conn, error) {
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)
cc, err := wrapConnServer(conn, c.Password, c.RsaPrivateKey, c.RsaPublicKey)
if err != nil {
return nil, fmt.Errorf("minecraft finalmask: %w", err)
}
+23 -101
View File
@@ -21,91 +21,20 @@ 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[1]
mi := &file_transport_internet_finalmask_xmc_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -117,7 +46,7 @@ func (x *Config) String() string {
func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_finalmask_xmc_config_proto_msgTypes[1]
mi := &file_transport_internet_finalmask_xmc_config_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -130,7 +59,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{1}
return file_transport_internet_finalmask_xmc_config_proto_rawDescGZIP(), []int{0}
}
func (x *Config) GetPassword() string {
@@ -140,6 +69,13 @@ 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
@@ -161,30 +97,18 @@ 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\"\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" +
"-transport/internet/finalmask/xmc/config.proto\x12%xray.transport.internet.finalmask.xmc\"\xac\x01\n" +
"\x06Config\x12\x1a\n" +
"\bpassword\x18\x01 \x01(\tR\bpassword\x12&\n" +
"\bpassword\x18\x01 \x01(\tR\bpassword\x12\x1c\n" +
"\tusernames\x18\x02 \x03(\tR\tusernames\x12&\n" +
"\x0frsa_private_key\x18\b \x01(\fR\rrsaPrivateKey\x12$\n" +
"\x0ersa_public_key\x18\t \x01(\fR\frsaPublicKey\x12\x1a\n" +
"\bhostname\x18\n" +
" \x01(\tR\bhostname\x12J\n" +
"\bprofiles\x18\v \x03(\v2..xray.transport.internet.finalmask.xmc.ProfileR\bprofilesJ\x04\b\x02\x10\x03B\x91\x01\n" +
" \x01(\tR\bhostnameB\x91\x01\n" +
")com.xray.transport.internet.finalmask.xmcP\x01Z:github.com/xtls/xray-core/transport/internet/finalmask/xmc\xaa\x02%Xray.Transport.Internet.Finalmask.XMCb\x06proto3"
var (
@@ -199,18 +123,16 @@ 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, 2)
var file_transport_internet_finalmask_xmc_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_transport_internet_finalmask_xmc_config_proto_goTypes = []any{
(*Profile)(nil), // 0: xray.transport.internet.finalmask.xmc.Profile
(*Config)(nil), // 1: xray.transport.internet.finalmask.xmc.Config
(*Config)(nil), // 0: xray.transport.internet.finalmask.xmc.Config
}
var file_transport_internet_finalmask_xmc_config_proto_depIdxs = []int32{
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
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_transport_internet_finalmask_xmc_config_proto_init() }
@@ -224,7 +146,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: 2,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
+1 -11
View File
@@ -6,21 +6,11 @@ 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;
reserved 2;
repeated string usernames = 2;
bytes rsa_private_key = 8;
bytes rsa_public_key = 9;
string hostname = 10;
repeated Profile profiles = 11;
}
@@ -1,85 +0,0 @@
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
}
@@ -1,81 +0,0 @@
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,16 +2,10 @@ 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) {
@@ -30,19 +24,6 @@ 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 {
@@ -51,7 +32,7 @@ func TestHandshakeSuccess(t *testing.T) {
defer ln.Close()
password := "super-secure-shared-key-12345"
profiles := []loginProfile{testLoginProfile("test_user")}
usernames := []string{"test_user"}
privateKey, publicKey := deriveTestRSAKey(t, password)
go func() {
@@ -61,7 +42,7 @@ func TestHandshakeSuccess(t *testing.T) {
}
defer rawConn.Close()
server, err := wrapConnServer(rawConn, profiles, password, privateKey, publicKey)
server, err := wrapConnServer(rawConn, password, privateKey, publicKey)
if err != nil {
t.Errorf("failed to wrap server: %v", err)
return
@@ -92,7 +73,7 @@ func TestHandshakeSuccess(t *testing.T) {
}
defer clientRaw.Close()
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
client, err := newClientConn(clientRaw, usernames, password, publicKey, "localhost")
if err != nil {
t.Fatalf("failed to create client: %v", err)
}
@@ -122,7 +103,7 @@ func TestHandshakePasswordMismatch(t *testing.T) {
clientPassword := "client-secret-123"
serverPassword := "server-secret-456"
profiles := []loginProfile{testLoginProfile("test_user")}
usernames := []string{"test_user"}
serverPrivateKey, serverPublicKey := deriveTestRSAKey(t, serverPassword)
var wg sync.WaitGroup
@@ -136,7 +117,7 @@ func TestHandshakePasswordMismatch(t *testing.T) {
}
defer rawConn.Close()
server, err := wrapConnServer(rawConn, profiles, serverPassword, serverPrivateKey, serverPublicKey)
server, err := wrapConnServer(rawConn, serverPassword, serverPrivateKey, serverPublicKey)
if err != nil {
// Wrapping is synchronous and shouldn't fail initially simply because key derivation works with any string
t.Logf("wrapped server: %v", err)
@@ -158,233 +139,20 @@ func TestHandshakePasswordMismatch(t *testing.T) {
}
defer clientRaw.Close()
client, err := newClientConn(clientRaw, profiles, clientPassword, serverPublicKey, "localhost")
client, err := newClientConn(clientRaw, usernames, clientPassword, serverPublicKey, "localhost")
if err != nil {
t.Fatalf("failed to create client: %v", err)
}
err = client.handshake()
if err == nil {
t.Fatal("expected client handshake to fail due to password mismatch")
if err != nil {
t.Fatalf("client handshake err: %v", err)
}
_, _ = client.Write([]byte{0x1, 0x2, 0x3, 0x4})
wg.Wait()
// Check if we lost connection or received error
t.Log("Handshake mismatch tested")
}
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")
}
}
@@ -1,184 +0,0 @@
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
}
}
}
@@ -1,102 +0,0 @@
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)
}
}
-432
View File
@@ -1,432 +0,0 @@
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
}
@@ -1,282 +0,0 @@
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
}
@@ -1,618 +0,0 @@
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)
}
@@ -1,69 +0,0 @@
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
}
@@ -1,33 +0,0 @@
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])
}
}
+62 -171
View File
@@ -7,11 +7,6 @@ import (
"io"
)
const (
maxPacketDataLength = 32 * 1024
maxPacketBodyLength = maxPacketDataLength + 5
)
type field interface {
readFrom(r io.Reader) error
writeTo(w io.Writer) error
@@ -23,38 +18,25 @@ type mcPacket struct {
}
func readPacket(b io.Reader) (*mcPacket, error) {
packet, _, err := readPacketWithLength(b)
return packet, err
}
func readPacketWithLength(b io.Reader) (*mcPacket, int, error) {
packetData, wireLength, err := readFrame(b, maxPacketBodyLength)
var packetLength Varint
err := packetLength.readFrom(b)
if err != nil {
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))
return nil, fmt.Errorf("read packet length: %w", err)
}
body := bytes.NewReader(packetData)
var packetID Varint
err := packetID.readFrom(body)
err = packetID.readFrom(b)
if err != nil {
return nil, fmt.Errorf("read packet ID: %w", err)
}
dataLength := body.Len()
if dataLength > maxPacketDataLength {
dataLength := int(packetLength) - varintSize(packetID)
if dataLength < 0 || dataLength > 1024*32 {
return nil, fmt.Errorf("read packet: bad length: %d", dataLength)
}
data := make([]byte, dataLength)
_, err = io.ReadFull(body, data)
_, err = io.ReadFull(b, data)
if err != nil {
return nil, fmt.Errorf("read packet data: %w", err)
}
@@ -65,24 +47,9 @@ func decodePacketBody(packetData []byte) (*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 {
@@ -95,49 +62,47 @@ 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 {
value, _, err := readVarintWithLength(r)
if err != nil {
return err
SEGMENT_BITS := byte(0x7F)
CONTINUE_BIT := byte(0x80)
var err error
var value int32 = 0
var position int32 = 0
var currentByte byte
for true {
currentByte, err = readByte(r)
if err != nil {
return fmt.Errorf("read varint: %w", err)
}
value |= int32(currentByte&SEGMENT_BITS) << position
if (currentByte & CONTINUE_BIT) == 0 {
break
}
position += 7
if position >= 32 {
return fmt.Errorf("read varint: too large")
}
}
*v = value
*v = Varint(value)
return nil
}
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)
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)
SEGMENT_BITS := byte(0x7F)
CONTINUE_BIT := byte(0x80)
value := int32(*v)
for {
currentByte := byte(value & SEGMENT_BITS)
currentByte := byte(value & int32(SEGMENT_BITS))
value >>= 7
if value != 0 {
currentByte |= CONTINUE_BIT
@@ -157,12 +122,11 @@ func (v *Varint) writeTo(w io.Writer) error {
}
func varintSize(value Varint) int {
uintValue := uint32(value)
size := 0
for range 5 {
for {
size++
uintValue >>= 7
if uintValue == 0 {
value >>= 7
if value == 0 {
break
}
}
@@ -274,31 +238,6 @@ 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 {
@@ -317,7 +256,7 @@ func (v *Bytes) readFrom(r io.Reader) error {
}
if length < 0 || length >= 1024 {
return fmt.Errorf("read bytes: invalid size: %d", length)
return fmt.Errorf("read bytes: invalid size: %d", err)
}
buf := make([]byte, length)
@@ -332,24 +271,6 @@ 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)
@@ -376,66 +297,36 @@ 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 nil, fmt.Errorf("write packet field: %w", err)
return fmt.Errorf("write packet field: %w", err)
}
}
if dataBuf.Len() > maxPacketDataLength {
return nil, fmt.Errorf("write packet: bad length: %d", dataBuf.Len())
}
packetIDVarint := Varint(packetID)
bodyLength := varintSize(packetIDVarint) + dataBuf.Len()
if bodyLength > maxPacketBodyLength {
return nil, fmt.Errorf("write packet: bad length: %d", bodyLength)
var buf bytes.Buffer
var packetLength Varint = Varint(varintSize(Varint(packetID)) + dataBuf.Len())
err := packetLength.writeTo(&buf)
if err != nil {
return fmt.Errorf("write packet length: %w", err)
}
var 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)
var packetIDVarint Varint = Varint(packetID)
err = packetIDVarint.writeTo(&buf)
if err != nil {
return fmt.Errorf("write packet ID: %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
}
buf.Write(dataBuf.Bytes())
_, err = w.Write(buf.Bytes())
if err != nil {
return fmt.Errorf("write packet data: %w", err)
}
return nil
}
@@ -1,56 +0,0 @@
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)
}
}
+25 -100
View File
@@ -31,16 +31,10 @@ type serverConn struct {
state serverState
handshakeLock sync.Mutex
lifecycleMu sync.Mutex
closed bool
profiles []loginProfile
password string
rsaPrivateKey *rsa.PrivateKey
rsaPublicKey []byte
paddingSchedule []paddingTurn
packet *packetStream
deadlines *connectionDeadlines
handshakeLock sync.Mutex
password string
rsaPrivateKey *rsa.PrivateKey
rsaPublicKey []byte
}
func (c *serverConn) handshake() error {
@@ -51,10 +45,12 @@ func (c *serverConn) handshake() error {
return nil
}
if err := c.deadlines.beginHandshake(); err != nil {
// handshake timeout
err := c.c.SetDeadline(time.Now().Add(time.Second * 30))
if err != nil {
return fmt.Errorf("set deadline: %w", err)
}
defer func() { _ = c.deadlines.endHandshake() }()
defer c.c.SetDeadline(time.Time{})
var (
protocolVersion Varint
@@ -142,20 +138,17 @@ 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 Boolean = true
serverId String = String("")
publicKey Bytes = Bytes(c.rsaPublicKey)
verifyToken Bytes = Bytes(make([]byte, 4))
shouldAuthenticate Varint = Varint(1)
)
if _, err = rand.Read(verifyToken); err != nil {
return fmt.Errorf("generate verify token: %w", err)
}
rand.Read(verifyToken)
err = writePacket(c.writer, 0x01, &serverId, &publicKey, &verifyToken, &shouldAuthenticate)
if err != nil {
@@ -190,9 +183,6 @@ 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 {
@@ -220,47 +210,8 @@ 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
@@ -269,16 +220,6 @@ 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 {
@@ -298,13 +239,6 @@ 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()
}
@@ -317,47 +251,38 @@ func (c *serverConn) RemoteAddr() net.Addr {
}
func (c *serverConn) SetDeadline(t time.Time) error {
return c.deadlines.setDeadline(t)
return c.c.SetDeadline(t)
}
func (c *serverConn) SetReadDeadline(t time.Time) error {
return c.deadlines.setReadDeadline(t)
return c.c.SetReadDeadline(t)
}
func (c *serverConn) SetWriteDeadline(t time.Time) error {
return c.deadlines.setWriteDeadline(t)
return c.c.SetWriteDeadline(t)
}
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")
}
func wrapConnServer(c net.Conn, password string, rsaPrivateKeyDER []byte, rsaPublicKey []byte) (*serverConn, error) {
if len(rsaPrivateKeyDER) == 0 {
return nil, fmt.Errorf("empty rsa private key")
}
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,
profiles: profiles,
password: password,
rsaPrivateKey: rsaPrivateKey,
rsaPublicKey: rsaPublicKey,
paddingSchedule: paddingSchedule,
deadlines: newConnectionDeadlines(c),
reader: bufio.NewReader(c),
writer: c,
c: c,
state: serverStateHandshake,
password: password,
rsaPrivateKey: rsaPrivateKey,
rsaPublicKey: rsaPublicKey,
}
return s, nil
+6 -17
View File
@@ -6,14 +6,12 @@ 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) {
@@ -32,20 +30,13 @@ func (c *cryptoStream) Read(b []byte) (int, error) {
panic("read on a write-only crypto stream")
}
c.mu.Lock()
defer c.mu.Unlock()
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 0, fmt.Errorf("crypto reader: read: %w", err)
}
c.stream.XORKeyStream(b[:n], b[:n])
return n, nil
}
@@ -65,15 +56,13 @@ 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)
if err := writeFull(c.w, encrypted); err != nil {
n, err := c.w.Write(encrypted)
if err != nil {
return 0, fmt.Errorf("crypto writer: write: %w", err)
}
return len(b), nil
return n, nil
}
@@ -1,81 +0,0 @@
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")
}
}