mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-09-16 06:20:28 +00:00
Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9d6ded31b | ||
|
|
89aedc7d04 | ||
|
|
5ca6f4b7d4 | ||
|
|
18e283909c | ||
|
|
6ab123bf8f | ||
|
|
4aba687dd3 | ||
|
|
5b1b41058e | ||
|
|
6e3322d219 | ||
|
|
1d8eb81d70 | ||
|
|
e78d8ef184 | ||
|
|
6ce924ad56 | ||
|
|
035d438979 | ||
|
|
50231eaff9 | ||
|
|
1f74c480d6 | ||
|
|
af7eb68028 | ||
|
|
35387572e0 | ||
|
|
8f15190c23 | ||
|
|
64fada32b5 | ||
|
|
0bafca9486 | ||
|
|
d5bc58dc6b | ||
|
|
c18b39ed80 | ||
|
|
e2ad0acf60 | ||
|
|
c320e89108 | ||
|
|
412898fed7 | ||
|
|
5c62d50d43 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) })
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
//go:build darwin
|
||||
//go:build darwin && !ios
|
||||
|
||||
package net
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build darwin
|
||||
//go:build darwin && !ios
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build darwin
|
||||
//go:build darwin && !ios
|
||||
|
||||
package net
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build ios
|
||||
|
||||
package net
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (int, string, string, error) {
|
||||
return 0, "", "", errors.New("process lookup is not supported on this platform")
|
||||
}
|
||||
@@ -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...)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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 (
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user