Compare commits

...
19 Commits
Author SHA1 Message Date
patterniha a9d6ded31b Fix typo "Syetem" -> "System" 2026-08-11 15:35:18 +03:30
patterniha 89aedc7d04 Sockopt: Fix indentation of the CustomSockopt block
https://github.com/XTLS/Xray-core/issues/6564#issuecomment-5150960771

Closes https://github.com/XTLS/Xray-core/issues/6564
2026-08-01 14:50:26 +03:30
RPRXandGitHub 5ca6f4b7d4 Xray-core v26.7.28
Sponsor & Donation & NFTs: https://github.com/XTLS/Xray-core/issues/3668
Project X Channel: https://t.me/projectXtls

Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633
Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1

VLESS Post-Quantum Encryption: https://github.com/XTLS/Xray-core/pull/5067
VLESS NFT: https://opensea.io/collection/vless

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2026-07-28 07:59:48 +00:00
RPRX 18e283909c XHTTP client: Reduce default maxConnections from 6 to 3 for anti-TSPU
https://github.com/XTLS/Xray-core/issues/6376#issuecomment-5101210849

Replaces https://github.com/XTLS/Xray-core/commit/18b85adb4e288f49a7894351c6e0f2428c0beef6
2026-07-28 07:57:55 +00:00
6ab123bf8f XMC finalmask: Add default directional padding/keep-alive packets, matching Minecraft 26.1.2 login/join/etc traffic shapes (#6487)
https://github.com/XTLS/Xray-core/pull/6487#issuecomment-5092235757

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-07-28 06:45:36 +00:00
4aba687dd3 XHTTP & gRPC servers: Get accurate localAddr (#6526)
Fixes https://github.com/XTLS/Xray-core/pull/6476

---------

Co-authored-by: echoowall <echoowall@gmail.com>
2026-07-27 13:11:25 +00:00
yiguodevandRPRX 5b1b41058e Routing: Exclude iOS from Darwin for process (#6524)
Fixes https://github.com/XTLS/Xray-core/pull/6434#issuecomment-5016175948
2026-07-27 12:40:18 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6e3322d219 Bump actions/setup-go from 6 to 7 (#6511)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6 to 7.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-17 22:28:51 +00:00
Jasper344612andGitHub 1d8eb81d70 TUN inbound: Add desc config for Windows ("Wintun" by default) (#6486)
https://github.com/XTLS/Xray-docs-next/pull/872#issuecomment-4936055488
2026-07-17 22:21:33 +00:00
e78d8ef184 REALITY server: Refine warnings in parsing configuration (#6508)
https://github.com/XTLS/Xray-core/pull/6508#issuecomment-4995787310
https://github.com/XTLS/Xray-core/pull/6507#issuecomment-4995466980

---------

Co-authored-by: Enle <42325154+EnleStars@users.noreply.github.com>
Co-authored-by: Aleksey <fraybyl@gmail.com>
2026-07-17 21:51:29 +00:00
Jasper344612andGitHub 6ce924ad56 TUN inbound: Change default name to "utunN" (10~1024) (#6485)
https://github.com/XTLS/Xray-docs-next/pull/872#issuecomment-4936853064
2026-07-16 19:53:04 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
035d438979 Bump google.golang.org/grpc from 1.82.0 to 1.82.1 (#6502)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.82.0 to 1.82.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.82.0...v1.82.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.82.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-16 18:04:03 +00:00
RPRXandGitHub 50231eaff9 Xray-core v26.7.11
Sponsor & Donation & NFTs: https://github.com/XTLS/Xray-core/issues/3668
Project X Channel: https://t.me/projectXtls

Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633
Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1

VLESS Post-Quantum Encryption: https://github.com/XTLS/Xray-core/pull/5067
VLESS NFT: https://opensea.io/collection/vless

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2026-07-11 15:51:51 +00:00
1f74c480d6 Docker: Add pre-release tag (#6470)
https://github.com/XTLS/Xray-core/pull/6470#issuecomment-4947190027

---------

Co-authored-by: KobeArthurScofield <26771058+KobeArthurScofield@users.noreply.github.com>
Co-authored-by: Meow <197331664+Meo597@users.noreply.github.com>
2026-07-11 15:45:59 +00:00
RPRXandGitHub af7eb68028 REALITY server: Set default "minClientVer": "26.3.27" (change it at your own risk)
https://github.com/XTLS/Xray-core/pull/6181#issuecomment-4567373533
https://t.me/projectXtls/3373
https://t.me/projectXtls/3378
2026-07-11 14:30:54 +00:00
Exclude0122andGitHub 35387572e0 Finalmask: Add XMC (TCP, Minecraft) (#6210)
https://github.com/XTLS/Xray-core/pull/6210#issuecomment-4573680059
https://github.com/XTLS/Xray-core/pull/6210#issuecomment-4582554889
https://github.com/XTLS/Xray-core/pull/6210#issuecomment-4937008835
2026-07-11 13:59:24 +00:00
风扇滑翔翼andGitHub 8f15190c23 QUIC sniffer: Fix potential panic on malformed QUIC packets again (#6471)
Fixes https://github.com/XTLS/Xray-core/security/advisories/GHSA-xqmr-94vq-cxvx
Fixes https://github.com/XTLS/Xray-core/security/advisories/GHSA-9h6x-9r9m-5qw2
2026-07-10 21:40:50 +00:00
风扇滑翔翼andGitHub 64fada32b5 TLS client: Pinning CA must have serverName (or higher-priority verifyPeerCertByName or outbound's address) for pinnedPeerCertSha256 (#6472)
Fixes https://github.com/XTLS/Xray-core/security/advisories/GHSA-5wf9-h793-w73c
2026-07-10 21:03:35 +00:00
0bafca9486 Stats: Fix GetOrRegister*() races (#6468)
Fixes https://github.com/XTLS/Xray-core/pull/6467#issue-4856851059

---------

Co-authored-by: Joshua Su <i@joshua.su>
2026-07-10 20:34:27 +00:00
61 changed files with 4599 additions and 114 deletions
+19
View File
@@ -64,6 +64,14 @@ jobs:
echo "Latest: '$LATEST'."
echo "LATEST=$LATEST" >>${GITHUB_ENV}
NEWEST=false
if [[ "${{ github.event_name }}" == "release" ]]; then
NEWEST=true
fi
echo "Newest: '$NEWEST'."
echo "NEWEST=$NEWEST" >>${GITHUB_ENV}
- name: Checkout code
uses: actions/checkout@v7
@@ -124,6 +132,13 @@ jobs:
${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
fi
if [[ "${{ env.NEWEST }}" == "true" ]]; then
echo "Adding 'pre-release' tag to manifest: '${{ env.FULL_IMAGE_NAME }}:pre-release'."
docker buildx imagetools create \
--tag ${{ env.FULL_IMAGE_NAME }}:pre-release \
${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
fi
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
@@ -131,3 +146,7 @@ jobs:
if [[ "${{ env.LATEST }}" == "true" ]]; then
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:latest
fi
if [[ "${{ env.NEWEST }}" == "true" ]]; then
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:pre-release
fi
+1 -1
View File
@@ -92,7 +92,7 @@ jobs:
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
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@v6
uses: actions/setup-go@v7
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@v6
uses: actions/setup-go@v7
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@v6
uses: actions/setup-go@v7
with:
go-version-file: go.mod
check-latest: true
+5 -5
View File
@@ -162,7 +162,7 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran
p := d.policy.ForLevel(user.Level)
if p.Stats.UserUplink {
name := "user>>>" + user.Email + ">>>traffic>>>uplink"
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
if c, _ := d.stats.GetOrRegisterCounter(name); c != nil {
inboundLink.Writer = &SizeStatWriter{
Counter: c,
Writer: inboundLink.Writer,
@@ -171,7 +171,7 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran
}
if p.Stats.UserDownlink {
name := "user>>>" + user.Email + ">>>traffic>>>downlink"
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
if c, _ := d.stats.GetOrRegisterCounter(name); c != nil {
outboundLink.Writer = &SizeStatWriter{
Counter: c,
Writer: outboundLink.Writer,
@@ -200,13 +200,13 @@ func WrapLink(ctx context.Context, policyManager policy.Manager, statsManager st
p := policyManager.ForLevel(user.Level)
if p.Stats.UserUplink {
name := "user>>>" + user.Email + ">>>traffic>>>uplink"
if c, _ := stats.GetOrRegisterCounter(statsManager, name); c != nil {
if c, _ := statsManager.GetOrRegisterCounter(name); c != nil {
link.Reader.(*buf.TimeoutWrapperReader).Counter = c
}
}
if p.Stats.UserDownlink {
name := "user>>>" + user.Email + ">>>traffic>>>downlink"
if c, _ := stats.GetOrRegisterCounter(statsManager, name); c != nil {
if c, _ := statsManager.GetOrRegisterCounter(name); c != nil {
link.Writer = &SizeStatWriter{
Counter: c,
Writer: link.Writer,
@@ -223,7 +223,7 @@ func WrapLink(ctx context.Context, policyManager policy.Manager, statsManager st
func trackOnlineIP(ctx context.Context, sm stats.Manager, email, ip string) {
name := "user>>>" + email + ">>>online"
if om, _ := stats.GetOrRegisterOnlineMap(sm, name); om != nil {
if om, _ := sm.GetOrRegisterOnlineMap(name); om != nil {
om.AddIP(ip)
context.AfterFunc(ctx, func() { om.RemoveIP(ip) })
}
+2 -2
View File
@@ -26,7 +26,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
if len(tag) > 0 && policy.ForSystem().Stats.InboundUplink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + tag + ">>>traffic>>>uplink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
c, _ := statsManager.GetOrRegisterCounter(name)
if c != nil {
uplinkCounter = c
}
@@ -34,7 +34,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
if len(tag) > 0 && policy.ForSystem().Stats.InboundDownlink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + tag + ">>>traffic>>>downlink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
c, _ := statsManager.GetOrRegisterCounter(name)
if c != nil {
downlinkCounter = c
}
+2 -2
View File
@@ -39,7 +39,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
if len(tag) > 0 && policy.ForSystem().Stats.OutboundUplink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "outbound>>>" + tag + ">>>traffic>>>uplink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
c, _ := statsManager.GetOrRegisterCounter(name)
if c != nil {
uplinkCounter = c
}
@@ -47,7 +47,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
if len(tag) > 0 && policy.ForSystem().Stats.OutboundDownlink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "outbound>>>" + tag + ">>>traffic>>>downlink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
c, _ := statsManager.GetOrRegisterCounter(name)
if c != nil {
downlinkCounter = c
}
+48
View File
@@ -48,6 +48,20 @@ func (m *Manager) RegisterCounter(name string) (stats.Counter, error) {
return c, nil
}
// GetOrRegisterCounter implements stats.Manager.
func (m *Manager) GetOrRegisterCounter(name string) (stats.Counter, error) {
m.access.Lock()
defer m.access.Unlock()
if c, found := m.counters[name]; found {
return c, nil
}
errors.LogDebug(context.Background(), "create new counter ", name)
c := new(Counter)
m.counters[name] = c
return c, nil
}
// UnregisterCounter implements stats.Manager.
func (m *Manager) UnregisterCounter(name string) error {
m.access.Lock()
@@ -97,6 +111,20 @@ func (m *Manager) RegisterOnlineMap(name string) (stats.OnlineMap, error) {
return om, nil
}
// GetOrRegisterOnlineMap implements stats.Manager.
func (m *Manager) GetOrRegisterOnlineMap(name string) (stats.OnlineMap, error) {
m.access.Lock()
defer m.access.Unlock()
if om, found := m.onlineMaps[name]; found {
return om, nil
}
errors.LogDebug(context.Background(), "create new OnlineMap ", name)
om := NewOnlineMap()
m.onlineMaps[name] = om
return om, nil
}
// UnregisterOnlineMap implements stats.Manager.
func (m *Manager) UnregisterOnlineMap(name string) error {
m.access.Lock()
@@ -149,6 +177,26 @@ func (m *Manager) RegisterChannel(name string) (stats.Channel, error) {
return c, nil
}
// GetOrRegisterChannel implements stats.Manager.
func (m *Manager) GetOrRegisterChannel(name string) (stats.Channel, error) {
m.access.Lock()
defer m.access.Unlock()
if c, found := m.channels[name]; found {
return c, nil
}
errors.LogDebug(context.Background(), "create new channel ", name)
c := NewChannel(&ChannelConfig{BufferSize: 64, Blocking: false})
if m.running {
// Start before publishing so no goroutine can observe an unstarted channel.
if err := c.Start(); err != nil {
return nil, err
}
}
m.channels[name] = c
return c, nil
}
// UnregisterChannel implements stats.Manager.
func (m *Manager) UnregisterChannel(name string) error {
m.access.Lock()
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build darwin
//go:build darwin && !ios
package net
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build darwin
//go:build darwin && !ios
#include "textflag.h"
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build darwin
//go:build darwin && !ios
package net
+11
View File
@@ -0,0 +1,11 @@
//go:build ios
package net
import (
"github.com/xtls/xray-core/common/errors"
)
func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (int, string, string, error) {
return 0, "", "", errors.New("process lookup is not supported on this platform")
}
+3 -1
View File
@@ -143,7 +143,9 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
if err != nil {
return nil, err
}
if len(b) < hdrLen+4+block.BlockSize() {
return nil, errNotQuic
}
cache.Clear()
mask := cache.Extend(int32(block.BlockSize()))
block.Encrypt(mask, b[hdrLen+4:hdrLen+4+len(mask)])
+2 -2
View File
@@ -19,8 +19,8 @@ import (
var (
Version_x byte = 26
Version_y byte = 6
Version_z byte = 27
Version_y byte = 7
Version_z byte = 28
)
var (
+21 -30
View File
@@ -81,6 +81,8 @@ type Manager interface {
// RegisterCounter registers a new counter to the manager. The identifier string must not be empty, and unique among other counters.
RegisterCounter(string) (Counter, error)
// GetOrRegisterCounter returns the counter by its identifier, atomically creating and registering it if absent.
GetOrRegisterCounter(string) (Counter, error)
// UnregisterCounter unregisters a counter from the manager by its identifier.
UnregisterCounter(string) error
// GetCounter returns a counter by its identifier.
@@ -91,6 +93,8 @@ type Manager interface {
// RegisterOnlineMap registers a new OnlineMap to the manager. The identifier string must not be empty, and unique among other OnlineMaps.
RegisterOnlineMap(string) (OnlineMap, error)
// GetOrRegisterOnlineMap returns the OnlineMap by its identifier, atomically creating and registering it if absent.
GetOrRegisterOnlineMap(string) (OnlineMap, error)
// UnregisterOnlineMap unregisters an OnlineMap from the manager by its identifier.
UnregisterOnlineMap(string) error
// GetOnlineMap returns an OnlineMap by its identifier.
@@ -101,6 +105,8 @@ type Manager interface {
// RegisterChannel registers a new channel to the manager. The identifier string must not be empty, and unique among other channels.
RegisterChannel(string) (Channel, error)
// GetOrRegisterChannel returns the channel by its identifier, atomically creating and registering it if absent.
GetOrRegisterChannel(string) (Channel, error)
// UnregisterChannel unregisters a channel from the manager by its identifier.
UnregisterChannel(string) error
// GetChannel returns a channel by its identifier.
@@ -110,36 +116,6 @@ type Manager interface {
GetAllOnlineUsers() []string
}
// GetOrRegisterCounter tries to get the StatCounter first. If not exist, it then tries to create a new counter.
func GetOrRegisterCounter(m Manager, name string) (Counter, error) {
counter := m.GetCounter(name)
if counter != nil {
return counter, nil
}
return m.RegisterCounter(name)
}
// GetOrRegisterOnlineMap tries to get the OnlineMap first. If not exist, it then tries to create a new OnlineMap.
func GetOrRegisterOnlineMap(m Manager, name string) (OnlineMap, error) {
onlineMap := m.GetOnlineMap(name)
if onlineMap != nil {
return onlineMap, nil
}
return m.RegisterOnlineMap(name)
}
// GetOrRegisterChannel tries to get the StatChannel first. If not exist, it then tries to create a new channel.
func GetOrRegisterChannel(m Manager, name string) (Channel, error) {
channel := m.GetChannel(name)
if channel != nil {
return channel, nil
}
return m.RegisterChannel(name)
}
// ManagerType returns the type of Manager interface. Can be used to implement common.HasType.
//
// xray:api:stable
@@ -160,6 +136,11 @@ func (NoopManager) RegisterCounter(string) (Counter, error) {
return nil, errors.New("not implemented")
}
// GetOrRegisterCounter implements Manager.
func (NoopManager) GetOrRegisterCounter(string) (Counter, error) {
return nil, errors.New("not implemented")
}
// UnregisterCounter implements Manager.
func (NoopManager) UnregisterCounter(string) error {
return nil
@@ -178,6 +159,11 @@ func (NoopManager) RegisterOnlineMap(string) (OnlineMap, error) {
return nil, errors.New("not implemented")
}
// GetOrRegisterOnlineMap implements Manager.
func (NoopManager) GetOrRegisterOnlineMap(string) (OnlineMap, error) {
return nil, errors.New("not implemented")
}
// UnregisterOnlineMap implements Manager.
func (NoopManager) UnregisterOnlineMap(string) error {
return nil
@@ -196,6 +182,11 @@ func (NoopManager) RegisterChannel(string) (Channel, error) {
return nil, errors.New("not implemented")
}
// GetOrRegisterChannel implements Manager.
func (NoopManager) GetOrRegisterChannel(string) (Channel, error) {
return nil, errors.New("not implemented")
}
// UnregisterChannel implements Manager.
func (NoopManager) UnregisterChannel(string) error {
return nil
+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.0
google.golang.org/grpc v1.82.1
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.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
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/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=
+80
View File
@@ -1,14 +1,17 @@
package conf
import (
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"net/netip"
"net/url"
"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"
@@ -22,6 +25,7 @@ import (
"github.com/xtls/xray-core/transport/internet/finalmask/sudoku"
"github.com/xtls/xray-core/transport/internet/finalmask/xdns"
"github.com/xtls/xray-core/transport/internet/finalmask/xicmp"
"github.com/xtls/xray-core/transport/internet/finalmask/xmc"
"github.com/xtls/xray-core/transport/internet/tls"
"google.golang.org/protobuf/proto"
)
@@ -67,6 +71,7 @@ var (
"header-custom": func() interface{} { return new(HeaderCustomTCP) },
"fragment": func() interface{} { return new(FragmentMask) },
"sudoku": func() interface{} { return new(Sudoku) },
"xmc": func() interface{} { return new(XMC) },
}, "type", "settings")
udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
@@ -715,6 +720,81 @@ func (c *Xdns) Build() (proto.Message, error) {
}, nil
}
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
}
func (c *XMC) Build() (proto.Message, error) {
if len(c.Profiles) == 0 {
return nil, fmt.Errorf("minecraft profiles are required")
}
if c.Password == "" {
return nil, fmt.Errorf("empty password")
}
rsaPrivateKey, err := xmc.DeriveRSAKey(c.Password)
if err != nil {
return nil, fmt.Errorf("derive minecraft rsa key: %w", err)
}
rsaPublicKey, err := x509.MarshalPKIXPublicKey(&rsaPrivateKey.PublicKey)
if err != nil {
return nil, fmt.Errorf("marshal minecraft rsa public key: %w", err)
}
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,
Hostname: c.Hostname,
RsaPrivateKey: x509.MarshalPKCS1PrivateKey(rsaPrivateKey),
RsaPublicKey: rsaPublicKey,
Profiles: profiles,
}, nil
}
type Xicmp struct {
DGRAM bool `json:"dgram"`
IPs []string `json:"ips"`
@@ -0,0 +1,36 @@
package conf
import (
"strings"
"testing"
"github.com/xtls/xray-core/transport/internet/finalmask/xmc"
)
func TestXMCBuildProfile(t *testing.T) {
built, err := (&XMC{
Password: "test-password",
Profiles: []XMCProfile{
{
Username: "TestUser",
UUID: "00112233-4455-6677-8899-aabbccddeeff",
TexturesValue: "textures-value",
TexturesSignature: "textures-signature",
},
},
}).Build()
if err != nil {
t.Fatalf("build XMC config: %v", err)
}
config := built.(*xmc.Config)
if len(config.Profiles) != 1 || len(config.Profiles[0].Uuid) != 16 {
t.Fatalf("unexpected profiles: %+v", config.Profiles)
}
}
func TestXMCBuildRequiresProfile(t *testing.T) {
_, err := (&XMC{Password: "test-password"}).Build()
if err == nil || !strings.Contains(err.Error(), "profiles are required") {
t.Fatalf("expected required profiles error, got %v", err)
}
}
+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 = 6
c.Xmux.MaxConnections.To = 6
c.Xmux.MaxConnections.From = 3
c.Xmux.MaxConnections.To = 3
c.Xmux.HMaxRequestTimes.From = 600
c.Xmux.HMaxRequestTimes.To = 900
c.Xmux.HMaxReusableSecs.From = 1800
+8 -2
View File
@@ -113,6 +113,10 @@ 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`)
}
if c.MaxClientVer != "" {
config.MaxClientVer = make([]byte, 3)
@@ -157,8 +161,10 @@ func (c *REALITYConfig) Build() (proto.Message, error) {
}
for _, sn := range config.ServerNames {
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`)
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`)
}
}
+2 -2
View File
@@ -10,7 +10,7 @@ import (
)
type CustomSockoptConfig struct {
Syetem string `json:"system"`
System string `json:"system"`
Network string `json:"network"`
Level string `json:"level"`
Opt string `json:"opt"`
@@ -124,7 +124,7 @@ func (c *SocketConfig) Build() (*internet.SocketConfig, error) {
for _, copt := range c.CustomSockopt {
customSockopt := &internet.CustomSockopt{
System: copt.Syetem,
System: copt.System,
Network: copt.Network,
Level: copt.Level,
Opt: copt.Opt,
+70 -1
View File
@@ -1,12 +1,19 @@
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"`
@@ -18,6 +25,7 @@ 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,
@@ -32,10 +40,71 @@ func (v *TunConfig) Build() (proto.Message, error) {
}
if config.Name == "" {
config.Name = "xray0"
name, err := GetAvailableTunName()
if err != nil {
return nil, err
}
config.Name = name
}
if config.Desc == "" {
config.Desc = "Wintun"
}
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 may get your IP blocked by the GFW`)
errors.LogWarning(context.Background(), `REALITY: Listening on non-443 ports will increase the likelihood of your server's IP being blocked by the GFW`)
}
}
if c.SniffingConfig != nil {
+6 -2
View File
@@ -31,13 +31,17 @@ Here is simple Xray config snippet to enable the inbound:
"port": 0,
"protocol": "tun",
"settings": {
"name": "xray0",
"MTU": 1492
"name": "utun10",
"desc": "Wintun",
"mtu": 1500
}
}
],
```
`desc` sets the Windows Wintun adapter tunnel type and defaults to `Wintun`.
It is ignored on other platforms.
## SUPPORTED FEATURES
- IPv4 and IPv6
+14 -4
View File
@@ -7,11 +7,12 @@
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 (
@@ -30,6 +31,7 @@ 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
}
@@ -113,11 +115,18 @@ 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\"\xee\x01\n" +
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\x82\x02\n" +
"\x06Config\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
"\x03MTU\x18\x02 \x01(\rR\x03MTU\x12\x18\n" +
@@ -126,7 +135,8 @@ 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\x16autoOutboundsInterfaceBL\n" +
"\x18auto_outbounds_interface\x18\a \x01(\tR\x16autoOutboundsInterface\x12\x12\n" +
"\x04desc\x18\b \x01(\tR\x04descBL\n" +
"\x12com.xray.proxy.tunP\x01Z#github.com/xtls/xray-core/proxy/tun\xaa\x02\x0eXray.Proxy.Tunb\x06proto3"
var (
+1
View File
@@ -14,4 +14,5 @@ message Config {
uint32 user_level = 5;
repeated string auto_system_routing_table = 6;
string auto_outbounds_interface = 7;
string desc = 8;
}
+2 -2
View File
@@ -65,7 +65,7 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin
if len(t.tag) > 0 && pm.ForSystem().Stats.InboundUplink {
statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + t.tag + ">>>traffic>>>uplink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
c, _ := statsManager.GetOrRegisterCounter(name)
if c != nil {
t.uplinkCounter = c
}
@@ -73,7 +73,7 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin
if len(t.tag) > 0 && pm.ForSystem().Stats.InboundDownlink {
statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + t.tag + ">>>traffic>>>downlink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
c, _ := statsManager.GetOrRegisterCounter(name)
if c != nil {
t.downlinkCounter = c
}
+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)
adapter, err := open(options.Name, options.Desc)
if err != nil {
return nil, err
}
@@ -73,12 +73,12 @@ func NewTun(options *Config) (Tun, error) {
return tun, nil
}
func open(name string) (*wintun.Adapter, error) {
func open(name, desc 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, "Xray", guid)
adapter, err := wintun.CreateAdapter(name, desc, guid)
if err == nil {
return adapter, nil
}
+2 -2
View File
@@ -57,7 +57,7 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
if len(tag) > 0 && p.ForSystem().Stats.OutboundUplink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "outbound>>>" + tag + ">>>traffic>>>uplink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
c, _ := statsManager.GetOrRegisterCounter(name)
if c != nil {
uplinkCounter = c
}
@@ -65,7 +65,7 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
if len(tag) > 0 && p.ForSystem().Stats.OutboundDownlink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "outbound>>>" + tag + ">>>traffic>>>downlink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
c, _ := statsManager.GetOrRegisterCounter(name)
if c != nil {
downlinkCounter = c
}
+2 -2
View File
@@ -66,7 +66,7 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) {
if len(tag) > 0 && p.ForSystem().Stats.InboundUplink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + tag + ">>>traffic>>>uplink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
c, _ := statsManager.GetOrRegisterCounter(name)
if c != nil {
uplinkCounter = c
}
@@ -74,7 +74,7 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) {
if len(tag) > 0 && p.ForSystem().Stats.InboundDownlink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + tag + ">>>traffic>>>downlink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
c, _ := statsManager.GetOrRegisterCounter(name)
if c != nil {
downlinkCounter = c
}
+138
View File
@@ -0,0 +1,138 @@
package xmc
// Copied from https://github.com/Tnze/go-mc/blob/539b4a3a7f030332eb58b8a946116ae7907630d2/net/CFB8/cfb8.go
import (
"crypto/cipher"
"crypto/subtle"
"unsafe"
)
type cfb8 struct {
c cipher.Block
blockSize int
ivPos int
iv []byte
de bool
}
func newCFB8Decrypt(c cipher.Block, iv []byte) *cfb8 {
return newCFB8(c, iv, true)
}
func newCFB8Encrypt(c cipher.Block, iv []byte) *cfb8 {
return newCFB8(c, iv, false)
}
func newCFB8(c cipher.Block, iv []byte, de bool) *cfb8 {
cp := make([]byte, len(iv)*3)
copy(cp, iv)
return &cfb8{
c: c,
blockSize: c.BlockSize(),
iv: cp,
de: de,
}
}
func (cf *cfb8) XORKeyStream(dst, src []byte) {
if len(src) == 0 {
return
}
if len(dst) < len(src) {
panic("cfb8: output smaller than input")
}
// If dst and src does not overlap in first block size,
// and the length of src is greater than 2*blockSize,
// we can use an optimized implementation.
if len(src) > cf.blockSize<<1 &&
(uintptr(unsafe.Pointer(&dst[0]))+uintptr(cf.blockSize) <= uintptr(unsafe.Pointer(&src[0])) ||
uintptr(unsafe.Pointer(&src[0]))+uintptr(len(src)) <= uintptr(unsafe.Pointer(&dst[0]))) {
// encrypt/decrypt first blockSize bytes
// After this, the IV will come to the same as
// the last blockSize of ciphertext, so
// we can reuse them without copy.
cf.xorKeyStream(dst, src[:cf.blockSize])
var ciphertext []byte
if cf.de {
ciphertext = src
} else {
ciphertext = dst
}
dst = dst[cf.blockSize:]
src = src[cf.blockSize:]
iv := cf.iv
_ = iv[0] // bounds check hint to compiler; see golang.org/issue/14808
var (
i int
val byte
)
dst = dst[:len(src)]
if cf.de && // and requires to be non-overlapping at all
uintptr(unsafe.Pointer(&dst[0])) <= uintptr(unsafe.Pointer(&src[len(src)-1])) &&
uintptr(unsafe.Pointer(&src[0])) <= uintptr(unsafe.Pointer(&dst[len(dst)-1])) {
for i = 0; i < len(src)-cf.blockSize; i += 1 {
cf.c.Encrypt(dst[i:], ciphertext[i:])
}
subtle.XORBytes(dst, src[:i], dst)
for ; i < len(src); i += 1 {
cf.c.Encrypt(iv, ciphertext[i:])
dst[i] = src[i] ^ iv[0]
}
} else {
_ = ciphertext[len(src)]
for i, val = range src {
cf.c.Encrypt(iv, ciphertext[i:])
dst[i] = val ^ iv[0]
}
// for-range does not increase i in the last loop,
// compared to the classic for clause
i += 1
}
// copy the current IV for next operation
copy(iv, ciphertext[i:i+cf.blockSize])
cf.ivPos = 0
return
}
cf.xorKeyStream(dst, src)
}
func (cf *cfb8) xorKeyStream(dst, src []byte) {
dst = dst[:len(src)] // remove bounds check in loop
for i, val := range src {
posPlusBlockSize := cf.ivPos + cf.blockSize
// fast mod; 2*blockSize must be a non-negative integer power of 2
tempPos := posPlusBlockSize & (cf.blockSize<<1 - 1)
// reuse space to store encrypted block
cf.c.Encrypt(cf.iv[tempPos:], cf.iv[cf.ivPos:])
// Only the first byte of the encrypted block is used
// for encryption/decryption, other bytes are ignored.
val ^= cf.iv[tempPos]
if cf.ivPos == cf.blockSize<<1 {
// bound reached; move to next round for next operation
// copy next block to the start of the ring buffer
copy(cf.iv, cf.iv[cf.ivPos+1:])
// insert the encrypted byte to the end of IV
if cf.de {
cf.iv[cf.blockSize-1] = src[i]
} else {
cf.iv[cf.blockSize-1] = val
}
cf.ivPos = 0
} else {
// insert the encrypted byte to the end of IV
if cf.de {
cf.iv[posPlusBlockSize] = src[i]
} else {
cf.iv[posPlusBlockSize] = val
}
// move to next block
cf.ivPos += 1
}
dst[i] = val
}
}
+287
View File
@@ -0,0 +1,287 @@
package xmc
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"fmt"
"io"
"math/big"
"net"
"strconv"
"sync"
"time"
)
type clientConn struct {
reader io.Reader
writer io.Writer
c net.Conn
state clientState
handshakeLock sync.Mutex
lifecycleMu sync.Mutex
closed bool
profiles []loginProfile
password string
rsaPublicKey []byte
hostname string
paddingSchedule []paddingTurn
packet *packetStream
deadlines *connectionDeadlines
}
type clientState int
var (
clientStateHandshake clientState = 1
clientStateProxy clientState = 2
)
func newClientConn(c net.Conn, profiles []loginProfile, password string, rsaPublicKey []byte, hostname string) (*clientConn, error) {
if len(rsaPublicKey) == 0 {
return nil, fmt.Errorf("empty rsa public key")
}
if len(profiles) == 0 {
return nil, fmt.Errorf("empty profiles")
}
paddingSchedule, err := newClientPaddingSchedule2612()
if err != nil {
return nil, fmt.Errorf("select padding profile: %w", err)
}
return &clientConn{
reader: bufio.NewReader(c),
writer: c,
c: c,
state: clientStateHandshake,
handshakeLock: sync.Mutex{},
profiles: profiles,
password: password,
rsaPublicKey: rsaPublicKey,
hostname: hostname,
paddingSchedule: paddingSchedule,
deadlines: newConnectionDeadlines(c),
}, nil
}
func (c *clientConn) handshake() error {
c.handshakeLock.Lock()
defer c.handshakeLock.Unlock()
if c.state != clientStateHandshake {
return nil
}
if err := c.deadlines.beginHandshake(); err != nil {
return fmt.Errorf("set deadline: %w", err)
}
defer func() { _ = c.deadlines.endHandshake() }()
var (
protocolVersion Varint = Varint(775)
serverAddress String = String(c.hostname)
serverPort UnsignedShort = UnsignedShort(25565)
nextState Varint = Varint(2)
)
host, portString, err := net.SplitHostPort(c.c.RemoteAddr().String())
if err == nil {
port, err := strconv.Atoi(portString)
if err == nil {
serverPort = UnsignedShort(port)
}
if serverAddress == "" {
serverAddress = String(host)
}
}
err = writePacket(c.writer, 0x00, &protocolVersion, &serverAddress, &serverPort, &nextState)
if err != nil {
return fmt.Errorf("write handshake packet: %w", err)
}
// Login Start
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)
err = writePacket(c.writer, 0x00, &username, &selectedProfile.UUID)
if err != nil {
return fmt.Errorf("write login start: %w", err)
}
// Encryption Request
pkt, err := readPacket(c.reader)
if err != nil {
return fmt.Errorf("read encryption request: %w", err)
}
if pkt.packetID != 0x01 {
return fmt.Errorf("bad encrypt request packet id")
}
var (
serverId String
publicKey Bytes
verifyToken Bytes
)
err = pkt.readFields(&serverId, &publicKey, &verifyToken)
if err != nil {
return fmt.Errorf("read encryption request fields: %w", err)
}
if !bytes.Equal(publicKey, c.rsaPublicKey) {
return fmt.Errorf("server public key mismatch")
}
k, err := x509.ParsePKIXPublicKey(publicKey)
if err != nil {
return fmt.Errorf("parse server public key: %w", err)
}
rsaPublicKey, ok := k.(*rsa.PublicKey)
if !ok {
return fmt.Errorf("parse server public key: not rsa")
}
sharedSecret := make([]byte, 16)
if _, err = rand.Read(sharedSecret); err != nil {
return fmt.Errorf("generate shared secret: %w", err)
}
encryptedSharedSecret, err := rsa.EncryptPKCS1v15(rand.Reader, rsaPublicKey, sharedSecret)
if err != nil {
return fmt.Errorf("encrypt shared secret: %w", err)
}
verifyToken = append(verifyToken, []byte(c.password)...) // append pre-shared password
encryptedVerifyToken, err := rsa.EncryptPKCS1v15(rand.Reader, rsaPublicKey, verifyToken)
if err != nil {
return fmt.Errorf("encrypt verify token: %w", err)
}
// Send Encryption Response
err = writePacket(
c.writer,
0x01,
(*Bytes)(&encryptedSharedSecret),
(*Bytes)(&encryptedVerifyToken),
)
if err != nil {
return fmt.Errorf("write encryption response: %w", err)
}
// Enable encryption
c.reader, err = newCryptoReader(c.reader, sharedSecret)
if err != nil {
return fmt.Errorf("new crypto reader: %w", err)
}
c.writer, err = newCryptoWriter(c.writer, sharedSecret)
if err != nil {
return fmt.Errorf("new crypto writer: %w", err)
}
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
}
func (c *clientConn) Read(b []byte) (int, error) {
err := c.handshake()
if err != nil {
return 0, fmt.Errorf("handshake: %w", err)
}
return c.reader.Read(b)
}
func (c *clientConn) Write(b []byte) (int, error) {
err := c.handshake()
if err != nil {
return 0, fmt.Errorf("handshake: %w", err)
}
return c.writer.Write(b)
}
func (c *clientConn) Close() error {
c.lifecycleMu.Lock()
c.closed = true
packet := c.packet
c.lifecycleMu.Unlock()
if packet != nil {
packet.Stop()
}
return c.c.Close()
}
func (c *clientConn) LocalAddr() net.Addr {
return c.c.LocalAddr()
}
func (c *clientConn) RemoteAddr() net.Addr {
return c.c.RemoteAddr()
}
func (c *clientConn) SetDeadline(t time.Time) error {
return c.deadlines.setDeadline(t)
}
func (c *clientConn) SetReadDeadline(t time.Time) error {
return c.deadlines.setReadDeadline(t)
}
func (c *clientConn) SetWriteDeadline(t time.Time) error {
return c.deadlines.setWriteDeadline(t)
}
@@ -0,0 +1,35 @@
package xmc
import (
"fmt"
"net"
)
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)
if err != nil {
return nil, fmt.Errorf("minecraft finalmask: %w", err)
}
return cc, nil
}
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)
if err != nil {
return nil, fmt.Errorf("minecraft finalmask: %w", err)
}
return cc, nil
}
@@ -0,0 +1,238 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v6.33.5
// source: transport/internet/finalmask/xmc/config.proto
package xmc
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type 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"`
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]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Config) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_finalmask_xmc_config_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) {
return file_transport_internet_finalmask_xmc_config_proto_rawDescGZIP(), []int{1}
}
func (x *Config) GetPassword() string {
if x != nil {
return x.Password
}
return ""
}
func (x *Config) GetRsaPrivateKey() []byte {
if x != nil {
return x.RsaPrivateKey
}
return nil
}
func (x *Config) GetRsaPublicKey() []byte {
if x != nil {
return x.RsaPublicKey
}
return nil
}
func (x *Config) GetHostname() string {
if x != nil {
return x.Hostname
}
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" +
"\x06Config\x12\x1a\n" +
"\bpassword\x18\x01 \x01(\tR\bpassword\x12&\n" +
"\x0frsa_private_key\x18\b \x01(\fR\rrsaPrivateKey\x12$\n" +
"\x0ersa_public_key\x18\t \x01(\fR\frsaPublicKey\x12\x1a\n" +
"\bhostname\x18\n" +
" \x01(\tR\bhostname\x12J\n" +
"\bprofiles\x18\v \x03(\v2..xray.transport.internet.finalmask.xmc.ProfileR\bprofilesJ\x04\b\x02\x10\x03B\x91\x01\n" +
")com.xray.transport.internet.finalmask.xmcP\x01Z:github.com/xtls/xray-core/transport/internet/finalmask/xmc\xaa\x02%Xray.Transport.Internet.Finalmask.XMCb\x06proto3"
var (
file_transport_internet_finalmask_xmc_config_proto_rawDescOnce sync.Once
file_transport_internet_finalmask_xmc_config_proto_rawDescData []byte
)
func file_transport_internet_finalmask_xmc_config_proto_rawDescGZIP() []byte {
file_transport_internet_finalmask_xmc_config_proto_rawDescOnce.Do(func() {
file_transport_internet_finalmask_xmc_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_xmc_config_proto_rawDesc), len(file_transport_internet_finalmask_xmc_config_proto_rawDesc)))
})
return file_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_goTypes = []any{
(*Profile)(nil), // 0: xray.transport.internet.finalmask.xmc.Profile
(*Config)(nil), // 1: xray.transport.internet.finalmask.xmc.Config
}
var file_transport_internet_finalmask_xmc_config_proto_depIdxs = []int32{
0, // 0: xray.transport.internet.finalmask.xmc.Config.profiles:type_name -> xray.transport.internet.finalmask.xmc.Profile
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_transport_internet_finalmask_xmc_config_proto_init() }
func file_transport_internet_finalmask_xmc_config_proto_init() {
if File_transport_internet_finalmask_xmc_config_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_xmc_config_proto_rawDesc), len(file_transport_internet_finalmask_xmc_config_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_transport_internet_finalmask_xmc_config_proto_goTypes,
DependencyIndexes: file_transport_internet_finalmask_xmc_config_proto_depIdxs,
MessageInfos: file_transport_internet_finalmask_xmc_config_proto_msgTypes,
}.Build()
File_transport_internet_finalmask_xmc_config_proto = out.File
file_transport_internet_finalmask_xmc_config_proto_goTypes = nil
file_transport_internet_finalmask_xmc_config_proto_depIdxs = nil
}
@@ -0,0 +1,26 @@
syntax = "proto3";
package xray.transport.internet.finalmask.xmc;
option csharp_namespace = "Xray.Transport.Internet.Finalmask.XMC";
option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/xmc";
option java_package = "com.xray.transport.internet.finalmask.xmc";
option java_multiple_files = true;
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;
bytes rsa_private_key = 8;
bytes rsa_public_key = 9;
string hostname = 10;
repeated Profile profiles = 11;
}
@@ -0,0 +1,85 @@
package xmc
import (
"net"
"sync"
"time"
)
const handshakeTimeout = 2 * time.Minute
type connectionDeadlines struct {
mu sync.Mutex
c net.Conn
read time.Time
write time.Time
handshake time.Time
}
func newConnectionDeadlines(c net.Conn) *connectionDeadlines {
return &connectionDeadlines{c: c}
}
func (d *connectionDeadlines) beginHandshake() error {
d.mu.Lock()
defer d.mu.Unlock()
d.handshake = time.Now().Add(handshakeTimeout)
if err := d.applyLocked(); err != nil {
d.handshake = time.Time{}
_ = d.applyLocked()
return err
}
return nil
}
func (d *connectionDeadlines) endHandshake() error {
d.mu.Lock()
defer d.mu.Unlock()
d.handshake = time.Time{}
return d.applyLocked()
}
func (d *connectionDeadlines) setDeadline(t time.Time) error {
d.mu.Lock()
defer d.mu.Unlock()
d.read = t
d.write = t
return d.applyLocked()
}
func (d *connectionDeadlines) setReadDeadline(t time.Time) error {
d.mu.Lock()
defer d.mu.Unlock()
d.read = t
return d.c.SetReadDeadline(earlierDeadline(d.read, d.handshake))
}
func (d *connectionDeadlines) setWriteDeadline(t time.Time) error {
d.mu.Lock()
defer d.mu.Unlock()
d.write = t
return d.c.SetWriteDeadline(earlierDeadline(d.write, d.handshake))
}
func (d *connectionDeadlines) applyLocked() error {
if err := d.c.SetReadDeadline(earlierDeadline(d.read, d.handshake)); err != nil {
return err
}
return d.c.SetWriteDeadline(earlierDeadline(d.write, d.handshake))
}
func earlierDeadline(user, internal time.Time) time.Time {
if internal.IsZero() {
return user
}
if user.IsZero() || internal.Before(user) {
return internal
}
return user
}
@@ -0,0 +1,81 @@
package xmc
import (
"net"
"sync"
"testing"
"time"
)
func TestConnectionDeadlinesRestoreCallerValues(t *testing.T) {
client, server := net.Pipe()
defer client.Close()
defer server.Close()
recording := &deadlineRecordingConn{Conn: client}
deadlines := newConnectionDeadlines(recording)
callerDeadline := time.Now().Add(10 * time.Minute)
if err := deadlines.setDeadline(callerDeadline); err != nil {
t.Fatal(err)
}
if err := deadlines.beginHandshake(); err != nil {
t.Fatal(err)
}
read, write := recording.currentDeadlines()
if !read.Before(callerDeadline) || !write.Before(callerDeadline) {
t.Fatalf("handshake deadlines = %s/%s, caller = %s", read, write, callerDeadline)
}
shortReadDeadline := time.Now().Add(time.Second)
if err := deadlines.setReadDeadline(shortReadDeadline); err != nil {
t.Fatal(err)
}
read, _ = recording.currentDeadlines()
if !read.Equal(shortReadDeadline) {
t.Fatalf("read deadline = %s, want %s", read, shortReadDeadline)
}
if err := deadlines.endHandshake(); err != nil {
t.Fatal(err)
}
read, write = recording.currentDeadlines()
if !read.Equal(shortReadDeadline) || !write.Equal(callerDeadline) {
t.Fatalf("restored deadlines = %s/%s, want %s/%s", read, write, shortReadDeadline, callerDeadline)
}
}
type deadlineRecordingConn struct {
net.Conn
mu sync.Mutex
read time.Time
write time.Time
}
func (c *deadlineRecordingConn) SetDeadline(t time.Time) error {
c.mu.Lock()
c.read = t
c.write = t
c.mu.Unlock()
return c.Conn.SetDeadline(t)
}
func (c *deadlineRecordingConn) SetReadDeadline(t time.Time) error {
c.mu.Lock()
c.read = t
c.mu.Unlock()
return c.Conn.SetReadDeadline(t)
}
func (c *deadlineRecordingConn) SetWriteDeadline(t time.Time) error {
c.mu.Lock()
c.write = t
c.mu.Unlock()
return c.Conn.SetWriteDeadline(t)
}
func (c *deadlineRecordingConn) currentDeadlines() (time.Time, time.Time) {
c.mu.Lock()
defer c.mu.Unlock()
return c.read, c.write
}
@@ -0,0 +1,103 @@
package xmc
import (
"crypto/rsa"
"crypto/sha256"
"fmt"
"math/big"
)
type sha256Stream struct {
seed []byte
counter uint64
buf []byte
}
func newSHA256Stream(seed []byte) *sha256Stream {
return &sha256Stream{
seed: seed,
}
}
func (s *sha256Stream) Read(p []byte) (n int, err error) {
for len(p) > len(s.buf) {
h := sha256.New()
h.Write(s.seed)
h.Write([]byte(fmt.Sprintf("-%d", s.counter)))
s.counter++
s.buf = append(s.buf, h.Sum(nil)...)
}
n = copy(p, s.buf)
s.buf = s.buf[n:]
return n, nil
}
func derivePrime(stream *sha256Stream) *big.Int {
pBytes := make([]byte, 64) // 512 bits
_, _ = stream.Read(pBytes)
pBytes[0] |= 0xc0 // ensure it is big enough so p*q is 1024 bits
pBytes[63] |= 0x01 // ensure odd
p := new(big.Int).SetBytes(pBytes)
for {
if p.ProbablyPrime(20) {
pMinus1 := new(big.Int).Sub(p, big.NewInt(1))
e := big.NewInt(65537)
gcd := new(big.Int).GCD(nil, nil, pMinus1, e)
if gcd.Cmp(big.NewInt(1)) == 0 {
return p
}
}
p.Add(p, big.NewInt(2))
}
}
// DeriveRSAKey derives a 1024-bit RSA private key from a password.
func DeriveRSAKey(password string) (*rsa.PrivateKey, error) {
seed := []byte(password)
pStream := newSHA256Stream(append(seed, []byte("-p-prime")...))
qStream := newSHA256Stream(append(seed, []byte("-q-prime")...))
p := derivePrime(pStream)
q := derivePrime(qStream)
// ensure p != q (if they are, let's search q further)
for p.Cmp(q) == 0 {
q.Add(q, big.NewInt(2))
for {
if q.ProbablyPrime(20) {
qMinus1 := new(big.Int).Sub(q, big.NewInt(1))
e := big.NewInt(65537)
gcd := new(big.Int).GCD(nil, nil, qMinus1, e)
if gcd.Cmp(big.NewInt(1)) == 0 {
break
}
}
q.Add(q, big.NewInt(2))
}
}
n := new(big.Int).Mul(p, q)
pMinus1 := new(big.Int).Sub(p, big.NewInt(1))
qMinus1 := new(big.Int).Sub(q, big.NewInt(1))
totient := new(big.Int).Mul(pMinus1, qMinus1)
e := big.NewInt(65537)
d := new(big.Int).ModInverse(e, totient)
if d == nil {
return nil, fmt.Errorf("failed to compute mod inverse")
}
priv := &rsa.PrivateKey{
PublicKey: rsa.PublicKey{
N: n,
E: 65537,
},
D: d,
Primes: []*big.Int{p, q},
}
priv.Precompute()
return priv, nil
}
@@ -0,0 +1,58 @@
package xmc
import (
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"testing"
)
func TestDeriveRSAKey(t *testing.T) {
password := "my-very-secret-password-12345"
key1, err := DeriveRSAKey(password)
if err != nil {
t.Fatalf("failed to derive key: %v", err)
}
err = key1.Validate()
if err != nil {
t.Fatalf("key is not valid RSA key: %v", err)
}
key2, err := DeriveRSAKey(password)
if err != nil {
t.Fatalf("failed to derive key second time: %v", err)
}
// Verify determinism
if key1.D.Cmp(key2.D) != 0 || key1.N.Cmp(key2.N) != 0 {
t.Errorf("derived keys are not identical for the same password")
}
// Verify different passwords yield different keys
keyDifferent, err := DeriveRSAKey(password + "-different")
if err != nil {
t.Fatalf("failed to derive different key: %v", err)
}
if key1.D.Cmp(keyDifferent.D) == 0 || key1.N.Cmp(keyDifferent.N) == 0 {
t.Errorf("derived keys are identical for different passwords")
}
}
func TestDeriveRSAKeyGoldenPrivateKey(t *testing.T) {
const password = "deterministic-rsa-key-golden"
const wantPKCS1DERHash = "3a8c4ad56a6fb42dab73c4d5fc3af754460a2db1441edc0970cbc7f4e0798d2f"
key, err := DeriveRSAKey(password)
if err != nil {
t.Fatalf("failed to derive key: %v", err)
}
gotHash := sha256.Sum256(x509.MarshalPKCS1PrivateKey(key))
got := hex.EncodeToString(gotHash[:])
if got != wantPKCS1DERHash {
t.Fatalf("derived private key changed\nwant sha256: %s\n got sha256: %s", wantPKCS1DERHash, got)
}
}
@@ -0,0 +1,390 @@
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) {
t.Helper()
key, err := DeriveRSAKey(password)
if err != nil {
t.Fatalf("failed to derive rsa key: %v", err)
}
publicKey, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
if err != nil {
t.Fatalf("failed to marshal public key: %v", err)
}
return x509.MarshalPKCS1PrivateKey(key), publicKey
}
func 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 {
t.Fatalf("failed to listen: %v", err)
}
defer ln.Close()
password := "super-secure-shared-key-12345"
profiles := []loginProfile{testLoginProfile("test_user")}
privateKey, publicKey := deriveTestRSAKey(t, password)
go func() {
rawConn, err := ln.Accept()
if err != nil {
return
}
defer rawConn.Close()
server, err := wrapConnServer(rawConn, profiles, password, privateKey, publicKey)
if err != nil {
t.Errorf("failed to wrap server: %v", err)
return
}
buf := make([]byte, 1024)
n, err := server.Read(buf)
if err != nil {
t.Errorf("server read error: %v", err)
return
}
if !bytes.Equal(buf[:n], []byte("hello server")) {
t.Errorf("unexpected payload from client: %s", string(buf[:n]))
return
}
_, err = server.Write([]byte("hello client"))
if err != nil {
t.Errorf("server write error: %v", err)
return
}
}()
clientRaw, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
t.Fatalf("failed to dial: %v", err)
}
defer clientRaw.Close()
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
if err != nil {
t.Fatalf("failed to create client: %v", err)
}
_, err = client.Write([]byte("hello server"))
if err != nil {
t.Fatalf("client write error: %v", err)
}
buf := make([]byte, 1024)
n, err := client.Read(buf)
if err != nil {
t.Fatalf("client read error: %v", err)
}
if !bytes.Equal(buf[:n], []byte("hello client")) {
t.Errorf("unexpected payload from server: %s", string(buf[:n]))
}
}
func TestHandshakePasswordMismatch(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
defer ln.Close()
clientPassword := "client-secret-123"
serverPassword := "server-secret-456"
profiles := []loginProfile{testLoginProfile("test_user")}
serverPrivateKey, serverPublicKey := deriveTestRSAKey(t, serverPassword)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
rawConn, err := ln.Accept()
if err != nil {
return
}
defer rawConn.Close()
server, err := wrapConnServer(rawConn, profiles, serverPassword, serverPrivateKey, serverPublicKey)
if err != nil {
// Wrapping is synchronous and shouldn't fail initially simply because key derivation works with any string
t.Logf("wrapped server: %v", err)
}
// When client sends data, handshake happens and should fail
buf := make([]byte, 1024)
_, err = server.Read(buf)
if err == nil {
t.Errorf("expected handshake to fail due to password mismatch, but it succeeded")
} else {
t.Logf("server read failed as expected: %v", err)
}
}()
clientRaw, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
t.Fatalf("failed to dial: %v", err)
}
defer clientRaw.Close()
client, err := newClientConn(clientRaw, profiles, 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")
}
wg.Wait()
// Check if we lost connection or received error
t.Log("Handshake mismatch tested")
}
func TestHandshakeNetPipeWithKeepAlive(t *testing.T) {
clientRaw, serverRaw := net.Pipe()
defer clientRaw.Close()
defer serverRaw.Close()
const password = "net-pipe-shared-key"
profiles := []loginProfile{testLoginProfile("pipe_user")}
privateKey, publicKey := deriveTestRSAKey(t, password)
serverDone := make(chan error, 1)
go func() {
server, err := wrapConnServer(serverRaw, profiles, password, privateKey, publicKey)
if err != nil {
serverDone <- err
return
}
request := make([]byte, len("hello server"))
if _, err = io.ReadFull(server, request); err != nil {
serverDone <- fmt.Errorf("read request: %w", err)
return
}
if string(request) != "hello server" {
serverDone <- fmt.Errorf("unexpected request: %q", request)
return
}
followupDone := make(chan error, 1)
go func() {
followup := make([]byte, len("after keepalive"))
_, readErr := io.ReadFull(server, followup)
if readErr == nil && string(followup) != "after keepalive" {
readErr = fmt.Errorf("unexpected followup: %q", followup)
}
followupDone <- readErr
}()
if err = server.packet.writeKeepAlive(Long(42)); err != nil {
serverDone <- fmt.Errorf("write keep-alive: %w", err)
return
}
if _, err = server.Write([]byte("hello client")); err != nil {
serverDone <- fmt.Errorf("write response: %w", err)
return
}
serverDone <- <-followupDone
}()
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
if err != nil {
t.Fatal(err)
}
if _, err = client.Write([]byte("hello server")); err != nil {
t.Fatalf("write request: %v", err)
}
response := make([]byte, len("hello client"))
if _, err = io.ReadFull(client, response); err != nil {
t.Fatalf("read response: %v", err)
}
if string(response) != "hello client" {
t.Fatalf("unexpected response: %q", response)
}
if _, err = client.Write([]byte("after keepalive")); err != nil {
t.Fatalf("write followup: %v", err)
}
select {
case err = <-serverDone:
if err != nil {
t.Fatal(err)
}
case <-time.After(5 * time.Second):
t.Fatal("net.Pipe handshake timed out")
}
}
func TestStatusQueryUnaffected(t *testing.T) {
clientRaw, serverRaw := net.Pipe()
defer clientRaw.Close()
defer serverRaw.Close()
const password = "status-shared-key"
profiles := []loginProfile{testLoginProfile("status_user")}
privateKey, publicKey := deriveTestRSAKey(t, password)
serverDone := make(chan error, 1)
go func() {
server, err := wrapConnServer(serverRaw, profiles, password, privateKey, publicKey)
if err == nil {
err = server.handshake()
}
serverDone <- err
}()
protocolVersion := Varint(775)
serverAddress := String("localhost")
serverPort := UnsignedShort(25565)
nextState := Varint(1)
if err := writePacket(clientRaw, 0x00, &protocolVersion, &serverAddress, &serverPort, &nextState); err != nil {
t.Fatal(err)
}
if err := writePacket(clientRaw, 0x00); err != nil {
t.Fatal(err)
}
response, err := readPacket(clientRaw)
if err != nil {
t.Fatal(err)
}
if response.packetID != 0x00 {
t.Fatalf("status packet id = %d", response.packetID)
}
var responseJSON String
if err = response.readFields(&responseJSON); err != nil {
t.Fatal(err)
}
if string(responseJSON) != statusResponse {
t.Fatalf("status response = %q", responseJSON)
}
payload := Long(0x0102030405060708)
if err = writePacket(clientRaw, 0x01, &payload); err != nil {
t.Fatal(err)
}
pong, err := readPacket(clientRaw)
if err != nil {
t.Fatal(err)
}
var receivedPayload Long
if pong.packetID != 0x01 {
t.Fatalf("pong packet id = %d", pong.packetID)
}
if err = pong.readFields(&receivedPayload); err != nil {
t.Fatal(err)
}
if receivedPayload != payload {
t.Fatalf("pong payload = %x", receivedPayload)
}
select {
case err = <-serverDone:
if err == nil || !strings.Contains(err.Error(), "ping") {
t.Fatalf("server error = %v", err)
}
case <-time.After(time.Second):
t.Fatal("status handshake timed out")
}
}
func TestClientHandshakeHonorsCallerDeadline(t *testing.T) {
clientRaw, serverRaw := net.Pipe()
defer clientRaw.Close()
defer serverRaw.Close()
const password = "deadline-shared-key"
profiles := []loginProfile{testLoginProfile("deadline_user")}
_, publicKey := deriveTestRSAKey(t, password)
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
if err != nil {
t.Fatal(err)
}
if err = client.SetDeadline(time.Now().Add(30 * time.Millisecond)); err != nil {
t.Fatal(err)
}
started := time.Now()
_, err = client.Write([]byte("blocked"))
var netErr net.Error
if !errors.As(err, &netErr) || !netErr.Timeout() {
t.Fatalf("error = %v, want network timeout", err)
}
if elapsed := time.Since(started); elapsed > 500*time.Millisecond {
t.Fatalf("caller deadline took %s", elapsed)
}
}
func TestClientCloseInterruptsHandshake(t *testing.T) {
clientRaw, serverRaw := net.Pipe()
defer serverRaw.Close()
const password = "close-shared-key"
profiles := []loginProfile{testLoginProfile("close_user")}
_, publicKey := deriveTestRSAKey(t, password)
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
if err != nil {
t.Fatal(err)
}
done := make(chan error, 1)
go func() {
_, writeErr := client.Write([]byte("blocked"))
done <- writeErr
}()
time.Sleep(20 * time.Millisecond)
if err = client.Close(); err != nil {
t.Fatal(err)
}
select {
case err = <-done:
if err == nil {
t.Fatal("handshake unexpectedly succeeded after close")
}
case <-time.After(time.Second):
t.Fatal("close did not interrupt handshake")
}
}
func TestValidateLoginAcknowledgedPacketRejectsData(t *testing.T) {
if err := validateLoginAcknowledgedPacket(&mcPacket{packetID: 0x03}); err != nil {
t.Fatalf("valid login acknowledged packet: %v", err)
}
if err := validateLoginAcknowledgedPacket(&mcPacket{packetID: 0x03, data: []byte{0x00}}); err == nil {
t.Fatal("login acknowledged packet with trailing data was accepted")
}
}
@@ -0,0 +1,184 @@
package xmc
import (
"bytes"
"fmt"
"io"
"sync"
"sync/atomic"
"time"
)
const (
configurationClientboundCustomPayload = 0x01
configurationServerboundCustomPayload = 0x02
configurationKeepAlive = 0x04
packetChannel = "xmc:data"
maxPacketData = 24 * 1024
keepAlivePeriod = 15 * time.Second
)
// packetStream carries the raw proxy byte stream in Minecraft configuration
// custom payload packets. The configuration state provides bidirectional
// payload packets and keep-alives without requiring version-specific world data.
type packetStream struct {
reader io.Reader
writer io.Writer
isClient bool
readMu sync.Mutex
writeMu sync.Mutex
pending []byte
keepAliveID atomic.Int64
done chan struct{}
stopOnce sync.Once
}
func newPacketStream(reader io.Reader, writer io.Writer, isClient bool) *packetStream {
s := &packetStream{
reader: reader,
writer: writer,
isClient: isClient,
done: make(chan struct{}),
}
if !isClient {
go s.keepAliveLoop()
}
return s
}
func (s *packetStream) Read(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
s.readMu.Lock()
defer s.readMu.Unlock()
if len(s.pending) > 0 {
n := copy(p, s.pending)
s.pending = s.pending[n:]
return n, nil
}
for {
packet, err := readPacket(s.reader)
if err != nil {
return 0, fmt.Errorf("read minecraft packet stream: %w", err)
}
if packet.packetID == s.remoteCustomPayloadID() {
payload, ok, err := parseCustomPayload(packet)
if err != nil {
return 0, err
}
if !ok || len(payload) == 0 {
continue
}
n := copy(p, payload)
if n < len(payload) {
s.pending = append(s.pending[:0], payload[n:]...)
}
return n, nil
}
if packet.packetID == configurationKeepAlive {
var id Long
if err := packet.readFields(&id); err != nil {
return 0, fmt.Errorf("read minecraft keep-alive: %w", err)
}
if s.isClient {
if err := s.writeKeepAlive(id); err != nil {
return 0, err
}
}
}
}
}
func (s *packetStream) Write(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
s.writeMu.Lock()
defer s.writeMu.Unlock()
written := 0
for written < len(p) {
end := written + maxPacketData
if end > len(p) {
end = len(p)
}
channel := String(packetChannel)
payload := RestBytes(p[written:end])
if err := writePacket(s.writer, s.localCustomPayloadID(), &channel, &payload); err != nil {
return written, fmt.Errorf("write minecraft custom payload: %w", err)
}
written = end
}
return written, nil
}
func (s *packetStream) Stop() {
s.stopOnce.Do(func() { close(s.done) })
}
func (s *packetStream) localCustomPayloadID() int {
if s.isClient {
return configurationServerboundCustomPayload
}
return configurationClientboundCustomPayload
}
func (s *packetStream) remoteCustomPayloadID() int {
if s.isClient {
return configurationClientboundCustomPayload
}
return configurationServerboundCustomPayload
}
func parseCustomPayload(packet *mcPacket) ([]byte, bool, error) {
r := bytes.NewReader(packet.data)
var channel String
if err := channel.readFrom(r); err != nil {
return nil, false, fmt.Errorf("read minecraft custom payload channel: %w", err)
}
if string(channel) != packetChannel {
return nil, false, nil
}
payload := make([]byte, r.Len())
if _, err := io.ReadFull(r, payload); err != nil {
return nil, false, fmt.Errorf("read minecraft custom payload data: %w", err)
}
return payload, true, nil
}
func (s *packetStream) writeKeepAlive(id Long) error {
s.writeMu.Lock()
defer s.writeMu.Unlock()
if err := writePacket(s.writer, configurationKeepAlive, &id); err != nil {
return fmt.Errorf("write minecraft keep-alive: %w", err)
}
return nil
}
func (s *packetStream) keepAliveLoop() {
ticker := time.NewTicker(keepAlivePeriod)
defer ticker.Stop()
for {
select {
case <-ticker.C:
id := Long(s.keepAliveID.Add(1))
if err := s.writeKeepAlive(id); err != nil {
return
}
case <-s.done:
return
}
}
}
@@ -0,0 +1,102 @@
package xmc
import (
"bytes"
"io"
"net"
"testing"
)
func TestPacketStreamUsesPlainFraming(t *testing.T) {
payload := []byte("hello")
var wire bytes.Buffer
stream := newPacketStream(bytes.NewReader(nil), &wire, true)
written, err := stream.Write(payload)
if err != nil {
t.Fatalf("write payload: %v", err)
}
if written != len(payload) {
t.Fatalf("written = %d, want %d", written, len(payload))
}
wantOutbound := []byte{0x0f, 0x02, 0x08, 'x', 'm', 'c', ':', 'd', 'a', 't', 'a', 'h', 'e', 'l', 'l', 'o'}
if !bytes.Equal(wire.Bytes(), wantOutbound) {
t.Fatalf("wire frame = %x, want %x", wire.Bytes(), wantOutbound)
}
wantInbound := append([]byte(nil), wantOutbound...)
wantInbound[1] = configurationClientboundCustomPayload
reader := newPacketStream(bytes.NewReader(wantInbound), io.Discard, true)
got := make([]byte, len(payload))
if _, err = io.ReadFull(reader, got); err != nil {
t.Fatalf("read payload: %v", err)
}
if !bytes.Equal(got, payload) {
t.Fatalf("payload = %q, want %q", got, payload)
}
}
func TestPacketStreamRoundTrip(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
const password = "packet-stream-shared-key"
privateKey, publicKey := deriveTestRSAKey(t, password)
profiles := []loginProfile{testLoginProfile("packet_user")}
clientPayload := bytes.Repeat([]byte("client-payload-"), 5000)
serverPayload := bytes.Repeat([]byte("server-payload-"), 5000)
serverDone := make(chan error, 1)
go func() {
rawConn, acceptErr := ln.Accept()
if acceptErr != nil {
serverDone <- acceptErr
return
}
defer rawConn.Close()
server, wrapErr := wrapConnServer(rawConn, profiles, password, privateKey, publicKey)
if wrapErr != nil {
serverDone <- wrapErr
return
}
got := make([]byte, len(clientPayload))
if _, readErr := io.ReadFull(server, got); readErr != nil {
serverDone <- readErr
return
}
if !bytes.Equal(got, clientPayload) {
serverDone <- io.ErrUnexpectedEOF
return
}
_, writeErr := server.Write(serverPayload)
serverDone <- writeErr
}()
rawClient, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
t.Fatal(err)
}
defer rawClient.Close()
client, err := newClientConn(rawClient, profiles, password, publicKey, "localhost")
if err != nil {
t.Fatal(err)
}
if _, err = client.Write(clientPayload); err != nil {
t.Fatalf("write payload: %v", err)
}
got := make([]byte, len(serverPayload))
if _, err = io.ReadFull(client, got); err != nil {
t.Fatalf("read payload: %v", err)
}
if !bytes.Equal(got, serverPayload) {
t.Fatal("server payload mismatch")
}
if err = <-serverDone; err != nil {
t.Fatalf("server: %v", err)
}
}
+432
View File
@@ -0,0 +1,432 @@
package xmc
import (
"bytes"
"crypto/rand"
"fmt"
"io"
"math/big"
"time"
)
type paddingDirection uint8
const (
paddingClientToServer paddingDirection = iota + 1
paddingServerToClient
paddingBufferLength = 16 * 1024
maxPaddingChunkLength = 48 * 1024
maxPaddingTurnLength = 8 * 1024 * 1024
)
type paddingVariant struct {
chunks []int
delays []paddingDelayRange
}
type paddingDelayRange struct {
min time.Duration
max time.Duration
}
type paddingTurn struct {
direction paddingDirection
minLength int
maxLength int
variants []paddingVariant
startDelay paddingDelayRange
chunkDelay paddingDelayRange
writeChunkMinLength int
writeChunkLength int
sendMinLength int
sendMaxLength int
sendVariants []int
}
func runPaddingSchedule(reader io.Reader, writer io.Writer, isClient bool, firstTurnPrefixLength int, schedule []paddingTurn) error {
if err := validatePaddingSchedule(schedule, firstTurnPrefixLength); err != nil {
return err
}
var writeBuffer []byte
for i, turn := range schedule {
prefixLength := 0
if i == 0 {
prefixLength = firstTurnPrefixLength
}
localSends := isClient == (turn.direction == paddingClientToServer)
if localSends {
if err := writePaddingTurnWithBuffer(writer, turn, prefixLength, time.Sleep, &writeBuffer); err != nil {
return fmt.Errorf("write padding turn %d: %w", i, err)
}
continue
}
if err := readPaddingTurn(reader, turn, prefixLength); err != nil {
return fmt.Errorf("read padding turn %d: %w", i, err)
}
}
return nil
}
func validatePaddingSchedule(schedule []paddingTurn, firstTurnPrefixLength int) error {
if len(schedule) == 0 {
return fmt.Errorf("empty padding schedule")
}
if firstTurnPrefixLength < 0 {
return fmt.Errorf("negative first turn prefix length: %d", firstTurnPrefixLength)
}
if firstTurnPrefixLength > 0 && schedule[0].direction != paddingClientToServer {
return fmt.Errorf("first prefixed padding turn is not client-to-server")
}
for i, turn := range schedule {
if turn.direction != paddingClientToServer && turn.direction != paddingServerToClient {
return fmt.Errorf("padding turn %d has invalid direction: %d", i, turn.direction)
}
if err := validatePaddingDelayRange(turn.startDelay); err != nil {
return fmt.Errorf("padding turn %d has an invalid start delay: %w", i, err)
}
if err := validatePaddingDelayRange(turn.chunkDelay); err != nil {
return fmt.Errorf("padding turn %d has an invalid chunk delay: %w", i, err)
}
if turn.writeChunkMinLength < 0 || turn.writeChunkLength < turn.writeChunkMinLength || turn.writeChunkLength > maxPaddingChunkLength {
return fmt.Errorf("padding turn %d has an invalid write chunk range: %d-%d", i, turn.writeChunkMinLength, turn.writeChunkLength)
}
if len(turn.variants) > 0 && turn.writeChunkLength != 0 {
return fmt.Errorf("padding turn %d combines variants with generated write chunks", i)
}
minLength, maxLength, err := paddingTurnBounds(turn)
if err != nil {
return fmt.Errorf("padding turn %d: %w", i, err)
}
hasSendRange := turn.sendMinLength != 0 || turn.sendMaxLength != 0
if hasSendRange {
if len(turn.variants) > 0 {
return fmt.Errorf("padding turn %d combines variants with a send range", i)
}
if turn.sendMinLength < minLength || turn.sendMaxLength < turn.sendMinLength || turn.sendMaxLength > maxLength {
return fmt.Errorf("padding turn %d has an invalid send range: %d-%d", i, turn.sendMinLength, turn.sendMaxLength)
}
}
if i == 0 && minLength-firstTurnPrefixLength < 1 {
return fmt.Errorf("padding turn 0 is too short for %d prefix bytes", firstTurnPrefixLength)
}
if i == 0 && len(turn.variants) > 0 {
for j, variant := range turn.variants {
if _, _, err = trimPaddingPrefix(variant, firstTurnPrefixLength); err != nil {
return fmt.Errorf("padding turn 0 variant %d: %w", j, err)
}
}
}
if i > 0 && turn.direction == schedule[i-1].direction {
return fmt.Errorf("padding turns %d and %d have the same direction", i-1, i)
}
}
return nil
}
func writePaddingTurn(w io.Writer, turn paddingTurn, prefixLength int) error {
return writePaddingTurnWithSleep(w, turn, prefixLength, time.Sleep)
}
func writePaddingTurnWithSleep(w io.Writer, turn paddingTurn, prefixLength int, sleep func(time.Duration)) error {
return writePaddingTurnWithBuffer(w, turn, prefixLength, sleep, nil)
}
func writePaddingTurnWithBuffer(w io.Writer, turn paddingTurn, prefixLength int, sleep func(time.Duration), reusableBuffer *[]byte) error {
startDelay, err := randomPaddingDelay(turn.startDelay)
if err != nil {
return fmt.Errorf("select padding start delay: %w", err)
}
if startDelay > 0 {
sleep(startDelay)
}
targetLength, chunks, delays, err := selectPaddingVariant(turn, prefixLength)
if err != nil {
return err
}
recordLength := targetLength - prefixLength
if recordLength < 1 {
return fmt.Errorf("target length %d leaves an invalid record length %d", targetLength, recordLength)
}
encodedLength := Varint(recordLength)
var header bytes.Buffer
if err = encodedLength.writeTo(&header); err != nil {
return fmt.Errorf("write padding header: %w", err)
}
if len(chunks) == 0 {
writeChunkLength := turn.writeChunkLength
if writeChunkLength == 0 {
writeChunkLength = paddingBufferLength
} else if turn.writeChunkMinLength > 0 {
writeChunkLength, err = randomPaddingTarget(turn.writeChunkMinLength, writeChunkLength)
if err != nil {
return fmt.Errorf("select padding write chunk length: %w", err)
}
}
chunks = defaultPaddingChunks(recordLength, writeChunkLength)
delays = make([]paddingDelayRange, len(chunks))
for i := 1; i < len(delays); i++ {
delays[i] = turn.chunkDelay
}
}
if chunks[0] < header.Len() {
return fmt.Errorf("first padding chunk %d is shorter than header %d", chunks[0], header.Len())
}
maxChunkLength := 0
for _, chunkLength := range chunks {
if chunkLength < 1 || chunkLength > maxPaddingChunkLength {
return fmt.Errorf("invalid padding chunk length: %d", chunkLength)
}
maxChunkLength = max(maxChunkLength, chunkLength)
}
var buffer []byte
if reusableBuffer == nil {
buffer = make([]byte, maxChunkLength)
} else {
if cap(*reusableBuffer) < maxChunkLength {
*reusableBuffer = make([]byte, maxChunkLength)
}
buffer = (*reusableBuffer)[:maxChunkLength]
clear(buffer)
}
copy(buffer, header.Bytes())
written := 0
for i, chunkLength := range chunks {
if i < len(delays) {
delay, delayErr := randomPaddingDelay(delays[i])
if delayErr != nil {
return fmt.Errorf("select padding chunk %d delay: %w", i, delayErr)
}
if delay > 0 {
sleep(delay)
}
}
if err = writeFull(w, buffer[:chunkLength]); err != nil {
return fmt.Errorf("write padding chunk %d: %w", i, err)
}
written += chunkLength
if i == 0 {
clear(buffer[:header.Len()])
}
}
if written != recordLength {
return fmt.Errorf("padding chunks total %d, want %d", written, recordLength)
}
return nil
}
func readPaddingTurn(r io.Reader, turn paddingTurn, prefixLength int) error {
encodedLength, headerLength, err := readVarintWithLength(r)
if err != nil {
return fmt.Errorf("read padding header: %w", err)
}
recordLength := int(encodedLength)
if recordLength < headerLength || recordLength > maxPaddingTurnLength {
return fmt.Errorf("invalid padding record length: %d", recordLength)
}
totalLength := prefixLength + recordLength
if !paddingTurnAcceptsLength(turn, totalLength) {
if len(turn.variants) > 0 {
return fmt.Errorf("padding turn length %d is not an allowed variant", totalLength)
}
return fmt.Errorf("padding turn length %d is outside %d-%d", totalLength, turn.minLength, turn.maxLength)
}
var buffer [paddingBufferLength]byte
remaining := recordLength - headerLength
for remaining > 0 {
chunkLength := min(remaining, len(buffer))
if _, err := io.ReadFull(r, buffer[:chunkLength]); err != nil {
return fmt.Errorf("read padding body: %w", err)
}
remaining -= chunkLength
}
return nil
}
func selectPaddingVariant(turn paddingTurn, prefixLength int) (int, []int, []paddingDelayRange, error) {
if len(turn.variants) == 0 {
minimum, maximum := turn.minLength, turn.maxLength
if turn.sendMinLength != 0 || turn.sendMaxLength != 0 {
minimum, maximum = turn.sendMinLength, turn.sendMaxLength
}
targetLength, err := randomPaddingTarget(minimum, maximum)
return targetLength, nil, nil, err
}
indices := turn.sendVariants
if len(indices) == 0 {
indices = make([]int, len(turn.variants))
for i := range indices {
indices[i] = i
}
}
selected, err := randomPaddingIndex(len(indices))
if err != nil {
return 0, nil, nil, err
}
variantIndex := indices[selected]
if variantIndex < 0 || variantIndex >= len(turn.variants) {
return 0, nil, nil, fmt.Errorf("invalid send variant index: %d", variantIndex)
}
variant := turn.variants[variantIndex]
targetLength := paddingVariantLength(variant)
chunks, delays, err := trimPaddingPrefix(variant, prefixLength)
if err != nil {
return 0, nil, nil, err
}
return targetLength, chunks, delays, nil
}
func trimPaddingPrefix(variant paddingVariant, prefixLength int) ([]int, []paddingDelayRange, error) {
remainingPrefix := prefixLength
firstChunk := 0
for firstChunk < len(variant.chunks) && remainingPrefix > 0 {
chunkLength := variant.chunks[firstChunk]
if remainingPrefix < chunkLength {
return nil, nil, fmt.Errorf("prefix length %d splits chunk %d", prefixLength, firstChunk)
}
remainingPrefix -= chunkLength
firstChunk++
}
if remainingPrefix != 0 || firstChunk == len(variant.chunks) {
return nil, nil, fmt.Errorf("prefix length %d leaves no padding record", prefixLength)
}
chunks := append([]int(nil), variant.chunks[firstChunk:]...)
delays := make([]paddingDelayRange, len(chunks))
if len(variant.delays) > 0 {
copy(delays, variant.delays[firstChunk:])
}
return chunks, delays, nil
}
func defaultPaddingChunks(recordLength, writeChunkLength int) []int {
chunks := make([]int, 0, (recordLength+writeChunkLength-1)/writeChunkLength)
for remaining := recordLength; remaining > 0; {
chunkLength := min(remaining, writeChunkLength)
chunks = append(chunks, chunkLength)
remaining -= chunkLength
}
return chunks
}
func paddingTurnBounds(turn paddingTurn) (int, int, error) {
if len(turn.variants) == 0 {
if turn.minLength < 1 || turn.maxLength < turn.minLength || turn.maxLength > maxPaddingTurnLength {
return 0, 0, fmt.Errorf("invalid range: %d-%d", turn.minLength, turn.maxLength)
}
return turn.minLength, turn.maxLength, nil
}
if turn.minLength != 0 || turn.maxLength != 0 {
return 0, 0, fmt.Errorf("variants cannot be combined with a length range")
}
minLength := maxPaddingTurnLength + 1
maxLength := 0
for i, variant := range turn.variants {
if len(variant.chunks) == 0 {
return 0, 0, fmt.Errorf("variant %d has no chunks", i)
}
if len(variant.delays) != 0 && len(variant.delays) != len(variant.chunks) {
return 0, 0, fmt.Errorf("variant %d has %d chunks and %d delays", i, len(variant.chunks), len(variant.delays))
}
for j, chunkLength := range variant.chunks {
if chunkLength < 1 || chunkLength > maxPaddingChunkLength {
return 0, 0, fmt.Errorf("variant %d chunk %d has invalid length: %d", i, j, chunkLength)
}
if len(variant.delays) > 0 {
if err := validatePaddingDelayRange(variant.delays[j]); err != nil {
return 0, 0, fmt.Errorf("variant %d chunk %d has an invalid delay: %w", i, j, err)
}
}
}
length := paddingVariantLength(variant)
if length > maxPaddingTurnLength {
return 0, 0, fmt.Errorf("variant %d is too long: %d", i, length)
}
minLength = min(minLength, length)
maxLength = max(maxLength, length)
}
for _, index := range turn.sendVariants {
if index < 0 || index >= len(turn.variants) {
return 0, 0, fmt.Errorf("invalid send variant index: %d", index)
}
}
return minLength, maxLength, nil
}
func paddingTurnAcceptsLength(turn paddingTurn, length int) bool {
if len(turn.variants) == 0 {
return length >= turn.minLength && length <= turn.maxLength
}
for _, variant := range turn.variants {
if paddingVariantLength(variant) == length {
return true
}
}
return false
}
func paddingVariantLength(variant paddingVariant) int {
total := 0
for _, chunkLength := range variant.chunks {
total += chunkLength
}
return total
}
func validatePaddingDelayRange(delay paddingDelayRange) error {
if delay.min < 0 || delay.max < delay.min {
return fmt.Errorf("invalid range: %s-%s", delay.min, delay.max)
}
return nil
}
func randomPaddingDelay(delay paddingDelayRange) (time.Duration, error) {
if err := validatePaddingDelayRange(delay); err != nil {
return 0, err
}
if delay.min == delay.max {
return delay.min, nil
}
span := int64(delay.max-delay.min) + 1
offset, err := rand.Int(rand.Reader, big.NewInt(span))
if err != nil {
return 0, fmt.Errorf("select padding delay: %w", err)
}
return delay.min + time.Duration(offset.Int64()), nil
}
func randomPaddingIndex(length int) (int, error) {
if length < 1 {
return 0, fmt.Errorf("select from empty padding choices")
}
if length == 1 {
return 0, nil
}
index, err := rand.Int(rand.Reader, big.NewInt(int64(length)))
if err != nil {
return 0, fmt.Errorf("select padding choice: %w", err)
}
return int(index.Int64()), nil
}
func randomPaddingTarget(minLength, maxLength int) (int, error) {
if minLength == maxLength {
return minLength, nil
}
span := int64(maxLength-minLength) + 1
offset, err := rand.Int(rand.Reader, big.NewInt(span))
if err != nil {
return 0, fmt.Errorf("select padding length: %w", err)
}
return minLength + int(offset.Int64()), nil
}
@@ -0,0 +1,282 @@
package xmc
import (
"time"
)
// Length and write-boundary templates come from controlled Minecraft 26.1.2
// logins. Timing deliberately uses broad random bands that preserve only the
// rough ordering of short and long phases; it does not replay captured delays.
var startupPaddingSchedule2612 = []paddingTurn{
{
direction: paddingClientToServer,
variants: []paddingVariant{
paddingVariantFromChunks(2, 26, 16),
},
},
{
direction: paddingServerToClient,
variants: []paddingVariant{
paddingVariantFromChunks(26, 21, 25),
},
startDelay: millisecondRange(0, 20),
},
{
direction: paddingClientToServer,
variants: []paddingVariant{
paddingVariantFromChunks(25),
},
startDelay: millisecondRange(2, 22),
},
{
direction: paddingServerToClient,
variants: []paddingVariant{
registryPaddingVariant(),
},
startDelay: millisecondRange(20, 50),
},
{
direction: paddingClientToServer,
variants: []paddingVariant{
paddingVariantFromChunks(2),
},
startDelay: millisecondRange(10, 35),
},
{
direction: paddingServerToClient,
variants: []paddingVariant{
playStartPaddingVariant(4941, 252, 259, 267, 268, 251, 303, 259, 264, 54, 346),
playStartPaddingVariant(4941, 262, 284, 272, 260, 260, 313, 264, 151, 224, 207, 215, 224, 390),
playStartPaddingVariant(4941, 257, 272, 275, 260, 260, 313, 283, 274, 226, 207, 230, 215, 204, 221, 352),
playStartPaddingVariant(4941, 259, 272, 288, 260, 260, 311, 270, 70, 236, 223, 201, 210, 352),
playStartPaddingVariant(4941, 255, 269, 277, 263, 260, 136, 207, 210, 232, 325),
playStartPaddingVariant(4941, 259, 270, 274, 263, 258, 327, 170, 210, 375),
playStartPaddingVariant(4941, 257, 275, 291, 260, 260, 325, 269, 70, 230, 226, 207, 221, 352),
playStartPaddingVariant(4941, 252, 273, 262, 252, 254, 306, 93),
playStartPaddingVariant(4941, 273, 270, 269, 258, 256, 322, 221, 207, 215, 438),
playStartPaddingVariant(4941, 259, 275, 274, 250, 258, 308, 267, 154, 233, 209, 207, 213, 393),
playStartPaddingVariant(4941, 254, 267, 272, 260, 253, 311, 167, 204, 232, 207, 481, 8),
playStartPaddingVariant(4941, 259, 269, 272, 261, 313, 207, 213, 500, 19),
playStartPaddingVariant(4941, 262, 269, 274, 263, 274, 311, 270, 242, 210, 229, 221, 210, 431),
playStartPaddingVariant(4941, 259, 265, 277, 263, 277, 316, 269, 156, 204, 210, 226, 207, 413),
playStartPaddingVariant(4941, 215, 251, 249, 317, 260, 270, 249, 52),
playStartPaddingVariant(4941, 224, 263, 277, 316, 267, 272, 260, 138, 230, 226, 207, 204, 352),
playStartPaddingVariant(4941, 221, 258, 263, 319, 269, 288, 263, 136, 204, 210, 220, 207, 378),
playStartPaddingVariant(4941, 221, 258, 260, 316, 273, 291, 226, 204, 229, 213, 489, 8),
playStartPaddingVariant(4941, 238, 260, 261, 306, 272, 277, 260, 224, 241, 212, 207, 204, 393),
playStartPaddingVariant(4941, 224, 260, 260, 309, 272, 277, 277, 138, 207, 207, 212, 241, 352),
},
startDelay: millisecondRange(35, 50),
},
}
// These turns cover the finite Play-state tail through the client's
// player_loaded packet. Bounds are the observed per-turn minima and maxima
// across 20 controlled 26.1.2 logins; payload bytes remain opaque padding.
var playJoinPaddingSchedule2612 = []paddingTurn{
clientPlayPaddingTurn(6, 883),
serverPlayPaddingTurn(346, 58638),
clientPlayPaddingTurn(6, 887),
serverPlayPaddingTurn(388, 61077),
clientPlayPaddingTurn(2, 50),
serverPlayPaddingTurn(575, 65584),
clientPlayPaddingTurn(6, 45),
serverPlayPaddingTurn(86, 63563),
clientPlayPaddingTurn(2, 44),
serverPlayPaddingTurn(42, 51983),
clientPlayPaddingTurn(2, 851),
serverPlayPaddingTurn(309, 25083),
clientPlayPaddingTurn(2, 19),
serverPlayPaddingTurn(74, 63885),
clientPlayPaddingTurn(8, 24),
serverPlayPaddingTurn(30, 66128),
clientPlayPaddingTurn(2, 19),
serverPlayPaddingTurn(26, 35818),
clientPlayPaddingTurn(6, 19),
serverPlayPaddingTurn(35, 59407),
clientPlayPaddingTurn(6, 19),
serverPlayPaddingTurn(37, 65328),
clientPlayPaddingTurn(2, 19),
serverPlayPaddingTurn(26, 60622),
clientPlayPaddingTurn(6, 19),
serverPlayPaddingTurn(11, 60808),
clientPlayPaddingTurn(8, 43),
serverPlayPaddingTurn(55, 62027),
clientPlayPaddingTurn(2, 19),
serverPlayPaddingTurn(427, 65622),
clientPlayPaddingTurn(5, 19),
serverPlayPaddingTurn(35, 59401),
clientPlayPaddingTurn(6, 19),
}
type paddingLengthRange2612 struct {
minimum int
maximum int
}
type serverPlayLengthBranches2612 struct {
small paddingLengthRange2612
large paddingLengthRange2612
}
var serverPlayBranches2612 = []serverPlayLengthBranches2612{
{small: paddingLengthRange2612{346, 18812}, large: paddingLengthRange2612{51702, 58638}},
{small: paddingLengthRange2612{388, 20689}, large: paddingLengthRange2612{51445, 61077}},
{small: paddingLengthRange2612{575, 20915}, large: paddingLengthRange2612{41428, 65584}},
{small: paddingLengthRange2612{86, 2772}, large: paddingLengthRange2612{41428, 63563}},
{small: paddingLengthRange2612{42, 26813}, large: paddingLengthRange2612{51983, 51983}},
{small: paddingLengthRange2612{309, 19484}, large: paddingLengthRange2612{24837, 25083}},
{small: paddingLengthRange2612{74, 40686}, large: paddingLengthRange2612{63885, 63885}},
{small: paddingLengthRange2612{30, 44114}, large: paddingLengthRange2612{66128, 66128}},
{small: paddingLengthRange2612{26, 1464}, large: paddingLengthRange2612{9941, 35818}},
{small: paddingLengthRange2612{35, 42885}, large: paddingLengthRange2612{52194, 59407}},
{small: paddingLengthRange2612{37, 47553}, large: paddingLengthRange2612{61765, 65328}},
{small: paddingLengthRange2612{26, 1121}, large: paddingLengthRange2612{16162, 60622}},
{small: paddingLengthRange2612{11, 45629}, large: paddingLengthRange2612{60808, 60808}},
{small: paddingLengthRange2612{55, 10035}, large: paddingLengthRange2612{30237, 62027}},
{small: paddingLengthRange2612{427, 52536}, large: paddingLengthRange2612{64014, 65622}},
{small: paddingLengthRange2612{35, 22708}, large: paddingLengthRange2612{38987, 59401}},
}
// Each mask preserves only the small/large branch order from one baseline
// login. Actual lengths and timing are selected randomly inside each branch.
var serverPlayBranchMasks2612 = []uint32{
0x011c, 0x090a, 0x0821, 0xe921, 0x2102,
0x0844, 0xa101, 0x1106, 0x2e00, 0xab01,
0xe900, 0xac01, 0xab01, 0x8b80, 0x0808,
0x2001, 0x0901, 0x000a, 0x2c01, 0x0801,
}
type clientPlayBurst2612 struct {
playIndex int
regular paddingLengthRange2612
burst paddingLengthRange2612
}
var clientPlayBursts2612 = []clientPlayBurst2612{
{playIndex: 0, regular: paddingLengthRange2612{6, 44}, burst: paddingLengthRange2612{877, 883}},
{playIndex: 2, regular: paddingLengthRange2612{6, 45}, burst: paddingLengthRange2612{884, 887}},
{playIndex: 10, regular: paddingLengthRange2612{2, 19}, burst: paddingLengthRange2612{851, 851}},
}
// The 20 samples placed the one client initialization burst in these slots.
var clientPlayBurstChoices2612 = []int{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1,
2,
}
var paddingSchedule2612 = buildPaddingSchedule2612()
func buildPaddingSchedule2612() []paddingTurn {
schedule := make([]paddingTurn, 0, len(startupPaddingSchedule2612)+len(playJoinPaddingSchedule2612))
schedule = append(schedule, startupPaddingSchedule2612...)
schedule = append(schedule, playJoinPaddingSchedule2612...)
return schedule
}
func clientPlayPaddingTurn(minimum, maximum int) paddingTurn {
return paddingTurn{
direction: paddingClientToServer,
minLength: minimum,
maxLength: maximum,
startDelay: millisecondRange(1, 30),
writeChunkLength: 1024,
}
}
func serverPlayPaddingTurn(minimum, maximum int) paddingTurn {
return paddingTurn{
direction: paddingServerToClient,
minLength: minimum,
maxLength: maximum,
startDelay: millisecondRange(1, 45),
chunkDelay: millisecondRange(1, 4),
writeChunkMinLength: 32 * 1024,
writeChunkLength: maxPaddingChunkLength,
}
}
func newClientPaddingSchedule2612() ([]paddingTurn, error) {
choice, err := randomPaddingIndex(len(clientPlayBurstChoices2612))
if err != nil {
return nil, err
}
selectedBurst := clientPlayBurstChoices2612[choice]
schedule := append([]paddingTurn(nil), paddingSchedule2612...)
for i, burst := range clientPlayBursts2612 {
lengthRange := burst.regular
if i == selectedBurst {
lengthRange = burst.burst
}
turn := &schedule[len(startupPaddingSchedule2612)+burst.playIndex]
turn.sendMinLength = lengthRange.minimum
turn.sendMaxLength = lengthRange.maximum
}
return schedule, nil
}
type paddingPause struct {
chunk int
delay paddingDelayRange
}
func paddingVariantFromChunks(chunks ...int) paddingVariant {
return paddingVariant{chunks: chunks}
}
func pacedPaddingVariant(chunks []int, pauses ...paddingPause) paddingVariant {
delays := make([]paddingDelayRange, len(chunks))
for _, pause := range pauses {
if pause.chunk < 0 || pause.chunk >= len(delays) {
panic("xmc: padding pause index is outside its chunk template")
}
delays[pause.chunk] = pause.delay
}
return paddingVariant{chunks: chunks, delays: delays}
}
func registryPaddingVariant() paddingVariant {
return pacedPaddingVariant(
[]int{1590, 226, 329, 229, 186, 151, 78, 81, 79, 235, 67, 67, 78, 71, 82, 74, 982, 117, 1118, 1038, 970, 400, 239, 49, 50, 95, 65, 104, 32320, 2},
paddingPause{28, millisecondRange(1, 4)},
paddingPause{29, millisecondRange(44, 61)},
)
}
func playStartPaddingVariant(chunks ...int) paddingVariant {
if len(chunks) < 2 {
panic("xmc: play start padding variant needs at least two chunks")
}
return pacedPaddingVariant(
chunks,
paddingPause{len(chunks) / 2, millisecondRange(1, 5)},
paddingPause{len(chunks) - 1, millisecondRange(9, 20)},
)
}
func millisecondRange(minimum, maximum int) paddingDelayRange {
return paddingDelayRange{
min: time.Duration(minimum) * time.Millisecond,
max: time.Duration(maximum) * time.Millisecond,
}
}
func newServerPaddingSchedule2612() ([]paddingTurn, error) {
schedule := append([]paddingTurn(nil), paddingSchedule2612...)
profileIndex, err := randomPaddingIndex(len(serverPlayBranchMasks2612))
if err != nil {
return nil, err
}
profile := serverPlayBranchMasks2612[profileIndex]
for i, branches := range serverPlayBranches2612 {
lengthRange := branches.small
if profile&(1<<i) != 0 {
lengthRange = branches.large
}
turn := &schedule[len(startupPaddingSchedule2612)+1+i*2]
turn.sendMinLength = lengthRange.minimum
turn.sendMaxLength = lengthRange.maximum
}
return schedule, nil
}
@@ -0,0 +1,618 @@
package xmc
import (
"bytes"
"errors"
"io"
"net"
"strconv"
"strings"
"testing"
"time"
)
func TestPaddingTurnReachesFinalTargetLength(t *testing.T) {
turn := paddingTurn{direction: paddingClientToServer, minLength: 128, maxLength: 128}
const prefixLength = 3
var encoded bytes.Buffer
if err := writePaddingTurn(&encoded, turn, prefixLength); err != nil {
t.Fatal(err)
}
if got := prefixLength + encoded.Len(); got != turn.minLength {
t.Fatalf("total turn length = %d, want %d", got, turn.minLength)
}
encodedReader := bytes.NewReader(encoded.Bytes())
var recordLength Varint
if err := recordLength.readFrom(encodedReader); err != nil {
t.Fatal(err)
}
if got := int(recordLength); got != encoded.Len() {
t.Fatalf("record length = %d, encoded = %d", got, encoded.Len())
}
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, prefixLength); err != nil {
t.Fatal(err)
}
}
func TestPaddingTurnSupportsThreeByteTarget(t *testing.T) {
turn := paddingTurn{direction: paddingClientToServer, minLength: 3, maxLength: 3}
var encoded bytes.Buffer
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
t.Fatal(err)
}
if got := encoded.Len(); got != 3 {
t.Fatalf("padding length = %d, want 3", got)
}
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, 0); err != nil {
t.Fatal(err)
}
}
func TestPaddingTurnVarintBoundaries(t *testing.T) {
for _, targetLength := range []int{127, 128, 16383, 16384} {
t.Run(strconv.Itoa(targetLength), func(t *testing.T) {
turn := paddingTurn{direction: paddingClientToServer, minLength: targetLength, maxLength: targetLength}
var encoded bytes.Buffer
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
t.Fatal(err)
}
if encoded.Len() != targetLength {
t.Fatalf("padding length = %d, want %d", encoded.Len(), targetLength)
}
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, 0); err != nil {
t.Fatal(err)
}
})
}
}
func TestPaddingTurnRandomRange(t *testing.T) {
turn := paddingTurn{direction: paddingServerToClient, minLength: 127, maxLength: 129}
seen := make(map[int]bool)
for range 100 {
var encoded bytes.Buffer
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
t.Fatal(err)
}
if encoded.Len() < turn.minLength || encoded.Len() > turn.maxLength {
t.Fatalf("padding length = %d", encoded.Len())
}
seen[encoded.Len()] = true
}
if len(seen) < 2 {
t.Fatalf("padding range did not vary: %v", seen)
}
}
func TestPaddingTurnUsesRestrictedSendRange(t *testing.T) {
turn := paddingTurn{
direction: paddingServerToClient,
minLength: 3,
maxLength: 100,
sendMinLength: 90,
sendMaxLength: 100,
}
seen := make(map[int]bool)
for range 100 {
var encoded bytes.Buffer
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
t.Fatal(err)
}
if encoded.Len() < turn.sendMinLength || encoded.Len() > turn.sendMaxLength {
t.Fatalf("padding length = %d", encoded.Len())
}
seen[encoded.Len()] = true
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, 0); err != nil {
t.Fatal(err)
}
}
if len(seen) < 2 {
t.Fatalf("restricted send range did not vary: %v", seen)
}
}
func TestPaddingScheduleSynchronizesDirections(t *testing.T) {
client, server := net.Pipe()
defer client.Close()
defer server.Close()
schedule := []paddingTurn{
{direction: paddingClientToServer, minLength: 33, maxLength: 33},
{direction: paddingServerToClient, minLength: 4097, maxLength: 4097},
{direction: paddingClientToServer, minLength: 16385, maxLength: 16385},
}
serverDone := make(chan error, 1)
go func() {
serverDone <- runPaddingSchedule(server, server, false, 3, schedule)
}()
if err := runPaddingSchedule(client, client, true, 3, schedule); err != nil {
t.Fatal(err)
}
select {
case err := <-serverDone:
if err != nil {
t.Fatal(err)
}
case <-time.After(time.Second):
t.Fatal("server padding schedule did not complete")
}
}
func TestReadPaddingTurnHandlesFragmentedInput(t *testing.T) {
turn := paddingTurn{direction: paddingClientToServer, minLength: 1024, maxLength: 1024}
var encoded bytes.Buffer
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
t.Fatal(err)
}
if err := readPaddingTurn(&oneByteReader{reader: bytes.NewReader(encoded.Bytes())}, turn, 0); err != nil {
t.Fatal(err)
}
}
func TestReadPaddingTurnRejectsInvalidLength(t *testing.T) {
turn := paddingTurn{direction: paddingClientToServer, minLength: 64, maxLength: 96}
data := encodePaddingLength(t, 63)
if err := readPaddingTurn(bytes.NewReader(data), turn, 0); err == nil || !strings.Contains(err.Error(), "outside") {
t.Fatalf("error = %v", err)
}
}
func TestReadPaddingTurnRejectsNonCanonicalHeader(t *testing.T) {
turn := paddingTurn{direction: paddingClientToServer, minLength: 3, maxLength: 3}
err := readPaddingTurn(bytes.NewReader([]byte{0x83, 0x00, 0x00}), turn, 0)
if err == nil || !strings.Contains(err.Error(), "non-canonical") {
t.Fatalf("error = %v", err)
}
}
func TestReadPaddingTurnRejectsTruncatedBody(t *testing.T) {
turn := paddingTurn{direction: paddingClientToServer, minLength: 64, maxLength: 64}
data := encodePaddingLength(t, 64)
if err := readPaddingTurn(bytes.NewReader(data), turn, 0); err == nil || !strings.Contains(err.Error(), "body") {
t.Fatalf("error = %v", err)
}
}
func TestReadPaddingTurnHonorsConnectionTimeout(t *testing.T) {
client, server := net.Pipe()
defer client.Close()
defer server.Close()
if err := server.SetReadDeadline(time.Now().Add(20 * time.Millisecond)); err != nil {
t.Fatal(err)
}
turn := paddingTurn{direction: paddingClientToServer, minLength: 64, maxLength: 64}
err := readPaddingTurn(server, turn, 0)
var netErr net.Error
if !errors.As(err, &netErr) || !netErr.Timeout() {
t.Fatalf("error = %v, want network timeout", err)
}
}
func TestValidatePaddingSchedule(t *testing.T) {
tests := []struct {
name string
schedule []paddingTurn
prefix int
}{
{name: "empty"},
{name: "bad direction", schedule: []paddingTurn{{direction: 99, minLength: 4, maxLength: 4}}},
{name: "too small", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 0, maxLength: 4}}},
{name: "reversed range", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 7}}},
{name: "wrong first direction", prefix: 3, schedule: []paddingTurn{{direction: paddingServerToClient, minLength: 8, maxLength: 8}}},
{name: "prefix leaves no header", prefix: 8, schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8}}},
{name: "same direction", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8}, {direction: paddingClientToServer, minLength: 8, maxLength: 8}}},
{name: "range with variants", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, variants: []paddingVariant{paddingVariantFromChunks(8)}}}},
{name: "empty variant", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{{}}}}},
{name: "bad chunk", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(maxPaddingChunkLength + 1)}}}},
{
name: "delay mismatch",
schedule: []paddingTurn{{
direction: paddingClientToServer,
variants: []paddingVariant{{
chunks: []int{4, 4},
delays: []paddingDelayRange{{min: time.Millisecond, max: time.Millisecond}},
}},
}},
},
{name: "reversed start delay", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, startDelay: paddingDelayRange{min: 2 * time.Millisecond, max: time.Millisecond}}}},
{name: "reversed generated chunk delay", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, chunkDelay: paddingDelayRange{min: 2 * time.Millisecond, max: time.Millisecond}}}},
{name: "oversized generated chunk", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, writeChunkLength: maxPaddingChunkLength + 1}}},
{name: "reversed generated chunk range", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, writeChunkMinLength: 9, writeChunkLength: 8}}},
{name: "variant with generated chunks", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8)}, writeChunkLength: 8}}},
{name: "send range outside accepted range", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 16, sendMinLength: 7, sendMaxLength: 12}}},
{name: "variant with send range", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8)}, sendMinLength: 8, sendMaxLength: 8}}},
{name: "negative chunk delay", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{{chunks: []int{8}, delays: []paddingDelayRange{{min: -time.Millisecond}}}}}}},
{name: "bad send variant", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8)}, sendVariants: []int{1}}}},
{name: "prefix splits chunk", prefix: 3, schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8, 4)}}}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if err := validatePaddingSchedule(test.schedule, test.prefix); err == nil {
t.Fatal("expected invalid padding schedule")
}
})
}
}
func TestPaddingSchedule2612MatchesCapturedTemplates(t *testing.T) {
wantDirections := []paddingDirection{
paddingClientToServer,
paddingServerToClient,
paddingClientToServer,
paddingServerToClient,
paddingClientToServer,
paddingServerToClient,
}
wantLengths := [][]int{
{44},
{72},
{25},
{41172},
{2},
{7464, 8267, 8790, 8153, 7375, 7347, 8184, 6633, 7670, 8241, 7857, 7254, 8407, 8283, 6804, 8177, 8177, 7929, 8296, 8177},
}
if len(paddingSchedule2612) != len(wantDirections)+33 {
t.Fatalf("padding schedule has %d turns, want %d", len(paddingSchedule2612), len(wantDirections)+33)
}
for i, turn := range paddingSchedule2612[:len(wantDirections)] {
if turn.direction != wantDirections[i] {
t.Fatalf("padding turn %d direction = %d, want %d", i, turn.direction, wantDirections[i])
}
if len(turn.variants) != len(wantLengths[i]) {
t.Fatalf("padding turn %d has %d variants, want %d", i, len(turn.variants), len(wantLengths[i]))
}
for j, variant := range turn.variants {
if got := paddingVariantLength(variant); got != wantLengths[i][j] {
t.Fatalf("padding turn %d variant %d length = %d, want %d", i, j, got, wantLengths[i][j])
}
}
}
wantPlayBounds := [][2]int{
{6, 883},
{346, 58638},
{6, 887},
{388, 61077},
{2, 50},
{575, 65584},
{6, 45},
{86, 63563},
{2, 44},
{42, 51983},
{2, 851},
{309, 25083},
{2, 19},
{74, 63885},
{8, 24},
{30, 66128},
{2, 19},
{26, 35818},
{6, 19},
{35, 59407},
{6, 19},
{37, 65328},
{2, 19},
{26, 60622},
{6, 19},
{11, 60808},
{8, 43},
{55, 62027},
{2, 19},
{427, 65622},
{5, 19},
{35, 59401},
{6, 19},
}
for i, want := range wantPlayBounds {
turn := paddingSchedule2612[len(wantDirections)+i]
wantDirection := paddingClientToServer
if i%2 == 1 {
wantDirection = paddingServerToClient
}
if turn.direction != wantDirection {
t.Fatalf("play turn %d direction = %d, want %d", i, turn.direction, wantDirection)
}
if turn.minLength != want[0] || turn.maxLength != want[1] {
t.Fatalf("play turn %d bounds = %d-%d, want %d-%d", i, turn.minLength, turn.maxLength, want[0], want[1])
}
if len(turn.variants) != 0 {
t.Fatalf("play turn %d unexpectedly has captured variants", i)
}
}
if got := len(paddingSchedule2612[3].variants[0].chunks); got != 30 {
t.Fatalf("registry turn chunks = %d, want 30", got)
}
minimumPlayStart := maxPaddingTurnLength
maximumPlayStart := 0
for _, variant := range paddingSchedule2612[5].variants {
length := paddingVariantLength(variant)
minimumPlayStart = min(minimumPlayStart, length)
maximumPlayStart = max(maximumPlayStart, length)
if variant.chunks[0] != 4941 {
t.Fatalf("play start first chunk = %d, want 4941", variant.chunks[0])
}
}
if minimumPlayStart != 6633 || maximumPlayStart != 8790 {
t.Fatalf("play start bounds = %d-%d, want 6633-8790", minimumPlayStart, maximumPlayStart)
}
if err := validatePaddingSchedule(paddingSchedule2612, 2); err != nil {
t.Fatalf("captured schedule is invalid: %v", err)
}
serverSchedule, err := newServerPaddingSchedule2612()
if err != nil {
t.Fatal(err)
}
if err = validatePaddingSchedule(serverSchedule, 2); err != nil {
t.Fatalf("server schedule is invalid: %v", err)
}
for i, branches := range serverPlayBranches2612 {
turn := serverSchedule[len(startupPaddingSchedule2612)+1+i*2]
got := paddingLengthRange2612{turn.sendMinLength, turn.sendMaxLength}
if got != branches.small && got != branches.large {
t.Fatalf("server play turn %d send range = %v, want %v or %v", i, got, branches.small, branches.large)
}
}
for range 20 {
clientSchedule, clientErr := newClientPaddingSchedule2612()
if clientErr != nil {
t.Fatal(clientErr)
}
if clientErr = validatePaddingSchedule(clientSchedule, 2); clientErr != nil {
t.Fatalf("client schedule is invalid: %v", clientErr)
}
burstCount := 0
for _, burst := range clientPlayBursts2612 {
turn := clientSchedule[len(startupPaddingSchedule2612)+burst.playIndex]
got := paddingLengthRange2612{turn.sendMinLength, turn.sendMaxLength}
switch got {
case burst.regular:
case burst.burst:
burstCount++
default:
t.Fatalf("client play turn %d send range = %v", burst.playIndex, got)
}
}
if burstCount != 1 {
t.Fatalf("client schedule has %d initialization bursts, want 1", burstCount)
}
}
for variantIndex := range paddingSchedule2612[3].variants {
turn := paddingSchedule2612[3]
turn.sendVariants = []int{variantIndex}
var encoded bytes.Buffer
if err = writePaddingTurnWithSleep(&encoded, turn, 0, func(time.Duration) {}); err != nil {
t.Fatal(err)
}
if err = readPaddingTurn(bytes.NewReader(encoded.Bytes()), paddingSchedule2612[3], 0); err != nil {
t.Fatalf("registry variant %d was rejected: %v", variantIndex, err)
}
}
}
func TestPaddingVariantPreservesWriteBoundaries(t *testing.T) {
tests := []struct {
name string
turn paddingTurn
prefix int
want []int
}{
{name: "login acknowledged turn", turn: paddingSchedule2612[0], prefix: 2, want: []int{26, 16}},
{name: "server response turn", turn: paddingSchedule2612[1], want: []int{26, 21, 25}},
{name: "single packet turn", turn: paddingSchedule2612[2], want: []int{25}},
{name: "fixed registry profile", turn: paddingSchedule2612[3], want: paddingSchedule2612[3].variants[0].chunks},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var writer recordingWriter
if err := writePaddingTurnWithSleep(&writer, test.turn, test.prefix, func(time.Duration) {}); err != nil {
t.Fatal(err)
}
if len(writer.writes) != len(test.want) {
t.Fatalf("writes = %v, want %v", writer.writes, test.want)
}
for i := range test.want {
if writer.writes[i] != test.want[i] {
t.Fatalf("writes = %v, want %v", writer.writes, test.want)
}
}
if err := readPaddingTurn(bytes.NewReader(writer.Bytes()), test.turn, test.prefix); err != nil {
t.Fatal(err)
}
})
}
}
func TestPaddingVariantAppliesPacing(t *testing.T) {
turn := paddingTurn{
direction: paddingClientToServer,
startDelay: paddingDelayRange{min: 3 * time.Millisecond, max: 3 * time.Millisecond},
variants: []paddingVariant{{
chunks: []int{3, 5, 7},
delays: []paddingDelayRange{
{},
{min: 2 * time.Millisecond, max: 2 * time.Millisecond},
{min: 4 * time.Millisecond, max: 4 * time.Millisecond},
},
}},
}
var slept []time.Duration
var writer recordingWriter
if err := writePaddingTurnWithSleep(&writer, turn, 3, func(delay time.Duration) {
slept = append(slept, delay)
}); err != nil {
t.Fatal(err)
}
want := []time.Duration{3 * time.Millisecond, 2 * time.Millisecond, 4 * time.Millisecond}
if len(slept) != len(want) {
t.Fatalf("delays = %v, want %v", slept, want)
}
for i := range want {
if slept[i] != want[i] {
t.Fatalf("delays = %v, want %v", slept, want)
}
}
}
func TestGeneratedPaddingChunksApplyPacing(t *testing.T) {
turn := paddingTurn{
direction: paddingServerToClient,
minLength: 100,
maxLength: 100,
writeChunkLength: 32,
chunkDelay: paddingDelayRange{min: 2 * time.Millisecond, max: 2 * time.Millisecond},
}
var slept []time.Duration
var writer recordingWriter
if err := writePaddingTurnWithSleep(&writer, turn, 0, func(delay time.Duration) {
slept = append(slept, delay)
}); err != nil {
t.Fatal(err)
}
wantWrites := []int{32, 32, 32, 4}
if !slicesEqual(writer.writes, wantWrites) {
t.Fatalf("writes = %v, want %v", writer.writes, wantWrites)
}
wantSleeps := []time.Duration{2 * time.Millisecond, 2 * time.Millisecond, 2 * time.Millisecond}
if !slicesEqual(slept, wantSleeps) {
t.Fatalf("delays = %v, want %v", slept, wantSleeps)
}
if err := readPaddingTurn(bytes.NewReader(writer.Bytes()), turn, 0); err != nil {
t.Fatal(err)
}
}
func TestGeneratedPaddingChunkLengthIsRandomized(t *testing.T) {
turn := paddingTurn{
direction: paddingServerToClient,
minLength: 100,
maxLength: 100,
writeChunkMinLength: 16,
writeChunkLength: 32,
}
seen := make(map[int]bool)
for range 100 {
var writer recordingWriter
if err := writePaddingTurnWithSleep(&writer, turn, 0, func(time.Duration) {}); err != nil {
t.Fatal(err)
}
firstWrite := writer.writes[0]
if firstWrite < turn.writeChunkMinLength || firstWrite > turn.writeChunkLength {
t.Fatalf("first write = %d", firstWrite)
}
seen[firstWrite] = true
}
if len(seen) < 2 {
t.Fatalf("generated write chunk length did not vary: %v", seen)
}
}
func TestPaddingDelayRangeIsRandomized(t *testing.T) {
delayRange := millisecondRange(25, 40)
seen := make(map[time.Duration]bool)
for range 100 {
delay, err := randomPaddingDelay(delayRange)
if err != nil {
t.Fatal(err)
}
if delay < delayRange.min || delay > delayRange.max {
t.Fatalf("delay = %s, want %s-%s", delay, delayRange.min, delayRange.max)
}
seen[delay] = true
}
if len(seen) < 2 {
t.Fatalf("padding delay did not vary: %v", seen)
}
}
func TestPaddingSchedule2612UsesCoarseTimingBands(t *testing.T) {
assertDelayRange(t, "turn 3 to 4", paddingSchedule2612[3].startDelay, 20*time.Millisecond, 50*time.Millisecond)
assertDelayRange(t, "turn 5 to 6", paddingSchedule2612[5].startDelay, 35*time.Millisecond, 50*time.Millisecond)
assertDelayRange(t, "first play client turn", paddingSchedule2612[6].startDelay, time.Millisecond, 30*time.Millisecond)
assertDelayRange(t, "first play server turn", paddingSchedule2612[7].startDelay, time.Millisecond, 45*time.Millisecond)
assertDelayRange(t, "play server chunk pacing", paddingSchedule2612[7].chunkDelay, time.Millisecond, 4*time.Millisecond)
if paddingSchedule2612[6].writeChunkLength != 1024 {
t.Fatalf("play client write chunk = %d, want 1024", paddingSchedule2612[6].writeChunkLength)
}
if paddingSchedule2612[7].writeChunkLength != maxPaddingChunkLength {
t.Fatalf("play server write chunk = %d, want %d", paddingSchedule2612[7].writeChunkLength, maxPaddingChunkLength)
}
if paddingSchedule2612[7].writeChunkMinLength != 32*1024 {
t.Fatalf("play server minimum write chunk = %d, want %d", paddingSchedule2612[7].writeChunkMinLength, 32*1024)
}
for i, variant := range paddingSchedule2612[3].variants {
minimum, maximum := paddingVariantDelayBounds(variant)
if minimum != 45*time.Millisecond || maximum != 65*time.Millisecond {
t.Fatalf("turn 4 variant %d duration = %s-%s, want 45ms-65ms", i, minimum, maximum)
}
}
for i, variant := range paddingSchedule2612[5].variants {
minimum, maximum := paddingVariantDelayBounds(variant)
if minimum != 10*time.Millisecond || maximum != 25*time.Millisecond {
t.Fatalf("turn 6 variant %d duration = %s-%s, want 10ms-25ms", i, minimum, maximum)
}
}
}
func assertDelayRange(t *testing.T, name string, got paddingDelayRange, minimum, maximum time.Duration) {
t.Helper()
if got.min != minimum || got.max != maximum {
t.Fatalf("%s delay = %s-%s, want %s-%s", name, got.min, got.max, minimum, maximum)
}
}
func paddingVariantDelayBounds(variant paddingVariant) (time.Duration, time.Duration) {
var minimum time.Duration
var maximum time.Duration
for _, delay := range variant.delays {
minimum += delay.min
maximum += delay.max
}
return minimum, maximum
}
func slicesEqual[T comparable](left, right []T) bool {
if len(left) != len(right) {
return false
}
for i := range left {
if left[i] != right[i] {
return false
}
}
return true
}
func encodePaddingLength(t *testing.T, length int) []byte {
t.Helper()
var encoded bytes.Buffer
value := Varint(length)
if err := value.writeTo(&encoded); err != nil {
t.Fatal(err)
}
return encoded.Bytes()
}
type oneByteReader struct {
reader io.Reader
}
func (r *oneByteReader) Read(p []byte) (int, error) {
if len(p) > 1 {
p = p[:1]
}
return r.reader.Read(p)
}
type recordingWriter struct {
bytes.Buffer
writes []int
}
func (w *recordingWriter) Write(p []byte) (int, error) {
w.writes = append(w.writes, len(p))
return w.Buffer.Write(p)
}
@@ -0,0 +1,69 @@
package xmc
import "fmt"
type loginProfile struct {
Username string
UUID UUID
TexturesValue string
TexturesSignature string
}
func profilesFromConfig(configured []*Profile) ([]loginProfile, error) {
if len(configured) == 0 {
return nil, fmt.Errorf("empty profiles")
}
profiles := make([]loginProfile, 0, len(configured))
for _, configuredProfile := range configured {
if configuredProfile == nil || configuredProfile.Username == "" {
return nil, fmt.Errorf("invalid profile")
}
if len(configuredProfile.Uuid) != len(UUID{}) {
return nil, fmt.Errorf("bad profile UUID length: %d", len(configuredProfile.Uuid))
}
if configuredProfile.TexturesValue == "" || configuredProfile.TexturesSignature == "" {
return nil, fmt.Errorf("incomplete profile textures")
}
profile := loginProfile{
Username: configuredProfile.Username,
TexturesValue: configuredProfile.TexturesValue,
TexturesSignature: configuredProfile.TexturesSignature,
}
copy(profile.UUID[:], configuredProfile.Uuid)
profiles = append(profiles, profile)
}
return profiles, nil
}
func findProfile(profiles []loginProfile, username string, uuid UUID) (loginProfile, bool) {
for _, profile := range profiles {
if profile.Username == username && profile.UUID == uuid {
return profile, true
}
}
return loginProfile{}, false
}
func readLoginSuccess(packet *mcPacket) (loginProfile, error) {
var (
profile loginProfile
username String
propertyCount Varint
propertyName String
value String
signed Boolean
signature String
)
if err := packet.readFields(&profile.UUID, &username, &propertyCount, &propertyName, &value, &signed, &signature); err != nil {
return loginProfile{}, err
}
if propertyCount != 1 || propertyName != "textures" || !signed {
return loginProfile{}, fmt.Errorf("invalid login profile properties")
}
profile.Username = string(username)
profile.TexturesValue = string(value)
profile.TexturesSignature = string(signature)
return profile, nil
}
@@ -0,0 +1,33 @@
package xmc
import (
"bytes"
"testing"
)
func TestProfilesFromConfigRejectsEmpty(t *testing.T) {
if _, err := profilesFromConfig(nil); err == nil {
t.Fatal("expected empty profiles error")
}
}
func TestProfilesFromConfig(t *testing.T) {
uuid := bytes.Repeat([]byte{0x2a}, 16)
profiles, err := profilesFromConfig([]*Profile{
{
Username: "SignedUser",
Uuid: uuid,
TexturesValue: "textures-value",
TexturesSignature: "textures-signature",
},
})
if err != nil {
t.Fatalf("build explicit profile: %v", err)
}
if len(profiles) != 1 || profiles[0].Username != "SignedUser" {
t.Fatalf("unexpected profile: %+v", profiles)
}
if profiles[0].TexturesValue != "textures-value" || profiles[0].TexturesSignature != "textures-signature" {
t.Fatalf("textures were not preserved: %+v", profiles[0])
}
}
@@ -0,0 +1,444 @@
// Minecraft protocol
package xmc
import (
"bytes"
"fmt"
"io"
)
const (
maxPacketDataLength = 32 * 1024
maxPacketBodyLength = maxPacketDataLength + 5
)
type field interface {
readFrom(r io.Reader) error
writeTo(w io.Writer) error
}
type mcPacket struct {
packetID int
data []byte
}
func readPacket(b io.Reader) (*mcPacket, error) {
packet, _, err := readPacketWithLength(b)
return packet, err
}
func readPacketWithLength(b io.Reader) (*mcPacket, int, error) {
packetData, wireLength, err := readFrame(b, maxPacketBodyLength)
if err != nil {
return nil, 0, err
}
packet, err := decodePacketBody(packetData)
return packet, wireLength, err
}
func decodePacketBody(packetData []byte) (*mcPacket, error) {
if len(packetData) < 1 || len(packetData) > maxPacketBodyLength {
return nil, fmt.Errorf("read packet: bad length: %d", len(packetData))
}
body := bytes.NewReader(packetData)
var packetID Varint
err := packetID.readFrom(body)
if err != nil {
return nil, fmt.Errorf("read packet ID: %w", err)
}
dataLength := body.Len()
if dataLength > maxPacketDataLength {
return nil, fmt.Errorf("read packet: bad length: %d", dataLength)
}
data := make([]byte, dataLength)
_, err = io.ReadFull(body, data)
if err != nil {
return nil, fmt.Errorf("read packet data: %w", err)
}
return &mcPacket{
packetID: int(packetID),
data: data,
}, 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 {
return fmt.Errorf("read packet field: %w", err)
}
}
return nil
}
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
}
*v = 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)
for {
currentByte := byte(value & SEGMENT_BITS)
value >>= 7
if value != 0 {
currentByte |= CONTINUE_BIT
}
_, err := w.Write([]byte{currentByte})
if err != nil {
return fmt.Errorf("write varint: %w", err)
}
if value == 0 {
break
}
}
return nil
}
func varintSize(value Varint) int {
uintValue := uint32(value)
size := 0
for range 5 {
size++
uintValue >>= 7
if uintValue == 0 {
break
}
}
return size
}
type String string
func (v *String) readFrom(r io.Reader) error {
var length Varint = 0
err := length.readFrom(r)
if err != nil {
return fmt.Errorf("read string: %w", err)
}
if length < 0 || length > 4096 {
return fmt.Errorf("read string: bad length: %d", length)
}
buf := make([]byte, length)
_, err = io.ReadFull(r, buf)
if err != nil {
return fmt.Errorf("read string: %w", err)
}
*v = String(string(buf))
return nil
}
func (v *String) writeTo(w io.Writer) error {
strBytes := []byte(*v)
length := Varint(len(strBytes))
err := length.writeTo(w)
if err != nil {
return fmt.Errorf("write string: %w", err)
}
_, err = w.Write(strBytes)
if err != nil {
return fmt.Errorf("write string: %w", err)
}
return nil
}
type UnsignedShort uint16
func (v *UnsignedShort) readFrom(r io.Reader) error {
var buf [2]byte
_, err := io.ReadFull(r, buf[:])
if err != nil {
return fmt.Errorf("read unsigned short: %w", err)
}
*v = UnsignedShort(buf[0])<<8 | UnsignedShort(buf[1])
return nil
}
func (v *UnsignedShort) writeTo(w io.Writer) error {
buf := []byte{byte(*v >> 8), byte(*v & 0xFF)}
_, err := w.Write(buf)
if err != nil {
return fmt.Errorf("write unsigned short: %w", err)
}
return nil
}
type Long int64
func (v *Long) readFrom(r io.Reader) error {
var buf [8]byte
_, err := io.ReadFull(r, buf[:])
if err != nil {
return fmt.Errorf("read long: %w", err)
}
*v = Long(buf[0])<<56 | Long(buf[1])<<48 | Long(buf[2])<<40 | Long(buf[3])<<32 |
Long(buf[4])<<24 | Long(buf[5])<<16 | Long(buf[6])<<8 | Long(buf[7])
return nil
}
func (v *Long) writeTo(w io.Writer) error {
buf := []byte{
byte(*v >> 56), byte((*v >> 48) & 0xFF), byte((*v >> 40) & 0xFF), byte((*v >> 32) & 0xFF),
byte((*v >> 24) & 0xFF), byte((*v >> 16) & 0xFF), byte((*v >> 8) & 0xFF), byte(*v & 0xFF),
}
_, err := w.Write(buf)
if err != nil {
return fmt.Errorf("write long: %w", err)
}
return nil
}
type UUID [16]byte
func (v *UUID) readFrom(r io.Reader) error {
_, err := io.ReadFull(r, v[:])
if err != nil {
return fmt.Errorf("read UUID: %w", err)
}
return nil
}
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 {
return fmt.Errorf("write UUID: %w", err)
}
return nil
}
type Bytes []byte
func (v *Bytes) readFrom(r io.Reader) error {
var length Varint
err := length.readFrom(r)
if err != nil {
return fmt.Errorf("read bytes: %w", err)
}
if length < 0 || length >= 1024 {
return fmt.Errorf("read bytes: invalid size: %d", length)
}
buf := make([]byte, length)
_, err = io.ReadFull(r, buf)
if err != nil {
return fmt.Errorf("read bytes: %w", err)
}
*v = append([]byte(*v), buf...)
return nil
}
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)
if err != nil {
return fmt.Errorf("write bytes length: %w", err)
}
_, err = w.Write(*v)
if err != nil {
return fmt.Errorf("write bytes: %w", err)
}
return nil
}
func readByte(r io.Reader) (byte, error) {
var buf [1]byte
_, err := io.ReadFull(r, buf[:])
if err != nil {
return 0, fmt.Errorf("read byte: %w", err)
}
return buf[0], nil
}
func writePacket(w io.Writer, packetID int, fields ...field) error {
_, 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)
}
}
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 frame bytes.Buffer
frame.Grow(varintSize(Varint(bodyLength)) + bodyLength)
frameLength := Varint(bodyLength)
if err := frameLength.writeTo(&frame); err != nil {
return nil, fmt.Errorf("write packet length: %w", err)
}
if err := packetIDVarint.writeTo(&frame); err != nil {
return nil, fmt.Errorf("write packet ID: %w", err)
}
frame.Write(dataBuf.Bytes())
return frame.Bytes(), nil
}
func writeFull(w io.Writer, p []byte) error {
for len(p) > 0 {
n, err := w.Write(p)
if n > 0 {
p = p[n:]
}
if err != nil {
return err
}
if n == 0 {
return io.ErrShortWrite
}
}
return nil
}
func writeDisconnectPacket(w io.Writer, reason string) error {
return writePacket(w, 0x00, new(String(reason)))
}
@@ -0,0 +1,56 @@
package xmc
import (
"bytes"
"strings"
"testing"
)
func TestReadPacketDoesNotConsumeFollowingPacket(t *testing.T) {
data := []byte{0x01, 0x80, 0x01, 0x00}
r := bytes.NewReader(data)
if _, err := readPacket(r); err == nil {
t.Fatal("expected truncated packet ID to fail")
}
pkt, err := readPacket(r)
if err != nil {
t.Fatalf("read following packet: %v", err)
}
if pkt.packetID != 0 {
t.Fatalf("packet ID = %d", pkt.packetID)
}
}
func TestPacketWithLengthReportsWireBytes(t *testing.T) {
var wire bytes.Buffer
written, err := writePacketWithLength(&wire, 0x03)
if err != nil {
t.Fatal(err)
}
if written != 2 || !bytes.Equal(wire.Bytes(), []byte{0x01, 0x03}) {
t.Fatalf("wire = %x, length = %d", wire.Bytes(), written)
}
packet, read, err := readPacketWithLength(bytes.NewReader(wire.Bytes()))
if err != nil {
t.Fatal(err)
}
if packet.packetID != 0x03 || read != written {
t.Fatalf("packet ID = %d, read = %d, written = %d", packet.packetID, read, written)
}
}
func TestReadPacketRejectsNonCanonicalLengthVarint(t *testing.T) {
_, _, err := readPacketWithLength(bytes.NewReader([]byte{0x81, 0x00, 0x03}))
if err == nil || !strings.Contains(err.Error(), "non-canonical") {
t.Fatalf("error = %v", err)
}
}
func TestVarintRejectsOversizedFifthByte(t *testing.T) {
var value Varint
err := value.readFrom(bytes.NewReader([]byte{0xff, 0xff, 0xff, 0xff, 0x1f}))
if err == nil || !strings.Contains(err.Error(), "too large") {
t.Fatalf("error = %v", err)
}
}
+364
View File
@@ -0,0 +1,364 @@
package xmc
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/subtle"
"crypto/x509"
"fmt"
"io"
"net"
"sync"
"time"
)
// Response by vanilla 26.1.2 server.
var statusResponse = `{"description":"A Minecraft Server","players":{"max":20,"online":0},"version":{"name":"26.1.2","protocol":775},"enforcesSecureChat":true}`
type serverState int
var (
serverStateHandshake serverState = 1
serverStateProxy serverState = 3
)
type serverConn struct {
reader io.Reader
writer io.Writer
c net.Conn
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
}
func (c *serverConn) handshake() error {
c.handshakeLock.Lock()
defer c.handshakeLock.Unlock()
if c.state != serverStateHandshake {
return nil
}
if err := c.deadlines.beginHandshake(); err != nil {
return fmt.Errorf("set deadline: %w", err)
}
defer func() { _ = c.deadlines.endHandshake() }()
var (
protocolVersion Varint
serverAddress String
serverPort UnsignedShort
nextState Varint
)
// handshake packet
pkt, err := readPacket(c.reader)
if err != nil {
return fmt.Errorf("read handshake packet: %w", err)
}
if pkt.packetID != 0 {
return fmt.Errorf("bad handshake packet id")
}
err = pkt.readFields(&protocolVersion, &serverAddress, &serverPort, &nextState)
if err != nil {
return fmt.Errorf("read handshake packet: %w", err)
}
switch nextState {
case 1:
// Ping
for range 2 {
pkt, err := readPacket(c.reader)
if err != nil {
return fmt.Errorf("read packet: %w", err)
}
switch pkt.packetID {
case 0: // Status Request
err = writePacket(c.writer, 0, new(String(statusResponse)))
if err != nil {
return fmt.Errorf("write status response: %w", err)
}
case 1: // Ping
var payload Long
err = pkt.readFields(&payload)
if err != nil {
return fmt.Errorf("read ping packet: %w", err)
}
err = writePacket(c.writer, 1, &payload)
if err != nil {
return fmt.Errorf("write ping response: %w", err)
}
}
}
return fmt.Errorf("ping")
case 2:
// Login
// login start
pkt, err := readPacket(c.reader)
if err != nil {
return fmt.Errorf("read login start packet: %w", err)
}
if pkt.packetID != 0 {
return fmt.Errorf("bad login start packet id")
}
var (
username String
uuid UUID
)
err = pkt.readFields(&username, &uuid)
if err != nil {
return fmt.Errorf("read login start packet: %w", err)
}
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
)
if _, err = rand.Read(verifyToken); err != nil {
return fmt.Errorf("generate verify token: %w", err)
}
err = writePacket(c.writer, 0x01, &serverId, &publicKey, &verifyToken, &shouldAuthenticate)
if err != nil {
return fmt.Errorf("write encryption request: %w", err)
}
// encrypt response
var (
encryptedSharedSecret Bytes
encryptedVerifyToken Bytes
sharedSecret []byte
decryptedVerifyToken []byte
)
pkt, err = readPacket(c.reader)
if err != nil {
return fmt.Errorf("read encrypt response: %w", err)
}
if pkt.packetID != 0x01 {
return fmt.Errorf("bad encrypt response packet id")
}
err = pkt.readFields(&encryptedSharedSecret, &encryptedVerifyToken)
if err != nil {
return fmt.Errorf("read encrypt response: %w", err)
}
sharedSecret, err = rsa.DecryptPKCS1v15(rand.Reader, c.rsaPrivateKey, encryptedSharedSecret)
if err != nil {
return fmt.Errorf("decrypt shared secret: %w", err)
}
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 {
return fmt.Errorf("decrypt verify token: %w", err)
}
if len(decryptedVerifyToken) < 4 || !bytes.Equal(verifyToken, decryptedVerifyToken[:4]) {
return fmt.Errorf("verify token mismatch")
}
c.reader, err = newCryptoReader(c.reader, sharedSecret)
if err != nil {
return fmt.Errorf("new crypto reader: %w", err)
}
c.writer, err = newCryptoWriter(c.writer, sharedSecret)
if err != nil {
return fmt.Errorf("new crypto writer: %w", err)
}
// verify password
receivedPassword := decryptedVerifyToken[4:]
if subtle.ConstantTimeCompare(receivedPassword, []byte(c.password)) != 1 {
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
default:
return fmt.Errorf("bad handshake packet: bad next state: %d", nextState)
}
}
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 {
return 0, fmt.Errorf("handshake: %w", err)
}
return c.reader.Read(b)
}
func (c *serverConn) Write(b []byte) (int, error) {
err := c.handshake()
if err != nil {
return 0, fmt.Errorf("handshake: %w", err)
}
return c.writer.Write(b)
}
func (c *serverConn) Close() error {
c.lifecycleMu.Lock()
c.closed = true
packet := c.packet
c.lifecycleMu.Unlock()
if packet != nil {
packet.Stop()
}
return c.c.Close()
}
func (c *serverConn) LocalAddr() net.Addr {
return c.c.LocalAddr()
}
func (c *serverConn) RemoteAddr() net.Addr {
return c.c.RemoteAddr()
}
func (c *serverConn) SetDeadline(t time.Time) error {
return c.deadlines.setDeadline(t)
}
func (c *serverConn) SetReadDeadline(t time.Time) error {
return c.deadlines.setReadDeadline(t)
}
func (c *serverConn) SetWriteDeadline(t time.Time) error {
return c.deadlines.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")
}
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),
}
return s, nil
}
@@ -0,0 +1,79 @@
// Minecraft stream cipher
package xmc
import (
"crypto/aes"
"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) {
blockCipher, err := aes.NewCipher(sharedSecret)
if err != nil {
return nil, fmt.Errorf("new aes cipher: %w", err)
}
stream := newCFB8Decrypt(blockCipher, sharedSecret)
return &cryptoStream{stream: stream, r: r}, nil
}
func (c *cryptoStream) Read(b []byte) (int, error) {
if c.r == nil {
panic("read on a write-only crypto stream")
}
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 n, nil
}
func newCryptoWriter(w io.Writer, sharedSecret []byte) (*cryptoStream, error) {
blockCipher, err := aes.NewCipher(sharedSecret)
if err != nil {
return nil, fmt.Errorf("new aes cipher: %w", err)
}
stream := newCFB8Encrypt(blockCipher, sharedSecret)
return &cryptoStream{stream: stream, w: w}, nil
}
func (c *cryptoStream) Write(b []byte) (int, error) {
if c.w == nil {
panic("write on a read-only crypto stream")
}
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 {
return 0, fmt.Errorf("crypto writer: write: %w", err)
}
return len(b), nil
}
@@ -0,0 +1,81 @@
package xmc
import (
"bytes"
"io"
"testing"
)
type dataAndEOFReader struct {
data []byte
}
func (r *dataAndEOFReader) Read(p []byte) (int, error) {
if len(r.data) == 0 {
return 0, io.EOF
}
n := copy(p, r.data)
r.data = r.data[n:]
return n, io.EOF
}
type shortWriter struct {
bytes.Buffer
}
func (w *shortWriter) Write(p []byte) (int, error) {
if len(p) > 1 {
p = p[:len(p)/2]
}
return w.Buffer.Write(p)
}
func TestCryptoReaderPreservesDataReturnedWithEOF(t *testing.T) {
secret := []byte("0123456789abcdef")
plaintext := []byte("payload returned with EOF")
var encrypted bytes.Buffer
writer, err := newCryptoWriter(&encrypted, secret)
if err != nil {
t.Fatal(err)
}
if _, err = writer.Write(plaintext); err != nil {
t.Fatal(err)
}
reader, err := newCryptoReader(&dataAndEOFReader{data: encrypted.Bytes()}, secret)
if err != nil {
t.Fatal(err)
}
got := make([]byte, len(plaintext))
n, err := reader.Read(got)
if err == nil || n != len(plaintext) {
t.Fatalf("Read = %d, %v", n, err)
}
if !bytes.Equal(got[:n], plaintext) {
t.Fatalf("plaintext = %q", got[:n])
}
}
func TestCryptoWriterHandlesShortWrites(t *testing.T) {
secret := []byte("0123456789abcdef")
plaintext := bytes.Repeat([]byte("short-write"), 100)
var dst shortWriter
writer, err := newCryptoWriter(&dst, secret)
if err != nil {
t.Fatal(err)
}
if n, err := writer.Write(plaintext); err != nil || n != len(plaintext) {
t.Fatalf("Write = %d, %v", n, err)
}
reader, err := newCryptoReader(bytes.NewReader(dst.Bytes()), secret)
if err != nil {
t.Fatal(err)
}
got, err := io.ReadAll(reader)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, plaintext) {
t.Fatal("decrypted payload mismatch")
}
}
+1 -4
View File
@@ -136,10 +136,7 @@ func getGrpcClient(ctx context.Context, dest net.Destination, streamSettings *in
}
if tlsConfig != nil {
config := tlsConfig.GetTLSConfig()
if config.ServerName == "" && address.Family().IsDomain() {
config.ServerName = address.Domain()
}
config := tlsConfig.GetTLSConfig(tls.WithDestination(dest))
if fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint); fingerprint != nil {
return tls.UClient(c, config, fingerprint), nil
} else { // Fallback to normal gRPC TLS
@@ -38,12 +38,14 @@ func NewHunkReadWriter(hc HunkConn, cancel context.CancelFunc) *HunkReaderWriter
func NewHunkConn(hc HunkConn, cancel context.CancelFunc, trustedXForwardedFor []string) net.Conn {
rAddr := remoteAddrFromContext(hc.Context(), trustedXForwardedFor)
lAddr := localAddrFromContext(hc.Context())
wrc := NewHunkReadWriter(hc, cancel)
return cnc.NewConnection(
cnc.ConnectionInput(wrc),
cnc.ConnectionOutput(wrc),
cnc.ConnectionOnClose(wrc),
cnc.ConnectionRemoteAddr(rAddr),
cnc.ConnectionLocalAddr(lAddr),
)
}
@@ -33,12 +33,14 @@ func NewMultiHunkReadWriter(hc MultiHunkConn, cancel context.CancelFunc) *MultiH
func NewMultiHunkConn(hc MultiHunkConn, cancel context.CancelFunc, trustedXForwardedFor []string) net.Conn {
rAddr := remoteAddrFromContext(hc.Context(), trustedXForwardedFor)
lAddr := localAddrFromContext(hc.Context())
wrc := NewMultiHunkReadWriter(hc, cancel)
return cnc.NewConnection(
cnc.ConnectionInputMulti(wrc),
cnc.ConnectionOutputMulti(wrc),
cnc.ConnectionOnClose(wrc),
cnc.ConnectionRemoteAddr(rAddr),
cnc.ConnectionLocalAddr(lAddr),
)
}
@@ -56,3 +56,17 @@ func parseTrustedXForwardedFor(md metadata.MD, trusted []string, remoteAddr net.
}
return nil
}
func localAddrFromContext(ctx context.Context) net.Addr {
var localAddr net.Addr
if pr, ok := peer.FromContext(ctx); ok {
localAddr = pr.LocalAddr
}
if localAddr == nil {
localAddr = &net.TCPAddr{
IP: []byte{0, 0, 0, 0},
Port: 0,
}
}
return localAddr
}
+1 -1
View File
@@ -345,7 +345,7 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
c = &client{
dest: dest,
config: streamSettings.ProtocolSettings.(*Config),
tlsConfig: tlsConfig.GetTLSConfig(),
tlsConfig: tlsConfig.GetTLSConfig(tls.WithDestination(dest)),
socketConfig: streamSettings.SocketSettings,
udpmaskManager: streamSettings.UdpmaskManager,
quicParams: streamSettings.QuicParams,
+34 -33
View File
@@ -169,41 +169,42 @@ func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig)
return errors.New("failed to set TCP_MAXSEG", err)
}
}
if len(config.CustomSockopt) > 0 {
for _, custom := range config.CustomSockopt {
if custom.System != "" && custom.System != runtime.GOOS {
errors.LogDebug(context.Background(), "CustomSockopt system not match: ", "want ", custom.System, " got ", runtime.GOOS)
continue
}
if len(config.CustomSockopt) > 0 {
for _, custom := range config.CustomSockopt {
if custom.System != "" && custom.System != runtime.GOOS {
errors.LogDebug(context.Background(), "CustomSockopt system not match: ", "want ", custom.System, " got ", runtime.GOOS)
continue
}
// Skip unwanted network type
// network might be tcp4 or tcp6
// use HasPrefix so that "tcp" can match tcp4/6 with "tcp" if user want to control all tcp (udp is also the same)
// if it is empty, strings.HasPrefix will always return true to make it apply for all networks
if !strings.HasPrefix(network, custom.Network) {
continue
}
level := 0x6 // default TCP
var opt int
if len(custom.Opt) == 0 {
return errors.New("No opt!")
} else {
opt, _ = strconv.Atoi(custom.Opt)
}
if custom.Level != "" {
level, _ = strconv.Atoi(custom.Level)
}
if custom.Type == "int" {
value, _ := strconv.Atoi(custom.Value)
if err := syscall.SetsockoptInt(int(fd), level, opt, value); err != nil {
return errors.New("failed to set CustomSockoptInt", opt, value, err)
}
// Skip unwanted network type
// network might be tcp4 or tcp6
// use HasPrefix so that "tcp" can match tcp4/6 with "tcp" if user want to control all tcp (udp is also the same)
// if it is empty, strings.HasPrefix will always return true to make it apply for all networks
if !strings.HasPrefix(network, custom.Network) {
continue
}
level := 0x6 // default TCP
var opt int
if len(custom.Opt) == 0 {
return errors.New("No opt!")
} else {
opt, _ = strconv.Atoi(custom.Opt)
}
if custom.Level != "" {
level, _ = strconv.Atoi(custom.Level)
}
if custom.Type == "int" {
value, _ := strconv.Atoi(custom.Value)
if err := syscall.SetsockoptInt(int(fd), level, opt, value); err != nil {
return errors.New("failed to set CustomSockoptInt", opt, value, err)
}
} else if custom.Type == "str" {
if err := syscall.SetsockoptString(int(fd), level, opt, custom.Value); err != nil {
return errors.New("failed to set CustomSockoptString", opt, custom.Value, err)
}
} else {
return errors.New("unknown CustomSockopt type:", custom.Type)
} else if custom.Type == "str" {
if err := syscall.SetsockoptString(int(fd), level, opt, custom.Value); err != nil {
return errors.New("failed to set CustomSockoptString", opt, custom.Value, err)
}
} else {
return errors.New("unknown CustomSockopt type:", custom.Type)
}
}
}
+5 -1
View File
@@ -373,11 +373,15 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
Reader: request.Body,
ResponseWriter: writer,
}
localAddr := h.localAddr
if la, ok := request.Context().Value(http.LocalAddrContextKey).(net.Addr); ok && la != nil {
localAddr = la
}
conn := splitConn{
writer: httpSC,
reader: httpSC,
remoteAddr: remoteAddr,
localAddr: h.localAddr,
localAddr: localAddr,
}
if sessionId != "" { // if not stream-one
conn.reader = currentSession.uploadQueue
+3
View File
@@ -331,6 +331,9 @@ func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509
}
if verifyResult == foundCA { // if found CA, we need to verify here
if len(r.Config.ServerName) == 0 {
return errors.New("Pinning CA needs a valid ServerName")
}
opts := x509.VerifyOptions{
Roots: CAs,
CurrentTime: time.Now(),