Compare 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
yiguodevandGitHub d5bc58dc6b Root config: Add env config (#6400)
https://github.com/XTLS/Xray-core/pull/6400#issuecomment-4880198100
https://github.com/XTLS/Xray-core/pull/6400#issuecomment-4916892425
https://github.com/XTLS/Xray-core/pull/6400#issuecomment-4929136670
2026-07-10 02:56:49 +00:00
patternihaandGitHub c18b39ed80 TLS client: Support more cipherSuites for "unsafe" (golang) fingerprint for anti-NIN (#6450)
https://github.com/XTLS/Xray-core/pull/6450#issuecomment-4929495504
2026-07-10 02:02:31 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e2ad0acf60 Bump github.com/pires/go-proxyproto from 0.14.0 to 0.15.0 (#6464)
Bumps [github.com/pires/go-proxyproto](https://github.com/pires/go-proxyproto) from 0.14.0 to 0.15.0.
- [Release notes](https://github.com/pires/go-proxyproto/releases)
- [Commits](https://github.com/pires/go-proxyproto/compare/v0.14.0...v0.15.0)

---
updated-dependencies:
- dependency-name: github.com/pires/go-proxyproto
  dependency-version: 0.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 01:56:16 +00:00
c320e89108 HTTPUpgrade server: Keep accepting after transient errors as RAW does; Add handshake read deadline as WS does (#6463)
https://github.com/XTLS/Xray-core/pull/6463#issuecomment-4929183257

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-07-09 20:55:40 +00:00
LjhAUMEMandGitHub 412898fed7 WireGuard outbound: Fix a race condition (#6461)
Fixes https://github.com/XTLS/Xray-core/pull/6321#issuecomment-4881463309
2026-07-09 20:40:02 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5c62d50d43 Bump golang.org/x/net from 0.56.0 to 0.57.0 (#6455)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.56.0 to 0.57.0.
- [Commits](https://github.com/golang/net/compare/v0.56.0...v0.57.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.57.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 14:11:38 +00:00
74 changed files with 4886 additions and 197 deletions
+1
View File
@@ -37,6 +37,7 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json
RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json
RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
# Create log files
+1
View File
@@ -37,6 +37,7 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json
RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json
RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
# Create log files
+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
@@ -122,7 +122,7 @@ func NewReader(reader io.Reader) Reader {
}
_, isFile := reader.(*os.File)
if !isFile && useReadv {
if !isFile && useReadV() {
if sc, ok := reader.(syscall.Conn); ok {
rawConn, err := sc.SyscallConn()
if err != nil {
+15 -3
View File
@@ -5,6 +5,7 @@ package buf
import (
"io"
"sync/atomic"
"syscall"
"github.com/xtls/xray-core/common/platform"
@@ -143,13 +144,24 @@ func (r *ReadVReader) ReadMultiBuffer() (MultiBuffer, error) {
return mb, nil
}
var useReadv bool
var useReadv atomic.Bool
func init() {
func useReadV() bool {
return useReadv.Load()
}
func reloadEnvSettings() error {
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
value := platform.NewEnvFlag(platform.UseReadV).GetValue(func() string { return defaultFlagValue })
enabled := false
switch value {
case defaultFlagValue, "auto", "enable":
useReadv = true
enabled = true
}
useReadv.Store(enabled)
return nil
}
func init() {
platform.RegisterEnvReload(reloadEnvSettings)
}
+3 -1
View File
@@ -10,7 +10,9 @@ import (
"github.com/xtls/xray-core/features/stats"
)
const useReadv = false
func useReadV() bool {
return false
}
func NewReadVReader(reader io.Reader, rawConn syscall.RawConn, counter stats.Counter) Reader {
panic("not implemented")
+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")
}
+40
View File
@@ -0,0 +1,40 @@
package platform
import (
"errors"
"sync"
)
var envReloadRegistry = struct {
sync.RWMutex
handlers []func() error
}{}
// RegisterEnvReload registers an environment reload handler and runs it once
// immediately so package defaults keep the same behavior as init-time reads.
func RegisterEnvReload(handler func() error) {
if handler == nil {
return
}
envReloadRegistry.Lock()
envReloadRegistry.handlers = append(envReloadRegistry.handlers, handler)
envReloadRegistry.Unlock()
if err := handler(); err != nil {
panic(err)
}
}
// ReloadEnvSettings refreshes all registered environment-backed package state.
func ReloadEnvSettings() error {
envReloadRegistry.RLock()
handlers := append([]func() error{}, envReloadRegistry.handlers...)
envReloadRegistry.RUnlock()
var errs []error
for _, handler := range handlers {
if err := handler(); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
+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)])
+33 -19
View File
@@ -8,7 +8,7 @@ import (
"io"
"strconv"
"strings"
"time"
"sync/atomic"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
@@ -27,25 +27,39 @@ var AddrParser = protocol.NewAddressParser(
)
var (
Show bool
BaseKey []byte
Show atomic.Bool
baseKey atomic.Value
)
func init() {
if strings.ToLower(platform.NewEnvFlag(platform.XUDPLog).GetValue(func() string { return "" })) == "true" {
Show = true
func reloadEnvSettings() error {
Show.Store(strings.ToLower(platform.NewEnvFlag(platform.XUDPLog).GetValue(func() string { return "" })) == "true")
raw := platform.NewEnvFlag(platform.XUDPBaseKey).GetValue(func() string { return "" })
if raw == "" {
ensureBaseKey()
return nil
}
BaseKey = make([]byte, 32)
rand.Read(BaseKey)
go func() {
time.Sleep(100 * time.Millisecond) // this is not nice, but need to give some time for Android to setup ENV
if raw := platform.NewEnvFlag(platform.XUDPBaseKey).GetValue(func() string { return "" }); raw != "" {
if BaseKey, _ = base64.RawURLEncoding.DecodeString(raw); len(BaseKey) == 32 {
return
}
panic(platform.XUDPBaseKey + ": invalid value (BaseKey must be 32 bytes): " + raw + " len " + strconv.Itoa(len(BaseKey)))
}
}()
key, _ := base64.RawURLEncoding.DecodeString(raw)
if len(key) != 32 {
return errors.New(platform.XUDPBaseKey + ": invalid value (BaseKey must be 32 bytes): " + raw + " len " + strconv.Itoa(len(key)))
}
baseKey.Store(append([]byte(nil), key...))
return nil
}
func ensureBaseKey() []byte {
if key := baseKey.Load(); key != nil {
return key.([]byte)
}
key := make([]byte, 32)
if _, err := rand.Read(key); err != nil {
panic(err)
}
baseKey.Store(key)
return key
}
func init() {
platform.RegisterEnvReload(reloadEnvSettings)
}
func GetGlobalID(ctx context.Context) (globalID [8]byte) {
@@ -54,10 +68,10 @@ func GetGlobalID(ctx context.Context) (globalID [8]byte) {
}
if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.Network == net.Network_UDP &&
(inbound.Name == "dokodemo-door" || inbound.Name == "socks" || inbound.Name == "shadowsocks" || inbound.Name == "tun") {
h := blake3.New(8, BaseKey)
h := blake3.New(8, ensureBaseKey())
h.Write([]byte(inbound.Source.String()))
copy(globalID[:], h.Sum(nil))
if Show {
if Show.Load() {
errors.LogInfo(ctx, fmt.Sprintf("XUDP inbound.Source.String(): %v\tglobalID: %v\n", inbound.Source.String(), globalID))
}
}
+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 (
+3
View File
@@ -187,6 +187,9 @@ func NewWithContext(ctx context.Context, config *Config) (*Instance, error) {
}
func initInstanceWithConfig(config *Config, server *Instance) (bool, error) {
if err := platform.ReloadEnvSettings(); err != nil {
return true, errors.New("failed to reload environment settings").Base(err)
}
server.ctx = context.WithValue(server.ctx, "cone",
platform.NewEnvFlag(platform.UseCone).GetValue(func() string { return "" }) != "true")
+18 -8
View File
@@ -3,6 +3,7 @@ package policy
import (
"context"
"runtime"
"sync/atomic"
"time"
"github.com/xtls/xray-core/common/platform"
@@ -82,32 +83,41 @@ func ManagerType() interface{} {
return (*Manager)(nil)
}
var defaultBufferSize int32
var defaultBufferSize atomic.Int32
func init() {
func reloadEnvSettings() error {
defaultBufferSize.Store(readDefaultBufferSize())
return nil
}
func readDefaultBufferSize() int32 {
const defaultValue = -17
size := platform.NewEnvFlag(platform.BufferSize).GetValueAsInt(defaultValue)
switch size {
case 0:
defaultBufferSize = -1 // For pipe to use unlimited size
return -1 // For pipe to use unlimited size
case defaultValue: // Env flag not defined. Use default values per CPU-arch.
switch runtime.GOARCH {
case "arm", "mips", "mipsle":
defaultBufferSize = 0
return 0
case "arm64", "mips64", "mips64le":
defaultBufferSize = 4 * 1024 // 4k cache for low-end devices
return 4 * 1024 // 4k cache for low-end devices
default:
defaultBufferSize = 512 * 1024
return 512 * 1024
}
default:
defaultBufferSize = int32(size) * 1024 * 1024
return int32(size) * 1024 * 1024
}
}
func init() {
platform.RegisterEnvReload(reloadEnvSettings)
}
func defaultBufferPolicy() Buffer {
return Buffer{
PerConnection: defaultBufferSize,
PerConnection: defaultBufferSize.Load(),
}
}
+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
+9 -9
View File
@@ -14,7 +14,7 @@ require (
github.com/miekg/dns v1.1.72
github.com/pelletier/go-toml v1.9.5
github.com/pion/stun/v3 v3.1.6
github.com/pires/go-proxyproto v0.14.0
github.com/pires/go-proxyproto v0.15.0
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
github.com/robfig/cron/v3 v3.0.1
github.com/sagernet/sing v0.5.1
@@ -23,15 +23,15 @@ require (
github.com/vishvananda/netlink v1.3.1
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.53.0
golang.org/x/crypto v0.54.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/net v0.56.0
golang.org/x/sync v0.21.0
golang.org/x/sys v0.46.0
golang.org/x/net v0.57.0
golang.org/x/sync v0.22.0
golang.org/x/sys v0.47.0
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
@@ -52,10 +52,10 @@ require (
github.com/quic-go/qpack v0.6.0 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/text v0.38.0 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/text v0.40.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.45.0 // indirect
golang.org/x/tools v0.47.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+18 -18
View File
@@ -53,8 +53,8 @@ github.com/pion/stun/v3 v3.1.6 h1:WnhsD0eHCiwCfKNkVx0VJJwr2Y3eV4Ueih3KJ+dfZy8=
github.com/pion/stun/v3 v3.1.6/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs=
github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk=
github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
github.com/pires/go-proxyproto v0.14.0 h1:2vIGIfVG8eVRsKF0xukEoeT5RWhDXxBU0uv6smLOKdI=
github.com/pires/go-proxyproto v0.14.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24=
github.com/pires/go-proxyproto v0.15.0 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI=
github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
@@ -98,22 +98,22 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -121,21 +121,21 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -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=
+59
View File
@@ -0,0 +1,59 @@
package conf
import (
"encoding/json"
"os"
"testing"
)
func TestRootEnvAppliesArbitraryValues(t *testing.T) {
const (
valueKey = "XRAY_TEST_CONFIG_ENV"
emptyKey = "XRAY_TEST_CONFIG_EMPTY"
)
t.Setenv(valueKey, "before")
t.Setenv(emptyKey, "before")
config := new(Config)
if err := json.Unmarshal([]byte(`{
"env": {
"XRAY_TEST_CONFIG_ENV": "configured",
"XRAY_TEST_CONFIG_EMPTY": ""
}
}`), config); err != nil {
t.Fatal(err)
}
if _, err := config.Build(); err != nil {
t.Fatal(err)
}
if got := os.Getenv(valueKey); got != "configured" {
t.Fatalf("env %q = %q, want %q", valueKey, got, "configured")
}
if got := os.Getenv(emptyKey); got != "" {
t.Fatalf("env %q = %q, want empty", emptyKey, got)
}
}
func TestEnvConfigOverride(t *testing.T) {
base := EnvConfig{
"ONE": "one",
"TWO": "old",
}
override := EnvConfig{
"TWO": "new",
"THREE": "three",
}
base.Override(override)
want := map[string]string{
"ONE": "one",
"TWO": "new",
"THREE": "three",
}
for key, value := range want {
if got := base[key]; got != value {
t.Fatalf("env %q = %q, want %q", key, got, value)
}
}
}
+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
}
+23 -1
View File
@@ -3,6 +3,7 @@ package conf
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
@@ -175,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 {
@@ -379,11 +380,20 @@ func (c *StatsConfig) Build() (*stats.Config, error) {
return &stats.Config{}, nil
}
type EnvConfig map[string]string
func (c EnvConfig) Override(o EnvConfig) {
for key, value := range o {
c[key] = value
}
}
type Config struct {
// Deprecated: Global transport config is no longer used
// left for returning error
Transport map[string]json.RawMessage `json:"transport"`
Env EnvConfig `json:"env"`
LogConfig *LogConfig `json:"log"`
RouterConfig *RouterConfig `json:"routing"`
DNSConfig *DNSConfig `json:"dns"`
@@ -439,6 +449,12 @@ func (c *Config) Override(o *Config, fn string) {
if o.Transport != nil {
c.Transport = o.Transport
}
if o.Env != nil {
if c.Env == nil {
c.Env = EnvConfig{}
}
c.Env.Override(o.Env)
}
if o.Policy != nil {
c.Policy = o.Policy
}
@@ -514,6 +530,12 @@ func (c *Config) Override(o *Config, fn string) {
// Build implements Buildable.
func (c *Config) Build() (*core.Config, error) {
for key, value := range c.Env {
if err := os.Setenv(key, value); err != nil {
return nil, errors.New("failed to apply environment configuration").Base(err)
}
}
if err := PostProcessConfigureFile(c); err != nil {
return nil, errors.New("failed to post-process configuration file").Base(err)
}
+4
View File
@@ -36,6 +36,10 @@ The -confdir=dir flag sets a dir with multiple json config
The -format=json flag sets the format of config files.
Default "auto".
The config root env object sets process environment variables after all config
files are parsed. Variables needed to locate or parse config files must be set
in the process environment before Xray starts.
The -test flag tells Xray to test config files only,
without launching the server.
+16 -8
View File
@@ -5,6 +5,7 @@ import (
"crypto/rand"
"io"
"strings"
"sync/atomic"
"time"
"github.com/pires/go-proxyproto"
@@ -31,12 +32,24 @@ import (
)
var (
useSplice bool
useSplice atomic.Bool
allNetworks [8]bool
defaultBlockPrivateRule *FinalRule
defaultBlockAllRule *FinalRule
)
func reloadEnvSettings() error {
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue })
enabled := false
switch value {
case defaultFlagValue, "auto", "enable":
enabled = true
}
useSplice.Store(enabled)
return nil
}
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
h := new(Handler)
@@ -48,12 +61,7 @@ func init() {
return h, nil
}))
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue })
switch value {
case defaultFlagValue, "auto", "enable":
useSplice = true
}
platform.RegisterEnvReload(reloadEnvSettings)
for i := range allNetworks {
allNetworks[i] = true
@@ -422,7 +430,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
responseDone := func() error {
defer timer.SetTimeout(plcy.Timeouts.UplinkOnly)
if destination.Network == net.Network_TCP && useSplice && proxy.IsRAWTransportWithoutSecurity(conn) { // it would be tls conn in special use case of MITM, we need to let link handle traffic
if destination.Network == net.Network_TCP && useSplice.Load() && proxy.IsRAWTransportWithoutSecurity(conn) { // it would be tls conn in special use case of MITM, we need to let link handle traffic
var writeConn net.Conn
var inTimer *signal.ActivityTimer
if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Conn != 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
}
+11 -8
View File
@@ -5,6 +5,7 @@ import (
"crypto/hmac"
"crypto/sha256"
"hash/crc64"
"sync/atomic"
"time"
"github.com/xtls/xray-core/common"
@@ -218,10 +219,17 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
return nil
}
var enablePadding = false
var enablePadding atomic.Bool
func shouldEnablePadding(s protocol.SecurityType) bool {
return enablePadding || s == protocol.SecurityType_AES128_GCM || s == protocol.SecurityType_CHACHA20_POLY1305 || s == protocol.SecurityType_AUTO
return enablePadding.Load() || s == protocol.SecurityType_AES128_GCM || s == protocol.SecurityType_CHACHA20_POLY1305 || s == protocol.SecurityType_AUTO
}
func reloadEnvSettings() error {
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
paddingValue := platform.NewEnvFlag(platform.UseVmessPadding).GetValue(func() string { return defaultFlagValue })
enablePadding.Store(paddingValue != defaultFlagValue)
return nil
}
func init() {
@@ -229,10 +237,5 @@ func init() {
return New(ctx, config.(*Config))
}))
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
paddingValue := platform.NewEnvFlag(platform.UseVmessPadding).GetValue(func() string { return defaultFlagValue })
if paddingValue != defaultFlagValue {
enablePadding = true
}
platform.RegisterEnvReload(reloadEnvSettings)
}
+7 -10
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
}
@@ -139,13 +139,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
ob.Name = "wireguard"
ob.CanSpliceCopy = 3
if h.dev == nil {
if err := h.init(ctx); err != nil {
return err
}
}
if err := h.dev.Up(); err != nil {
if err := h.init(ctx); err != nil {
return err
}
@@ -258,8 +252,11 @@ func (h *Handler) Close() (err error) {
func (h *Handler) init(ctx context.Context) error {
h.mu.Lock()
defer h.mu.Unlock()
if h.tun == nil {
return errors.New("closed")
}
if h.dev != nil {
return nil
return h.dev.Up()
}
resolveFunc := h.resolveLocal
listenFunc := func() (net.PacketConn, error) {
+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
}
+14 -4
View File
@@ -27,9 +27,10 @@ type task struct {
}
var (
conns chan *websocket.Conn
server *http.Server
mu sync.Mutex
conns chan *websocket.Conn
server *http.Server
currentAddr string
mu sync.Mutex
)
var upgrader = &websocket.Upgrader{
@@ -47,8 +48,13 @@ func Reload() {
mu.Lock()
defer mu.Unlock()
if addr == currentAddr && (addr == "" || server != nil) {
return
}
if server != nil {
server.Close()
server = nil
}
if HasBrowserDialer() {
for len(conns) > 0 {
@@ -60,6 +66,7 @@ func Reload() {
}
conns = nil
}
currentAddr = addr
if addr != "" {
token := uuid.New()
csrfToken := token.String()
@@ -220,5 +227,8 @@ func CheckOK(conn *websocket.Conn) error {
}
func init() {
Reload()
platform.RegisterEnvReload(func() error {
Reload()
return nil
})
}
+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")
}
}
@@ -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
}
+16 -2
View File
@@ -4,8 +4,10 @@ import (
"bufio"
"context"
"crypto/tls"
"io"
"net/http"
"strings"
"time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
@@ -43,7 +45,11 @@ func (s *server) Handle(conn net.Conn) {
// upgrade execute a fake websocket upgrade process and return the available connection
func (s *server) upgrade(conn net.Conn) (stat.Connection, error) {
connReader := bufio.NewReader(conn)
// timeout and header limit are the same as websocket
conn.SetReadDeadline(time.Now().Add(time.Second * 4))
defer conn.SetReadDeadline(time.Time{})
connReader := bufio.NewReader(io.LimitReader(conn, 12288))
req, err := http.ReadRequest(connReader)
if err != nil {
return nil, err
@@ -94,7 +100,15 @@ func (s *server) keepAccepting() {
for {
conn, err := s.innnerListener.Accept()
if err != nil {
return
errStr := err.Error()
if strings.Contains(errStr, "closed") {
break
}
errors.LogWarningInner(context.Background(), err, "failed to accept raw connections")
if strings.Contains(errStr, "too many") {
time.Sleep(time.Millisecond * 500)
}
continue
}
go s.Handle(conn)
}
+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
+6 -2
View File
@@ -453,9 +453,13 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
for _, s := range tls.CipherSuites() {
id[s.Name] = s.ID
}
for _, s := range tls.InsecureCipherSuites() {
id[s.Name] = s.ID
}
for n := range strings.SplitSeq(c.CipherSuites, ":") {
if id[n] != 0 {
config.CipherSuites = append(config.CipherSuites, id[n])
n = strings.TrimSpace(n)
if v, ok := id[n]; ok {
config.CipherSuites = append(config.CipherSuites, v)
}
}
}