Compare commits

..
Author SHA1 Message Date
FanglidingandJoshua Su 22df426d47 Stat: Fix GetOrRegister race
Co-Authored-By: Joshua Su <i@joshua.su>
2026-07-11 01:11:51 +08:00
127 changed files with 806 additions and 7833 deletions
-1
View File
@@ -37,7 +37,6 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json
RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json
RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
# Create log files
-1
View File
@@ -37,7 +37,6 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json
RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json
RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
# Create log files
+1 -20
View File
@@ -64,14 +64,6 @@ 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
@@ -82,7 +74,7 @@ jobs:
uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v4.6.0
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -132,13 +124,6 @@ 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 }}
@@ -146,7 +131,3 @@ jobs:
if [[ "${{ env.LATEST }}" == "true" ]]; then
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:latest
fi
if [[ "${{ env.NEWEST }}" == "true" ]]; then
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:pre-release
fi
+1 -1
View File
@@ -92,7 +92,7 @@ jobs:
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
- name: Set up Go
uses: actions/setup-go@v7
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
+1 -1
View File
@@ -193,7 +193,7 @@ jobs:
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
- name: Set up Go
uses: actions/setup-go@v7
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
+2 -2
View File
@@ -61,7 +61,7 @@ jobs:
- name: Checkout codebase
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v7
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
@@ -85,7 +85,7 @@ jobs:
- name: Checkout codebase
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v7
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
-6
View File
@@ -78,12 +78,6 @@ func (o *Observer) background() {
sleepTime = time.Duration(o.config.ProbeInterval)
}
if len(outbounds) == 0 {
errors.LogWarning(o.ctx, "no outbound matches subjectSelector ", o.config.SubjectSelector)
time.Sleep(sleepTime)
continue
}
if !o.config.EnableConcurrency {
sort.Strings(outbounds)
for _, v := range outbounds {
+1
View File
@@ -347,6 +347,7 @@ func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound)
// case addr.Family().IsDomain():
default:
ob.Gateway = addr
}
}
+3 -3
View File
@@ -136,7 +136,7 @@ func (b *Balancer) SelectOutbounds() ([]string, error) {
// GetPrincipleTarget implements routing.BalancerPrincipleTarget
func (r *Router) GetPrincipleTarget(tag string) ([]string, error) {
if b, ok := (*r.balancers.Load())[tag]; ok {
if b, ok := r.balancers[tag]; ok {
if s, ok := b.strategy.(BalancingPrincipleTarget); ok {
candidates, err := b.SelectOutbounds()
if err != nil {
@@ -151,7 +151,7 @@ func (r *Router) GetPrincipleTarget(tag string) ([]string, error) {
// SetOverrideTarget implements routing.BalancerOverrider
func (r *Router) SetOverrideTarget(tag, target string) error {
if b, ok := (*r.balancers.Load())[tag]; ok {
if b, ok := r.balancers[tag]; ok {
b.override.Put(target)
return nil
}
@@ -160,7 +160,7 @@ func (r *Router) SetOverrideTarget(tag, target string) error {
// GetOverrideTarget implements routing.BalancerOverrider
func (r *Router) GetOverrideTarget(tag string) (string, error) {
if b, ok := (*r.balancers.Load())[tag]; ok {
if b, ok := r.balancers[tag]; ok {
return b.override.Get(), nil
}
return "", errors.New("cannot find tag")
+17
View File
@@ -2,8 +2,25 @@ package router
import (
sync "sync"
"github.com/xtls/xray-core/common/errors"
)
func (r *Router) OverrideBalancer(balancer string, target string) error {
var b *Balancer
for tag, bl := range r.balancers {
if tag == balancer {
b = bl
break
}
}
if b == nil {
return errors.New("balancer '", balancer, "' not found")
}
b.override.Put(target)
return nil
}
type overrideSettings struct {
target string
}
-20
View File
@@ -5,7 +5,6 @@ import (
"os"
"path/filepath"
"regexp"
"runtime"
"slices"
"strings"
@@ -394,22 +393,3 @@ func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool {
}
return false
}
// LocalOSMatcher matches the operating system Xray itself is running on. That never
// changes while Xray is running, so the result is resolved when the rule is built.
type LocalOSMatcher struct {
matched bool
}
func NewLocalOSMatcher(names []string) *LocalOSMatcher {
return &LocalOSMatcher{
matched: slices.ContainsFunc(names, func(name string) bool {
return strings.EqualFold(name, runtime.GOOS)
}),
}
}
// Apply implements Condition.
func (m *LocalOSMatcher) Apply(_ routing.Context) bool {
return m.matched
}
-27
View File
@@ -2,9 +2,7 @@ package router_test
import (
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
. "github.com/xtls/xray-core/app/router"
@@ -345,31 +343,6 @@ func TestChinaSites(t *testing.T) {
}
}
func TestLocalOSRule(t *testing.T) {
otherOS := "plan9"
if runtime.GOOS == otherOS {
otherOS = "linux"
}
cases := []struct {
localOS []string
output bool
}{
{localOS: []string{runtime.GOOS}, output: true},
{localOS: []string{otherOS}, output: false},
{localOS: []string{otherOS, runtime.GOOS}, output: true},
{localOS: []string{strings.ToUpper(runtime.GOOS)}, output: true},
}
for _, test := range cases {
cond, err := (&RoutingRule{LocalOs: test.localOS}).BuildCondition()
common.Must(err)
if got := cond.Apply(withBackground()); got != test.output {
t.Errorf("for localOS %v on %s: expected %v, got %v", test.localOS, runtime.GOOS, test.output, got)
}
}
}
func BenchmarkMphDomainMatcher(b *testing.B) {
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
rules, err := geodata.ParseDomainRules([]string{"geosite:cn"}, geodata.Domain_Substr)
-4
View File
@@ -33,10 +33,6 @@ func (r *Rule) Apply(ctx routing.Context) bool {
func (rr *RoutingRule) BuildCondition() (Condition, error) {
conds := NewConditionChan()
if len(rr.LocalOs) > 0 {
conds.Add(NewLocalOSMatcher(rr.LocalOs))
}
if len(rr.InboundTag) > 0 {
conds.Add(NewInboundTagMatcher(rr.InboundTag))
}
+4 -14
View File
@@ -107,10 +107,8 @@ type RoutingRule struct {
VlessRouteList *net.PortList `protobuf:"bytes,20,opt,name=vless_route_list,json=vlessRouteList,proto3" json:"vless_route_list,omitempty"`
Process []string `protobuf:"bytes,21,rep,name=process,proto3" json:"process,omitempty"`
Webhook *WebhookConfig `protobuf:"bytes,22,opt,name=webhook,proto3" json:"webhook,omitempty"`
// List of operating systems for matching the one Xray itself is running on.
LocalOs []string `protobuf:"bytes,23,rep,name=local_os,json=localOs,proto3" json:"local_os,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RoutingRule) Reset() {
@@ -280,13 +278,6 @@ func (x *RoutingRule) GetWebhook() *WebhookConfig {
return nil
}
func (x *RoutingRule) GetLocalOs() []string {
if x != nil {
return x.LocalOs
}
return nil
}
type isRoutingRule_TargetTag interface {
isRoutingRule_TargetTag()
}
@@ -646,7 +637,7 @@ var File_app_router_config_proto protoreflect.FileDescriptor
const file_app_router_config_proto_rawDesc = "" +
"\n" +
"\x17app/router/config.proto\x12\x0fxray.app.router\x1a!common/serial/typed_message.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\x1a\x1bcommon/geodata/geodat.proto\"\xdc\a\n" +
"\x17app/router/config.proto\x12\x0fxray.app.router\x1a!common/serial/typed_message.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\x1a\x1bcommon/geodata/geodat.proto\"\xc1\a\n" +
"\vRoutingRule\x12\x12\n" +
"\x03tag\x18\x01 \x01(\tH\x00R\x03tag\x12%\n" +
"\rbalancing_tag\x18\f \x01(\tH\x00R\fbalancingTag\x12\x19\n" +
@@ -670,8 +661,7 @@ const file_app_router_config_proto_rawDesc = "" +
"\x0flocal_port_list\x18\x12 \x01(\v2\x19.xray.common.net.PortListR\rlocalPortList\x12C\n" +
"\x10vless_route_list\x18\x14 \x01(\v2\x19.xray.common.net.PortListR\x0evlessRouteList\x12\x18\n" +
"\aprocess\x18\x15 \x03(\tR\aprocess\x128\n" +
"\awebhook\x18\x16 \x01(\v2\x1e.xray.app.router.WebhookConfigR\awebhook\x12\x19\n" +
"\blocal_os\x18\x17 \x03(\tR\alocalOs\x1a=\n" +
"\awebhook\x18\x16 \x01(\v2\x1e.xray.app.router.WebhookConfigR\awebhook\x1a=\n" +
"\x0fAttributesEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\f\n" +
-3
View File
@@ -56,9 +56,6 @@ message RoutingRule {
repeated string process = 21;
WebhookConfig webhook = 22;
// List of operating systems for matching the one Xray itself is running on.
repeated string local_os = 23;
}
message WebhookConfig {
+114 -59
View File
@@ -2,9 +2,7 @@ package router
import (
"context"
"maps"
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
@@ -19,8 +17,8 @@ import (
// Router is an implementation of routing.Router.
type Router struct {
domainStrategy Config_DomainStrategy
rules atomic.Pointer[[]*Rule]
balancers atomic.Pointer[map[string]*Balancer]
rules []*Rule
balancers map[string]*Balancer
dns dns.Client
ctx context.Context
@@ -45,9 +43,52 @@ func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm out
r.ohm = ohm
r.dispatcher = dispatcher
r.rules.Store(new([]*Rule))
r.balancers.Store(&map[string]*Balancer{})
return r.ReloadRules(config, false)
r.balancers = make(map[string]*Balancer, len(config.BalancingRule))
for _, rule := range config.BalancingRule {
balancer, err := rule.Build(ohm, dispatcher)
if err != nil {
return err
}
balancer.InjectContext(ctx)
r.balancers[rule.Tag] = balancer
}
r.rules = make([]*Rule, 0, len(config.Rule))
for _, rule := range config.Rule {
cond, err := rule.BuildCondition()
if err != nil {
r.closeWebhooks()
return err
}
rr := &Rule{
Condition: cond,
Tag: rule.GetTag(),
RuleTag: rule.GetRuleTag(),
}
if wh := rule.GetWebhook(); wh != nil {
notifier, err := NewWebhookNotifier(wh)
if err != nil {
r.closeWebhooks()
return err
}
rr.Webhook = notifier
}
btag := rule.GetBalancingTag()
if len(btag) > 0 {
brule, found := r.balancers[btag]
if !found {
if rr.Webhook != nil {
rr.Webhook.Close()
}
r.closeWebhooks()
return errors.New("balancer ", btag, " not found")
}
rr.Balancer = brule
}
r.rules = append(r.rules, rr)
}
return nil
}
// PickRoute implements routing.Router.
@@ -83,22 +124,18 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
r.mu.Lock()
defer r.mu.Unlock()
oldRules := *r.rules.Load()
oldBalancers := *r.balancers.Load()
var newRules []*Rule
newBalancers := make(map[string]*Balancer)
existTags := make(map[string]bool, len(oldRules)+len(config.Rule))
if shouldAppend {
newRules = append(newRules, oldRules...)
maps.Copy(newBalancers, oldBalancers)
for _, rule := range oldRules {
existTags[rule.RuleTag] = true
if !shouldAppend {
for _, rule := range r.rules {
if rule.Webhook != nil {
rule.Webhook.Close()
}
}
r.balancers = make(map[string]*Balancer, len(config.BalancingRule))
r.rules = make([]*Rule, 0, len(config.Rule))
}
for _, rule := range config.BalancingRule {
if _, found := newBalancers[rule.Tag]; found {
_, found := r.balancers[rule.Tag]
if found {
return errors.New("duplicate balancer tag")
}
balancer, err := rule.Build(r.ohm, r.dispatcher)
@@ -106,12 +143,27 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
return err
}
balancer.InjectContext(r.ctx)
newBalancers[rule.Tag] = balancer
r.balancers[rule.Tag] = balancer
}
startIdx := len(r.rules)
closeNewWebhooks := func() {
for i := startIdx; i < len(r.rules); i++ {
if r.rules[i].Webhook != nil {
r.rules[i].Webhook.Close()
}
}
r.rules = r.rules[:startIdx]
}
for _, rule := range config.Rule {
if r.RuleExists(rule.GetRuleTag()) {
closeNewWebhooks()
return errors.New("duplicate ruleTag ", rule.GetRuleTag())
}
cond, err := rule.BuildCondition()
if err != nil {
closeNewWebhooks()
return err
}
rr := &Rule{
@@ -119,64 +171,69 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
Tag: rule.GetTag(),
RuleTag: rule.GetRuleTag(),
}
if rr.RuleTag != "" && existTags[rr.RuleTag] {
return errors.New("duplicate ruleTag ", rr.RuleTag)
}
existTags[rr.RuleTag] = true
if wh := rule.GetWebhook(); wh != nil {
notifier, err := NewWebhookNotifier(wh)
if err != nil {
closeNewWebhooks()
return err
}
rr.Webhook = notifier
}
if btag := rule.GetBalancingTag(); len(btag) > 0 {
brule, found := newBalancers[btag]
btag := rule.GetBalancingTag()
if len(btag) > 0 {
brule, found := r.balancers[btag]
if !found {
if rr.Webhook != nil {
rr.Webhook.Close()
}
closeNewWebhooks()
return errors.New("balancer ", btag, " not found")
}
rr.Balancer = brule
}
newRules = append(newRules, rr)
r.rules = append(r.rules, rr)
}
r.balancers.Store(&newBalancers)
r.rules.Store(&newRules)
if !shouldAppend {
closeWebhooks(oldRules)
}
return nil
}
func (r *Router) RuleExists(tag string) bool {
if tag != "" {
for _, rule := range r.rules {
if rule.RuleTag == tag {
return true
}
}
}
return false
}
// RemoveRule implements routing.Router.
func (r *Router) RemoveRule(tag string) error {
if tag == "" {
return errors.New("empty tag name!")
}
r.mu.Lock()
defer r.mu.Unlock()
oldRules := *r.rules.Load()
newRules := make([]*Rule, 0, len(oldRules))
var removed []*Rule
for _, rule := range oldRules {
if rule.RuleTag != tag {
newRules = append(newRules, rule)
} else {
removed = append(removed, rule)
newRules := []*Rule{}
if tag != "" {
for _, rule := range r.rules {
if rule.RuleTag != tag {
newRules = append(newRules, rule)
} else if rule.Webhook != nil {
rule.Webhook.Close()
}
}
r.rules = newRules
return nil
}
r.rules.Store(&newRules)
closeWebhooks(removed)
return nil
return errors.New("empty tag name!")
}
// ListRule implements routing.Router
func (r *Router) ListRule() []routing.Route {
rules := *r.rules.Load()
ruleList := make([]routing.Route, 0, len(rules))
for _, rule := range rules {
r.mu.Lock()
defer r.mu.Unlock()
ruleList := make([]routing.Route, 0)
for _, rule := range r.rules {
ruleList = append(ruleList, &Route{
outboundTag: rule.Tag,
ruleTag: rule.RuleTag,
@@ -195,9 +252,7 @@ func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context,
ctx = routing_dns.ContextWithDNSClient(ctx, r.dns)
}
rules := *r.rules.Load()
for _, rule := range rules {
for _, rule := range r.rules {
if rule.Apply(ctx) {
return rule, ctx, nil
}
@@ -210,7 +265,7 @@ func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context,
ctx = routing_dns.ContextWithDNSClient(ctx, r.dns)
// Try applying rules again if we have IPs.
for _, rule := range rules {
for _, rule := range r.rules {
if rule.Apply(ctx) {
return rule, ctx, nil
}
@@ -224,9 +279,9 @@ func (r *Router) Start() error {
return nil
}
// closeWebhooks closes all webhook notifiers in the given rule set.
func closeWebhooks(rules []*Rule) {
for _, rule := range rules {
// closeWebhooks closes all webhook notifiers in the current rule set.
func (r *Router) closeWebhooks() {
for _, rule := range r.rules {
if rule.Webhook != nil {
rule.Webhook.Close()
}
@@ -237,7 +292,7 @@ func closeWebhooks(rules []*Rule) {
func (r *Router) Close() error {
r.mu.Lock()
defer r.mu.Unlock()
closeWebhooks(*r.rules.Load())
r.closeWebhooks()
return nil
}
+23 -17
View File
@@ -8,7 +8,6 @@ import (
"net"
"net/http"
"sync"
"sync/atomic"
"time"
"github.com/xtls/xray-core/common/errors"
@@ -41,7 +40,6 @@ type WebhookNotifier struct {
deduplication uint32
client *http.Client
seen sync.Map
lastSweep atomic.Int64
done chan struct{}
wg sync.WaitGroup
closeOnce sync.Once
@@ -79,6 +77,11 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) {
}
}
if h.deduplication > 0 {
h.wg.Add(1)
go h.cleanupLoop()
}
return h, nil
}
@@ -198,7 +201,6 @@ func (h *WebhookNotifier) isDuplicate(email string) bool {
}
ttl := time.Duration(h.deduplication) * time.Second
now := time.Now()
h.maybeSweep(now, ttl)
if v, loaded := h.seen.LoadOrStore(email, now); loaded {
if now.Sub(v.(time.Time)) < ttl {
return true
@@ -208,23 +210,27 @@ func (h *WebhookNotifier) isDuplicate(email string) bool {
return false
}
func (h *WebhookNotifier) maybeSweep(now time.Time, ttl time.Duration) {
last := h.lastSweep.Load()
if now.UnixNano()-last < int64(ttl) {
return
}
if !h.lastSweep.CompareAndSwap(last, now.UnixNano()) {
return // another goroutine did the sweep
}
h.seen.Range(func(key, value any) bool {
if now.Sub(value.(time.Time)) >= ttl {
h.seen.Delete(key)
func (h *WebhookNotifier) cleanupLoop() {
defer h.wg.Done()
ttl := time.Duration(h.deduplication) * time.Second
ticker := time.NewTicker(ttl)
defer ticker.Stop()
for {
select {
case <-h.done:
return
case <-ticker.C:
now := time.Now()
h.seen.Range(func(key, value any) bool {
if now.Sub(value.(time.Time)) >= ttl {
h.seen.Delete(key)
}
return true
})
}
return true
})
}
}
// Only need to call if the Notifier is really used, otherwise GC can clean it
func (h *WebhookNotifier) Close() error {
h.closeOnce.Do(func() {
close(h.done)
+1 -1
View File
@@ -122,7 +122,7 @@ func NewReader(reader io.Reader) Reader {
}
_, isFile := reader.(*os.File)
if !isFile && useReadV() {
if !isFile && useReadv {
if sc, ok := reader.(syscall.Conn); ok {
rawConn, err := sc.SyscallConn()
if err != nil {
+7 -19
View File
@@ -5,7 +5,6 @@ package buf
import (
"io"
"sync/atomic"
"syscall"
"github.com/xtls/xray-core/common/platform"
@@ -144,24 +143,13 @@ func (r *ReadVReader) ReadMultiBuffer() (MultiBuffer, error) {
return mb, nil
}
var useReadv atomic.Bool
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":
enabled = true
}
useReadv.Store(enabled)
return nil
}
var useReadv bool
func init() {
platform.RegisterEnvReload(reloadEnvSettings)
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
value := platform.NewEnvFlag(platform.UseReadV).GetValue(func() string { return defaultFlagValue })
switch value {
case defaultFlagValue, "auto", "enable":
useReadv = true
}
}
+1 -3
View File
@@ -10,9 +10,7 @@ import (
"github.com/xtls/xray-core/features/stats"
)
func useReadV() bool {
return false
}
const useReadv = false
func NewReadVReader(reader io.Reader, rawConn syscall.RawConn, counter stats.Counter) Reader {
panic("not implemented")
+2 -4
View File
@@ -1,4 +1,4 @@
//go:build darwin && !ios
//go:build darwin
package net
@@ -198,9 +198,7 @@ func darwinSocketInfoMatchLevel(info []byte, network string, srcAddr netip.Addr,
vflag := info[darwinInSockInfoVFlagOff]
if srcAddr.Is4() {
// Dual-stack sockets expose IPv4-mapped connections as AF_INET6
// while marking the endpoint as IPv4 in ini_vflag.
if (family != unix.AF_INET && family != unix.AF_INET6) || vflag&darwinInSockInfoIPv4 == 0 {
if family != unix.AF_INET || vflag&darwinInSockInfoIPv4 == 0 {
return darwinSocketNoMatch
}
} else {
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build darwin && !ios
//go:build darwin
#include "textflag.h"
+1 -64
View File
@@ -1,4 +1,4 @@
//go:build darwin && !ios
//go:build darwin
package net
@@ -52,57 +52,6 @@ func TestFindProcessDarwinTCP(t *testing.T) {
assertCurrentProcess(t, pid, name, path)
}
func TestFindProcessDarwinTCPIPv4Mapped(t *testing.T) {
listener, err := stdnet.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
accepted := make(chan stdnet.Conn, 1)
go func() {
conn, err := listener.Accept()
if err == nil {
accepted <- conn
return
}
close(accepted)
}()
listenerAddr := listener.Addr().(*stdnet.TCPAddr)
fd, err := unix.Socket(unix.AF_INET6, unix.SOCK_STREAM, unix.IPPROTO_TCP)
if err != nil {
t.Fatal(err)
}
defer unix.Close(fd)
mappedAddr := [16]byte{10: 0xff, 11: 0xff, 12: 127, 15: 1}
if err := unix.Connect(fd, &unix.SockaddrInet6{
Port: listenerAddr.Port,
Addr: mappedAddr,
}); err != nil {
t.Fatal(err)
}
serverConn := <-accepted
if serverConn == nil {
t.Fatal("server did not accept tcp connection")
}
defer serverConn.Close()
local, err := unix.Getsockname(fd)
if err != nil {
t.Fatal(err)
}
localPort := local.(*unix.SockaddrInet6).Port
pid, name, path, err := FindProcess("tcp", "127.0.0.1", uint16(localPort), "127.0.0.1", uint16(listenerAddr.Port))
if err != nil {
t.Fatal(err)
}
assertCurrentProcess(t, pid, name, path)
}
func TestFindProcessDarwinUDP(t *testing.T) {
conn, err := stdnet.ListenUDP("udp", &stdnet.UDPAddr{IP: stdnet.ParseIP("127.0.0.1")})
if err != nil {
@@ -315,18 +264,6 @@ func TestDarwinSocketInfoMatchLevelFallbacks(t *testing.T) {
}
}
func TestDarwinSocketInfoMatchLevelIPv4Mapped(t *testing.T) {
src := netip.MustParseAddr("127.0.0.1")
dst := netip.MustParseAddr("203.0.113.10")
info := newDarwinSocketInfo("tcp", src, 12345, dst, 443)
writeDarwinNativeUint32(info, darwinSocketInfoFamilyOff, uint32(unix.AF_INET6))
level := darwinSocketInfoMatchLevel(info, "tcp", src, 12345, dst, 443, true)
if level != darwinSocketExactMatch {
t.Fatalf("unexpected match level: got %d, want %d", level, darwinSocketExactMatch)
}
}
func newDarwinSocketInfo(network string, local netip.Addr, localPort uint16, remote netip.Addr, remotePort uint16) []byte {
info := make([]byte, darwinSocketFDInfoSize)
switch network {
-11
View File
@@ -1,11 +0,0 @@
//go:build ios
package net
import (
"github.com/xtls/xray-core/common/errors"
)
func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (int, string, string, error) {
return 0, "", "", errors.New("process lookup is not supported on this platform")
}
-40
View File
@@ -1,40 +0,0 @@
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...)
}
+34 -25
View File
@@ -3,8 +3,11 @@ package bittorrent
import (
"encoding/binary"
"errors"
"math"
"time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
)
type SniffHeader struct{}
@@ -36,44 +39,50 @@ func SniffUTP(b []byte) (*SniffHeader, error) {
return nil, common.ErrNoClue
}
// type 4 (ST_SYN), version 1
if b[0] != 0x41 {
buffer := buf.FromBytes(b)
var typeAndVersion uint8
if binary.Read(buffer, binary.BigEndian, &typeAndVersion) != nil {
return nil, common.ErrNoClue
} else if b[0]>>4&0xF > 4 || b[0]&0xF != 1 {
return nil, errNotBittorrent
}
// timestamp_difference is always 0 in new connections
if binary.BigEndian.Uint32(b[8:12]) != 0 {
var extension uint8
if binary.Read(buffer, binary.BigEndian, &extension) != nil {
return nil, common.ErrNoClue
} else if extension != 0 && extension != 1 {
return nil, errNotBittorrent
}
// Walk the extension chain. Selective ack (1) and extension bits (2)
extension, offset := b[1], 20
for extension != 0 {
if len(b) < offset+2 {
if extension != 1 {
return nil, errNotBittorrent
}
length := int(b[offset+1])
switch extension {
case 1: // selective ack
if length < 4 || length%4 != 0 {
return nil, errNotBittorrent
}
case 2: // extension bits: fixed 8 bytes, sent in ST_SYN by µTorrent
if length != 8 {
return nil, errNotBittorrent
}
default:
return nil, errNotBittorrent
if binary.Read(buffer, binary.BigEndian, &extension) != nil {
return nil, common.ErrNoClue
}
if len(b) < offset+2+length {
return nil, errNotBittorrent
var length uint8
if err := binary.Read(buffer, binary.BigEndian, &length); err != nil {
return nil, common.ErrNoClue
}
if common.Error2(buffer.ReadBytes(int32(length))) != nil {
return nil, common.ErrNoClue
}
extension = b[offset]
offset += 2 + length
}
// extensions should consume all ST_SYN payload
if len(b) != offset {
if common.Error2(buffer.ReadBytes(2)) != nil {
return nil, common.ErrNoClue
}
var timestamp uint32
if err := binary.Read(buffer, binary.BigEndian, &timestamp); err != nil {
return nil, common.ErrNoClue
}
if math.Abs(float64(time.Now().UnixMicro()-int64(timestamp))) > float64(24*time.Hour) {
return nil, errNotBittorrent
}
@@ -1,67 +0,0 @@
package bittorrent
import (
"encoding/binary"
"testing"
"github.com/xtls/xray-core/common"
)
// utpPacket builds the fixed 20-byte header defined by BEP 29.
func utpPacket(packetType, extension byte, tsDiff uint32, payload ...byte) []byte {
b := make([]byte, 20)
b[0] = packetType<<4 | 1
b[1] = extension
binary.BigEndian.PutUint16(b[2:4], 0x4a3f) // connection_id, random
binary.BigEndian.PutUint32(b[4:8], 0x8c3a91d2) // timestamp_microseconds, sender's clock
binary.BigEndian.PutUint32(b[8:12], tsDiff)
binary.BigEndian.PutUint32(b[12:16], 0x00100000) // wnd_size
binary.BigEndian.PutUint16(b[16:18], 0x71ee) // seq_nr, random in libutp/libtorrent
binary.BigEndian.PutUint16(b[18:20], 0x0000) // ack_nr
return append(b, payload...)
}
func TestSniffUTP(t *testing.T) {
selectiveAck := []byte{0, 4, 0xff, 0x00, 0xff, 0x00}
wrongVersion := utpPacket(4, 0, 0)
wrongVersion[0] = 4<<4 | 2
cases := []struct {
name string
payload []byte
err error
}{
{"syn", utpPacket(4, 0, 0), nil},
{"syn with selective ack", append(utpPacket(4, 1, 0), selectiveAck...), nil},
{"syn with extension bits", append(utpPacket(4, 2, 0), 0, 8, 1, 2, 3, 4, 5, 6, 7, 8), nil},
{"extension bits with wrong length", append(utpPacket(4, 2, 0), 0, 4, 1, 2, 3, 4), errNotBittorrent},
{"syn with nonzero timestamp_difference", utpPacket(4, 0, 0x1234), errNotBittorrent},
{"syn with trailing payload", utpPacket(4, 0, 0, 'x'), errNotBittorrent},
// txid 0x4100, no EDNS0: the worst case colliding with the uTP header
{"dns query", []byte{
0x41, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 'a', 0x02, 'c', 'o', 0x00, 0x00, 0x01, 0x00, 0x01,
}, errNotBittorrent},
{"established connection packets", utpPacket(0, 0, 0x5678, 'x', 'y', 'z'), errNotBittorrent},
{"state", utpPacket(2, 0, 0x5678), errNotBittorrent},
{"fin", utpPacket(1, 0, 0x5678), errNotBittorrent},
{"wrong version", wrongVersion, errNotBittorrent},
{"unknown packet type", utpPacket(5, 0, 0), errNotBittorrent},
{"unknown extension", utpPacket(4, 2, 0), errNotBittorrent},
{"extension chain past the datagram", utpPacket(4, 1, 0, 0, 8, 0xff), errNotBittorrent},
{"selective ack not in multiples of 4", append(utpPacket(4, 1, 0), 0, 3, 0xff, 0x00, 0xff), errNotBittorrent},
{"shorter than the header", utpPacket(4, 0, 0)[:19], common.ErrNoClue},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
h, err := SniffUTP(c.payload)
if err != c.err {
t.Fatalf("expected error %v, got %v", c.err, err)
}
if err == nil && h == nil {
t.Fatal("expected a sniff header, got nil")
}
})
}
}
+64 -55
View File
@@ -1,16 +1,13 @@
package http
import (
"bufio"
"bytes"
"context"
"errors"
"io"
"net/http"
"strings"
"unsafe"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session"
)
@@ -42,67 +39,79 @@ func (h *SniffHeader) Domain() string {
}
var (
validMethods = map[string]bool{}
errNotHTTP = errors.New("not an HTTP request")
methods = [...]string{"get", "post", "head", "put", "delete", "options", "connect"}
errNotHTTPMethod = errors.New("not an HTTP method")
)
func init() {
// https://www.iana.org/assignments/http-methods
methods := []string{
"ACL", "BASELINE-CONTROL", "BIND", "CHECKIN", "CHECKOUT",
"CONNECT", "COPY", "DELETE", "GET", "HEAD",
"LABEL", "LINK", "LOCK", "MERGE", "MKACTIVITY",
"MKCALENDAR", "MKCOL", "MKREDIRECTREF", "MKWORKSPACE", "MOVE",
"OPTIONS", "ORDERPATCH", "PATCH", "POST", "PRI",
"PROPFIND", "PROPPATCH", "PUT", "QUERY", "REBIND",
"REPORT", "SEARCH", "TRACE", "UNBIND", "UNCHECKOUT",
"UNLINK", "UNLOCK", "UPDATE", "UPDATEREDIRECTREF", "VERSION-CONTROL",
}
for _, m := range methods {
validMethods[m] = true
}
}
func beginWithHTTPMethod(b []byte) error {
for _, m := range &methods {
if len(b) >= len(m) && strings.EqualFold(string(b[:len(m)]), m) {
return nil
}
func isValidHTTPMethod(b []byte) bool {
if len(b) == 0 {
return false
if len(b) < len(m) {
return common.ErrNoClue
}
}
idx := bytes.IndexByte(b, ' ')
if idx == -1 {
return false
}
method := unsafe.String(unsafe.SliceData(b), idx)
return validMethods[method]
return errNotHTTPMethod
}
func SniffHTTP(b []byte, c context.Context) (*SniffHeader, error) {
if !isValidHTTPMethod(b) {
return nil, errNotHTTP
}
content := session.ContentFromContext(c)
r, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(b)))
if err != nil {
if err == io.ErrUnexpectedEOF {
return nil, common.ErrNoClue
}
return nil, errNotHTTP
}
if r.Host == "" {
return nil, common.ErrNoClue
}
sh := &SniffHeader{
version: HTTP1,
host: r.Host,
}
ShouldSniffAttr := true
// If content.Attributes have information, that means it comes from HTTP inbound PlainHTTP mode.
// It will set attributes, so skip it.
if content != nil && len(content.Attributes) == 0 {
for key, h := range r.Header {
content.Attributes[key] = strings.Join(h, ",")
}
content.Attributes[":method"] = r.Method
content.Attributes[":path"] = r.URL.Path
if content == nil || len(content.Attributes) != 0 {
ShouldSniffAttr = false
}
if err := beginWithHTTPMethod(b); err != nil {
return nil, err
}
return sh, nil
sh := &SniffHeader{
version: HTTP1,
}
headers := bytes.Split(b, []byte{'\n'})
for i := 1; i < len(headers); i++ {
header := headers[i]
if len(header) == 0 {
break
}
parts := bytes.SplitN(header, []byte{':'}, 2)
if len(parts) != 2 {
continue
}
key := strings.ToLower(string(parts[0]))
value := string(bytes.TrimSpace(parts[1]))
if ShouldSniffAttr {
content.SetAttribute(key, value) // Put header in attribute
}
if key == "host" {
rawHost := strings.ToLower(value)
dest, err := ParseHost(rawHost, net.Port(80))
if err != nil {
return nil, err
}
sh.host = dest.Address.String()
}
}
// Parse request line
// Request line is like this
// "GET /homo/114514 HTTP/1.1"
if len(headers) > 0 && ShouldSniffAttr {
RequestLineParts := bytes.Split(headers[0], []byte{' '})
if len(RequestLineParts) == 3 {
content.SetAttribute(":method", string(RequestLineParts[0]))
content.SetAttribute(":path", string(RequestLineParts[1]))
}
}
if len(sh.host) > 0 {
return sh, nil
}
return nil, common.ErrNoClue
}
+55 -57
View File
@@ -14,76 +14,75 @@ func TestHTTPHeaders(t *testing.T) {
err bool
}{
{
input: "GET /tutorials/other/top-20-mysql-best-practices/ HTTP/1.1\r\n" +
"Host: net.tutsplus.com\r\n" +
"User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729)\r\n" +
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
"Accept-Language: en-us,en;q=0.5\r\n" +
"Accept-Encoding: gzip,deflate\r\n" +
"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
"Keep-Alive: 300\r\n" +
"Connection: keep-alive\r\n" +
"Cookie: PHPSESSID=r2t5uvjq435r4q7ib3vtdjq120\r\n" +
"Pragma: no-cache\r\n" +
"Cache-Control: no-cache\r\n" +
"\r\n",
input: `GET /tutorials/other/top-20-mysql-best-practices/ HTTP/1.1
Host: net.tutsplus.com
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Cookie: PHPSESSID=r2t5uvjq435r4q7ib3vtdjq120
Pragma: no-cache
Cache-Control: no-cache`,
domain: "net.tutsplus.com",
},
{
input: "POST /foo.php HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729)\r\n" +
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
"Accept-Language: en-us,en;q=0.5\r\n" +
"Accept-Encoding: gzip,deflate\r\n" +
"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
"Keep-Alive: 300\r\n" +
"Connection: keep-alive\r\n" +
"Referer: http://localhost/test.php\r\n" +
"Content-Type: application/x-www-form-urlencoded\r\n" +
"Content-Length: 43\r\n" +
"\r\n" +
"first_name=John&last_name=Doe&action=Submit",
input: `POST /foo.php HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer: http://localhost/test.php
Content-Type: application/x-www-form-urlencoded
Content-Length: 43
first_name=John&last_name=Doe&action=Submit`,
domain: "localhost",
},
{
input: "X /foo.php HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729)\r\n" +
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
"Accept-Language: en-us,en;q=0.5\r\n" +
"Accept-Encoding: gzip,deflate\r\n" +
"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
"Keep-Alive: 300\r\n" +
"Connection: keep-alive\r\n" +
"Referer: http://localhost/test.php\r\n" +
"Content-Type: application/x-www-form-urlencoded\r\n" +
"Content-Length: 43\r\n" +
"\r\n" +
"first_name=John&last_name=Doe&action=Submit",
input: `X /foo.php HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer: http://localhost/test.php
Content-Type: application/x-www-form-urlencoded
Content-Length: 43
first_name=John&last_name=Doe&action=Submit`,
domain: "",
err: true,
},
{
input: "GET /foo.php HTTP/1.1\r\n" +
"User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729)\r\n" +
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
"Accept-Language: en-us,en;q=0.5\r\n" +
"Accept-Encoding: gzip,deflate\r\n" +
"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
"Keep-Alive: 300\r\n" +
"Connection: keep-alive\r\n" +
"Referer: http://localhost/test.php\r\n" +
"Content-Type: application/x-www-form-urlencoded\r\n" +
"Content-Length: 43\r\n" +
"\r\n" +
"Host: localhost\r\n" +
"first_name=John&last_name=Doe&action=Submit",
input: `GET /foo.php HTTP/1.1
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer: http://localhost/test.php
Content-Type: application/x-www-form-urlencoded
Content-Length: 43
Host: localhost
first_name=John&last_name=Doe&action=Submit`,
domain: "",
err: true,
},
{
input: "GET /tutorials/other/top-20-mysql-best-practices/ HTTP/1.1\r\n",
input: `GET /tutorials/other/top-20-mysql-best-practices/ HTTP/1.1`,
domain: "",
err: true,
},
@@ -98,7 +97,6 @@ func TestHTTPHeaders(t *testing.T) {
} else {
if err != nil {
t.Errorf("Expect no error but actually %s in test %v", err.Error(), test)
continue
}
if header.Domain() != test.domain {
t.Error("expected domain ", test.domain, " but got ", header.Domain())
+8
View File
@@ -1,10 +1,18 @@
package quic
import (
"crypto"
"crypto/cipher"
_ "crypto/tls"
_ "unsafe"
)
type CipherSuiteTLS13 struct {
ID uint16
KeyLen int
AEAD func(key, fixedNonce []byte) cipher.AEAD
Hash crypto.Hash
}
//go:linkname AEADAESGCMTLS13 crypto/tls.aeadAESGCMTLS13
func AEADAESGCMTLS13(key, nonceMask []byte) cipher.AEAD
+81 -98
View File
@@ -3,6 +3,7 @@ package quic
import (
"crypto"
"crypto/aes"
"crypto/tls"
"encoding/binary"
"io"
@@ -27,43 +28,22 @@ func (s SniffHeader) Domain() string {
return s.domain
}
var (
errNotQUIC = errors.New("not quic")
errNotQUICInitial = errors.New("not initial packet")
const (
versionDraft29 uint32 = 0xff00001d
version1 uint32 = 0x1
)
type quicVersionSpec struct {
ver uint32
typeInitial byte
initialSalt []byte
labelPrefix string
}
var (
quicDraft29 = quicVersionSpec{
ver: 0xff00001d,
typeInitial: 0b00,
initialSalt: []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99},
labelPrefix: "quic",
}
quicV1 = quicVersionSpec{
ver: 0x1,
typeInitial: 0b00,
initialSalt: []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a},
labelPrefix: "quic",
}
quicV2 = quicVersionSpec{
ver: 0x6b3343cf,
typeInitial: 0b01,
initialSalt: []byte{0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9},
labelPrefix: "quicv2",
}
quicVersionSpecMap = map[uint32]*quicVersionSpec{
quicDraft29.ver: &quicDraft29,
quicV1.ver: &quicV1,
quicV2.ver: &quicV2,
quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99}
quicSalt = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a}
initialSuite = &CipherSuiteTLS13{
ID: tls.TLS_AES_128_GCM_SHA256,
KeyLen: 16,
AEAD: AEADAESGCMTLS13,
Hash: crypto.SHA256,
}
errNotQuic = errors.New("not quic")
errNotQuicInitial = errors.New("not initial packet")
)
func SniffQUIC(b []byte) (*SniffHeader, error) {
@@ -83,61 +63,60 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
buffer := buf.FromBytes(b)
typeByte, err := buffer.ReadByte()
if err != nil {
return nil, errNotQUIC
return nil, errNotQuic
}
isLongHeader := typeByte&0x80 > 0
if !isLongHeader || typeByte&0x40 == 0 {
return nil, errNotQUICInitial
return nil, errNotQuicInitial
}
vb, err := buffer.ReadBytes(4)
if err != nil {
return nil, errNotQUIC
return nil, errNotQuic
}
versionNumber := binary.BigEndian.Uint32(vb)
var s *quicVersionSpec
if v, ok := quicVersionSpecMap[versionNumber]; ok {
s = v
} else {
return nil, errNotQUIC
}
var destConnID []byte
if l, err := buffer.ReadByte(); err != nil {
return nil, errNotQUIC
} else if destConnID, err = buffer.ReadBytes(int32(l)); err != nil {
return nil, errNotQUIC
}
if l, err := buffer.ReadByte(); err != nil {
return nil, errNotQUIC
} else if common.Error2(buffer.ReadBytes(int32(l))) != nil {
return nil, errNotQUIC
if versionNumber != 0 && typeByte&0x40 == 0 {
return nil, errNotQuic
} else if versionNumber != versionDraft29 && versionNumber != version1 {
return nil, errNotQuic
}
packetType := (typeByte & 0x30) >> 4
isQUICInitial := packetType == s.typeInitial
isQuicInitial := packetType == 0x0
if isQUICInitial { // Only initial packets have token, see https://datatracker.ietf.org/doc/html/rfc9000#section-17.2.2
tokenLen, err := readShortQUICVarint(buffer)
var destConnID []byte
if l, err := buffer.ReadByte(); err != nil {
return nil, errNotQuic
} else if destConnID, err = buffer.ReadBytes(int32(l)); err != nil {
return nil, errNotQuic
}
if l, err := buffer.ReadByte(); err != nil {
return nil, errNotQuic
} else if common.Error2(buffer.ReadBytes(int32(l))) != nil {
return nil, errNotQuic
}
if isQuicInitial { // Only initial packets have token, see https://datatracker.ietf.org/doc/html/rfc9000#section-17.2.2
tokenLen, err := readShortQuicVarint(buffer)
if err != nil || tokenLen > int32(len(b)) {
return nil, errNotQUIC
return nil, errNotQuic
}
if _, err = buffer.ReadBytes(tokenLen); err != nil {
return nil, errNotQUIC
return nil, errNotQuic
}
}
packetLen, err := readShortQUICVarint(buffer)
packetLen, err := readShortQuicVarint(buffer)
if err != nil {
return nil, errNotQUIC
return nil, errNotQuic
}
// packetLen is impossible to be shorter than this
if packetLen < 4 {
return nil, errNotQUIC
return nil, errNotQuic
}
hdrLen := len(b) - int(buffer.Len())
@@ -146,23 +125,25 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
}
restPayload := b[hdrLen+int(packetLen):]
if !isQUICInitial { // Skip this packet if it's not initial packet
if !isQuicInitial { // Skip this packet if it's not initial packet
b = restPayload
continue
}
salt := s.initialSalt
label := s.labelPrefix
var salt []byte
if versionNumber == version1 {
salt = quicSalt
} else {
salt = quicSaltOld
}
initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt)
secret := hkdfExpandLabel(initialSecret, "client in", crypto.SHA256.Size())
hpKey := hkdfExpandLabel(secret, label+" hp", 16)
secret := hkdfExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size())
hpKey := hkdfExpandLabel(initialSuite.Hash, secret, []byte{}, "quic hp", initialSuite.KeyLen)
block, err := aes.NewCipher(hpKey)
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)])
@@ -172,8 +153,8 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
b[hdrLen+i] ^= mask[i+1]
}
key := hkdfExpandLabel(secret, label+" key", 16)
iv := hkdfExpandLabel(secret, label+" iv", 12)
key := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16)
iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12)
cipher := AEADAESGCMTLS13(key, iv)
nonce := cache.Extend(int32(cipher.NonceSize()))
@@ -198,44 +179,44 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
case 0x00: // PADDING frame
case 0x01: // PING frame
case 0x02, 0x03: // ACK frame
if _, err = readShortQUICVarint(buffer); err != nil { // Field: Largest Acknowledged
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Largest Acknowledged
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Delay
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Delay
return nil, io.ErrUnexpectedEOF
}
ackRangeCount, err := readShortQUICVarint(buffer) // Field: ACK Range Count
ackRangeCount, err := readShortQuicVarint(buffer) // Field: ACK Range Count
if err != nil {
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQUICVarint(buffer); err != nil { // Field: First ACK Range
if _, err = readShortQuicVarint(buffer); err != nil { // Field: First ACK Range
return nil, io.ErrUnexpectedEOF
}
for i := 0; i < int(ackRangeCount); i++ { // Field: ACK Range
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Range -> Gap
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Range -> Gap
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Range -> ACK Range Length
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Range -> ACK Range Length
return nil, io.ErrUnexpectedEOF
}
}
if frameType == 0x03 {
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ECN Counts -> ECT0 Count
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ECN Counts -> ECT0 Count
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ECN Counts -> ECT1 Count
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ECN Counts -> ECT1 Count
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQUICVarint(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count
if _, err = readShortQuicVarint(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count
return nil, io.ErrUnexpectedEOF
}
}
case 0x06: // CRYPTO frame, we will use this frame
offset, err := readShortQUICVarint(buffer) // Field: Offset
offset, err := readShortQuicVarint(buffer) // Field: Offset
if err != nil {
return nil, io.ErrUnexpectedEOF
}
length, err := readShortQUICVarint(buffer) // Field: Length
length, err := readShortQuicVarint(buffer) // Field: Length
if err != nil || length > buffer.Len() {
return nil, io.ErrUnexpectedEOF
}
@@ -251,13 +232,13 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
return nil, io.ErrUnexpectedEOF
}
case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet
if _, err = readShortQUICVarint(buffer); err != nil { // Field: Error Code
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Error Code
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQUICVarint(buffer); err != nil { // Field: Frame Type
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Frame Type
return nil, io.ErrUnexpectedEOF
}
length, err := readShortQUICVarint(buffer) // Field: Reason Phrase Length
length, err := readShortQuicVarint(buffer) // Field: Reason Phrase Length
if err != nil {
return nil, io.ErrUnexpectedEOF
}
@@ -267,7 +248,7 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
default:
// Only above frame types are permitted in initial packet.
// See https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2.2-8
return nil, errNotQUICInitial
return nil, errNotQuicInitial
}
}
@@ -285,33 +266,35 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
return nil, protocol.ErrProtoNeedMoreData
}
func hkdfExpandLabel(secret []byte, label string, length int) []byte {
b := make([]byte, 0, 2+1+6+len(label)+1)
b = binary.BigEndian.AppendUint16(b, uint16(length))
b = append(b, byte(6+len(label)))
b = append(b, "tls13 "...)
b = append(b, label...)
b = append(b, 0) // context
func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte {
b := make([]byte, 3, 3+6+len(label)+1+len(context))
binary.BigEndian.PutUint16(b, uint16(length))
b[2] = uint8(6 + len(label))
b = append(b, []byte("tls13 ")...)
b = append(b, []byte(label)...)
b = b[:3+6+len(label)+1]
b[3+6+len(label)] = uint8(len(context))
b = append(b, context...)
out := make([]byte, length)
n, err := hkdf.Expand(crypto.SHA256.New, secret, b).Read(out)
n, err := hkdf.Expand(hash.New, secret, b).Read(out)
if err != nil || n != length {
panic("quic: HKDF-Expand-Label invocation failed unexpectedly")
}
return out
}
// readShortQUICVarint wraps quicvarint.Read with a max limit for length related fields.
// readShortQuicVarint wraps quicvarint.Read with a max limit for length related fields.
// we only handle QUIC Initial so these numbers should not exceed 65535
// returns int32 to reduce type conversion
func readShortQUICVarint(reader io.ByteReader) (int32, error) {
func readShortQuicVarint(reader io.ByteReader) (int32, error) {
v, err := quicvarint.Read(reader)
if err != nil {
return 0, err
}
if v > 65535 {
// not used(
return 0, errNotQUICInitial
return 0, errNotQuicInitial
}
return int32(v), nil
}
File diff suppressed because one or more lines are too long
-1
View File
@@ -207,7 +207,6 @@ func getConfig() string {
"tag": "XHTTP_IN",
"streamSettings": {
"network": "xhttp",
"security": "tls",
"xhttpSettings": {
"host": "bing.com",
"path": "/xhttp_client_upload",
+19 -33
View File
@@ -8,7 +8,7 @@ import (
"io"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
@@ -27,39 +27,25 @@ var AddrParser = protocol.NewAddressParser(
)
var (
Show atomic.Bool
baseKey atomic.Value
Show bool
BaseKey []byte
)
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
}
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)
if strings.ToLower(platform.NewEnvFlag(platform.XUDPLog).GetValue(func() string { return "" })) == "true" {
Show = true
}
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)))
}
}()
}
func GetGlobalID(ctx context.Context) (globalID [8]byte) {
@@ -68,10 +54,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, ensureBaseKey())
h := blake3.New(8, BaseKey)
h.Write([]byte(inbound.Source.String()))
copy(globalID[:], h.Sum(nil))
if Show.Load() {
if Show {
errors.LogInfo(ctx, fmt.Sprintf("XUDP inbound.Source.String(): %v\tglobalID: %v\n", inbound.Source.String(), globalID))
}
}
+2 -2
View File
@@ -19,8 +19,8 @@ import (
var (
Version_x byte = 26
Version_y byte = 7
Version_z byte = 28
Version_y byte = 6
Version_z byte = 27
)
var (
-3
View File
@@ -187,9 +187,6 @@ 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")
+8 -18
View File
@@ -3,7 +3,6 @@ package policy
import (
"context"
"runtime"
"sync/atomic"
"time"
"github.com/xtls/xray-core/common/platform"
@@ -83,41 +82,32 @@ func ManagerType() interface{} {
return (*Manager)(nil)
}
var defaultBufferSize atomic.Int32
var defaultBufferSize int32
func reloadEnvSettings() error {
defaultBufferSize.Store(readDefaultBufferSize())
return nil
}
func readDefaultBufferSize() int32 {
func init() {
const defaultValue = -17
size := platform.NewEnvFlag(platform.BufferSize).GetValueAsInt(defaultValue)
switch size {
case 0:
return -1 // For pipe to use unlimited size
defaultBufferSize = -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":
return 0
defaultBufferSize = 0
case "arm64", "mips64", "mips64le":
return 4 * 1024 // 4k cache for low-end devices
defaultBufferSize = 4 * 1024 // 4k cache for low-end devices
default:
return 512 * 1024
defaultBufferSize = 512 * 1024
}
default:
return int32(size) * 1024 * 1024
defaultBufferSize = int32(size) * 1024 * 1024
}
}
func init() {
platform.RegisterEnvReload(reloadEnvSettings)
}
func defaultBufferPolicy() Buffer {
return Buffer{
PerConnection: defaultBufferSize.Load(),
PerConnection: defaultBufferSize,
}
}
+21 -23
View File
@@ -3,37 +3,36 @@ module github.com/xtls/xray-core
go 1.26
require (
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
github.com/cloudflare/circl v1.6.5
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716
github.com/cloudflare/circl v1.6.4
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/klauspost/cpuid/v2 v2.4.0
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138
github.com/miekg/dns v1.1.73
github.com/miekg/dns v1.1.72
github.com/pelletier/go-toml v1.9.5
github.com/pion/stun/v3 v3.1.7
github.com/pires/go-proxyproto v0.15.0
github.com/pion/stun/v3 v3.1.6
github.com/pires/go-proxyproto v0.14.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
github.com/sagernet/sing-shadowsocks v0.2.7
github.com/stretchr/testify v1.12.1
github.com/stretchr/testify v1.11.1
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.55.0
golang.org/x/crypto v0.53.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/net v0.58.0
golang.org/x/sync v0.22.0
golang.org/x/sys v0.47.0
golang.org/x/net v0.56.0
golang.org/x/sync v0.21.0
golang.org/x/sys v0.46.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.83.1
google.golang.org/protobuf v1.36.12
google.golang.org/grpc v1.82.0
google.golang.org/protobuf v1.36.11
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
h12.io/socks v1.0.3
lukechampine.com/blake3 v1.4.1
@@ -41,24 +40,23 @@ require (
require (
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/huin/goupnp v1.2.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/koron/go-ssdp v0.0.4 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/libp2p/go-netroute v0.2.1 // indirect
github.com/pion/dtls/v3 v3.1.5 // indirect
github.com/pion/dtls/v3 v3.1.4 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v4 v4.1.0 // indirect
github.com/pion/transport/v4 v4.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
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
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/text v0.38.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
golang.org/x/tools v0.45.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
)
+52 -61
View File
@@ -1,12 +1,14 @@
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg=
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8=
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkdYbw5JPGwBqIHs2J8tiEP7Py9lPqkN2I=
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
github.com/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U=
github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
@@ -21,52 +23,40 @@ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364 h1:5XxdakFhqd9dnXoAZy1Mb2R/DZ6D1e+0bGC/JhucGYI=
github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c=
github.com/huin/goupnp v1.2.0 h1:uOKW26NG1hsSSbXIZ1IR7XP9Gjd1U8pnLaCMgntmkmY=
github.com/huin/goupnp v1.2.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138 h1:YohuNPT/1k3VcThCQlBZ43PCPWPfMRS1zcxWBF2SLK8=
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138/go.mod h1:TXQg5tfSy+bUjnhT5728j5j/MBj7keIYqqZ1+8k/ui8=
github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU=
github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ=
github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE=
github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pion/dtls/v3 v3.1.5 h1:9xJtVsHwMYeSjPp5Hh1FTis4DchnQWtnOa5o+6ygqfc=
github.com/pion/dtls/v3 v3.1.5/go.mod h1:gz1K4jg6c+fq86oQMH4pilpCEOEPwmEr2jY+VcF/mkU=
github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY=
github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/stun/v3 v3.1.7 h1:uRXMTlGLf89WgItGNyZ6aR5jMTX0NBbybXADpQCzn+E=
github.com/pion/stun/v3 v3.1.7/go.mod h1:Nq77RW4aRrSNrltf2ksUJLjxWeipj4lnlgdsYIxC8g8=
github.com/pion/transport/v4 v4.1.0 h1:8S+nF2reM2cJuqC6g78OVy2BBgmbdns+acx3jA97BvQ=
github.com/pion/transport/v4 v4.1.0/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
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/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
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/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=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
@@ -79,8 +69,8 @@ github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
@@ -92,40 +82,38 @@ github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt7
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
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.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
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/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
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/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.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
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/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.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
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/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=
@@ -133,20 +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.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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/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.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
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/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.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
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/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=
@@ -158,18 +147,20 @@ golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
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-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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/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=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h1:Lk6hARj5UPY47dBep70OD/TIMwikJ5fGUGX0Rm3Xigk=
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo=
-3
View File
@@ -65,9 +65,6 @@ func (v *Address) UnmarshalJSON(data []byte) error {
}
func (v *Address) Build() *net.IPOrDomain {
if v == nil {
return nil
}
return net.NewIPOrDomain(v.Address)
}
-59
View File
@@ -1,59 +0,0 @@
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 -1
View File
@@ -23,7 +23,7 @@ func (o *ObservatoryConfig) Build() (proto.Message, error) {
type BurstObservatoryConfig struct {
SubjectSelector []string `json:"subjectSelector"`
// health check settings
HealthCheck *HealthCheckSettings `json:"pingConfig,omitempty"`
HealthCheck *healthCheckSettings `json:"pingConfig,omitempty"`
}
func (b BurstObservatoryConfig) Build() (proto.Message, error) {
-5
View File
@@ -148,7 +148,6 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) {
LocalIP *StringList `json:"localIP"`
LocalPort *PortList `json:"localPort"`
Process *StringList `json:"process"`
LocalOS *StringList `json:"localOS"`
Webhook *WebhookRuleConfig `json:"webhook"`
}
rawFieldRule := new(RawFieldRule)
@@ -262,10 +261,6 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) {
rule.Process = *rawFieldRule.Process
}
if rawFieldRule.LocalOS != nil && len(*rawFieldRule.LocalOS) > 0 {
rule.LocalOs = *rawFieldRule.LocalOS
}
if rawFieldRule.Webhook != nil && rawFieldRule.Webhook.URL != "" {
rule.Webhook = &router.WebhookConfig{
Url: rawFieldRule.Webhook.URL,
+3 -3
View File
@@ -43,8 +43,8 @@ type strategyLeastLoadConfig struct {
Tolerance float64 `json:"tolerance,omitempty"`
}
// HealthCheckSettings holds settings for health Checker
type HealthCheckSettings struct {
// healthCheckSettings holds settings for health Checker
type healthCheckSettings struct {
Destination string `json:"destination"`
Connectivity string `json:"connectivity"`
Interval duration.Duration `json:"interval"`
@@ -53,7 +53,7 @@ type HealthCheckSettings struct {
HttpMethod string `json:"httpMethod"`
}
func (h HealthCheckSettings) Build() (proto.Message, error) {
func (h healthCheckSettings) Build() (proto.Message, error) {
var httpMethod string
if h.HttpMethod == "" {
httpMethod = "HEAD"
+17 -105
View File
@@ -1,17 +1,14 @@
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"
@@ -25,7 +22,6 @@ 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"
)
@@ -71,7 +67,6 @@ 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{
@@ -720,81 +715,6 @@ 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"`
@@ -816,11 +736,9 @@ func (c *Xicmp) Build() (proto.Message, error) {
}
type Realm struct {
Url string `json:"url"`
StunServers []string `json:"stunServers"`
TlsConfig *TLSConfig `json:"tlsConfig"`
IPMode string `json:"ipMode"`
PortMapping *realm.PortMapping `json:"portMapping"`
Url string `json:"url"`
StunServers []string `json:"stunServers"`
TlsConfig *TLSConfig `json:"tlsConfig"`
}
func (c *Realm) Build() (proto.Message, error) {
@@ -900,8 +818,6 @@ func (c *Realm) Build() (proto.Message, error) {
ID: id,
StunServers: stunServers,
TlsConfig: tlsConfig,
IPMode: strings.ToLower(c.IPMode),
PortMapping: c.PortMapping,
}, nil
}
@@ -932,24 +848,20 @@ func (c *Mask) Build(tcp bool) (proto.Message, error) {
}
type QuicParamsConfig struct {
Congestion string `json:"congestion"`
Debug bool `json:"debug"`
BbrProfile string `json:"bbrProfile"`
BrutalUp Bandwidth `json:"brutalUp"`
BrutalDown Bandwidth `json:"brutalDown"`
BrutalDisableLossCompensation bool `json:"brutalDisableLossCompensation"`
UdpHop UdpHop `json:"udpHop"`
InitStreamReceiveWindow uint64 `json:"initStreamReceiveWindow"`
MaxStreamReceiveWindow uint64 `json:"maxStreamReceiveWindow"`
InitConnectionReceiveWindow uint64 `json:"initConnectionReceiveWindow"`
MaxConnectionReceiveWindow uint64 `json:"maxConnectionReceiveWindow"`
MaxIdleTimeout int64 `json:"maxIdleTimeout"`
KeepAlivePeriod int64 `json:"keepAlivePeriod"`
DisablePathMTUDiscovery bool `json:"disablePathMTUDiscovery"`
DisableChromeParrot bool `json:"disableChromeParrot"`
DisableGSO bool `json:"disableGSO"`
MaxIncomingStreams int64 `json:"maxIncomingStreams"`
DisableStatelessReset bool `json:"disableStatelessReset"`
Congestion string `json:"congestion"`
Debug bool `json:"debug"`
BbrProfile string `json:"bbrProfile"`
BrutalUp Bandwidth `json:"brutalUp"`
BrutalDown Bandwidth `json:"brutalDown"`
UdpHop UdpHop `json:"udpHop"`
InitStreamReceiveWindow uint64 `json:"initStreamReceiveWindow"`
MaxStreamReceiveWindow uint64 `json:"maxStreamReceiveWindow"`
InitConnectionReceiveWindow uint64 `json:"initConnectionReceiveWindow"`
MaxConnectionReceiveWindow uint64 `json:"maxConnectionReceiveWindow"`
MaxIdleTimeout int64 `json:"maxIdleTimeout"`
KeepAlivePeriod int64 `json:"keepAlivePeriod"`
DisablePathMTUDiscovery bool `json:"disablePathMTUDiscovery"`
MaxIncomingStreams int64 `json:"maxIncomingStreams"`
}
type FinalMask struct {
@@ -1,36 +0,0 @@
package conf
import (
"strings"
"testing"
"github.com/xtls/xray-core/transport/internet/finalmask/xmc"
)
func TestXMCBuildProfile(t *testing.T) {
built, err := (&XMC{
Password: "test-password",
Profiles: []XMCProfile{
{
Username: "TestUser",
UUID: "00112233-4455-6677-8899-aabbccddeeff",
TexturesValue: "textures-value",
TexturesSignature: "textures-signature",
},
},
}).Build()
if err != nil {
t.Fatalf("build XMC config: %v", err)
}
config := built.(*xmc.Config)
if len(config.Profiles) != 1 || len(config.Profiles[0].Uuid) != 16 {
t.Fatalf("unexpected profiles: %+v", config.Profiles)
}
}
func TestXMCBuildRequiresProfile(t *testing.T) {
_, err := (&XMC{Password: "test-password"}).Build()
if err == nil || !strings.Contains(err.Error(), "profiles are required") {
t.Fatalf("expected required profiles error, got %v", err)
}
}
+4 -8
View File
@@ -285,11 +285,10 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
}
config.QuicParams = &internet.QuicParams{
Congestion: c.FinalMask.QuicParams.Congestion,
BbrProfile: profile,
BrutalUp: up,
BrutalDown: down,
BrutalDisableLossCompensation: c.FinalMask.QuicParams.BrutalDisableLossCompensation,
Congestion: c.FinalMask.QuicParams.Congestion,
BbrProfile: profile,
BrutalUp: up,
BrutalDown: down,
UdpHop: &internet.UdpHop{
Ports: c.FinalMask.QuicParams.UdpHop.PortList.Build().Ports(),
IntervalMin: int64(c.FinalMask.QuicParams.UdpHop.Interval.From),
@@ -302,10 +301,7 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
MaxIdleTimeout: c.FinalMask.QuicParams.MaxIdleTimeout,
KeepAlivePeriod: c.FinalMask.QuicParams.KeepAlivePeriod,
DisablePathMtuDiscovery: c.FinalMask.QuicParams.DisablePathMTUDiscovery,
DisableChromeParrot: c.FinalMask.QuicParams.DisableChromeParrot,
DisableGSO: c.FinalMask.QuicParams.DisableGSO,
MaxIncomingStreams: c.FinalMask.QuicParams.MaxIncomingStreams,
DisableStatelessReset: c.FinalMask.QuicParams.DisableStatelessReset,
}
}
}
+2 -4
View File
@@ -450,8 +450,8 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
}
if c.Xmux == (XmuxConfig{}) {
c.Xmux.MaxConnections.From = 3
c.Xmux.MaxConnections.To = 3
c.Xmux.MaxConnections.From = 6
c.Xmux.MaxConnections.To = 6
c.Xmux.HMaxRequestTimes.From = 600
c.Xmux.HMaxRequestTimes.To = 900
c.Xmux.HMaxReusableSecs.From = 1800
@@ -751,7 +751,6 @@ type Masquerade struct {
Url string `json:"url"`
RewriteHost bool `json:"rewriteHost"`
XForwarded bool `json:"xForwarded"`
Insecure bool `json:"insecure"`
Content string `json:"content"`
@@ -792,7 +791,6 @@ func (c *HysteriaConfig) Build() (proto.Message, error) {
config.MasqFile = c.Masquerade.Dir
config.MasqUrl = c.Masquerade.Url
config.MasqUrlRewriteHost = c.Masquerade.RewriteHost
config.MasqUrlXForwarded = c.Masquerade.XForwarded
config.MasqUrlInsecure = c.Masquerade.Insecure
config.MasqString = c.Masquerade.Content
config.MasqStringHeaders = c.Masquerade.Headers
+2 -8
View File
@@ -113,10 +113,6 @@ 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)
@@ -161,10 +157,8 @@ func (c *REALITYConfig) Build() (proto.Message, error) {
}
for _, sn := range config.ServerNames {
sn = strings.ToLower(sn)
if strings.HasSuffix(sn, ".ru") || strings.HasSuffix(sn, ".ir") || strings.HasSuffix(sn, ".cn") ||
strings.Contains(sn, "apple") || strings.Contains(sn, "icloud") || strings.Contains(sn, "microsoft") {
errors.LogWarning(context.Background(), `REALITY: Choosing "`, sn, `" as the target will increase the likelihood of your server's IP being blocked by the GFW`)
if strings.Contains(sn, "apple") || strings.Contains(sn, "icloud") {
errors.LogWarning(context.Background(), `REALITY: Choosing apple, icloud, etc. as the target may get your IP blocked by the GFW`)
}
}
+1 -70
View File
@@ -1,19 +1,12 @@
package conf
import (
"crypto/rand"
"fmt"
"math/big"
"net"
"strconv"
"github.com/xtls/xray-core/proxy/tun"
"google.golang.org/protobuf/proto"
)
type TunConfig struct {
Name string `json:"name"`
Desc string `json:"desc"`
MTU uint32 `json:"mtu"`
Gateway []string `json:"gateway"`
DNS []string `json:"dns"`
@@ -25,7 +18,6 @@ type TunConfig struct {
func (v *TunConfig) Build() (proto.Message, error) {
config := &tun.Config{
Name: v.Name,
Desc: v.Desc,
MTU: v.MTU,
Gateway: v.Gateway,
DNS: v.DNS,
@@ -40,71 +32,10 @@ func (v *TunConfig) Build() (proto.Message, error) {
}
if config.Name == "" {
name, err := GetAvailableTunName()
if err != nil {
return nil, err
}
config.Name = name
}
if config.Desc == "" {
config.Desc = "Wintun"
config.Name = "xray0"
}
if config.MTU == 0 {
config.MTU = 1500
}
return config, nil
}
const (
tunNamePrefix = "utun"
minTunIndex = 10
maxTunIndex = 1024
)
func GetAvailableTunName() (string, error) {
interfaces, err := net.Interfaces()
if err != nil {
return "", fmt.Errorf("fail to get system interface information: %w", err)
}
usedNames := make(map[string]struct{}, len(interfaces))
for _, iface := range interfaces {
usedNames[iface.Name] = struct{}{}
}
startIndex, err := randomInt(minTunIndex, maxTunIndex)
if err != nil {
return "", fmt.Errorf("fail to generate valid tun name: %w", err)
}
rangeSize := maxTunIndex - minTunIndex + 1
for offset := 0; offset < rangeSize; offset++ {
index := minTunIndex + (startIndex-minTunIndex+offset)%rangeSize
name := tunNamePrefix + strconv.Itoa(index)
if _, exists := usedNames[name]; !exists {
return name, nil
}
}
return "", fmt.Errorf(
"no available TUN interface name in range %s%d-%s%d",
tunNamePrefix,
minTunIndex,
tunNamePrefix,
maxTunIndex,
)
}
func randomInt(min, max int) (int, error) {
value, err := rand.Int(
rand.Reader,
big.NewInt(int64(max-min+1)),
)
if err != nil {
return 0, err
}
return min + int(value.Int64()), nil
}
-2
View File
@@ -66,7 +66,6 @@ type WireGuardConfig struct {
MTU int32 `json:"mtu"`
Reserved []byte `json:"reserved"`
DomainStrategy string `json:"domainStrategy"`
DNS []string `json:"remoteDNS"`
}
func (c *WireGuardConfig) Build() (proto.Message, error) {
@@ -142,7 +141,6 @@ func (c *WireGuardConfig) Build() (proto.Message, error) {
config.IsClient = c.IsClient
config.NoKernelTun = c.NoKernelTun
config.DNS = c.DNS
return config, nil
}
+7 -29
View File
@@ -3,7 +3,6 @@ package conf
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
@@ -140,7 +139,7 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
// TUN inbound doesn't need port configuration as it uses network interface instead
if strings.ToLower(c.Protocol) == "tun" {
// Skip port validation for TUN
} else if c.ListenOn == nil || len(c.ListenOn.String()) == 0 {
} else if c.ListenOn == nil {
// Listen on anyip, must set PortList
if c.PortList == nil {
return nil, errors.New("Listen on AnyIP but no Port(s) set in InboundDetour.")
@@ -176,7 +175,7 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
receiverSettings.StreamSettings = ss
if strings.Contains(ss.SecurityType, "reality") && (receiverSettings.PortList == nil ||
len(receiverSettings.PortList.Ports()) != 1 || receiverSettings.PortList.Ports()[0] != 443) {
errors.LogWarning(context.Background(), `REALITY: Listening on non-443 ports will increase the likelihood of your server's IP being blocked by the GFW`)
errors.LogWarning(context.Background(), `REALITY: Listening on non-443 ports may get your IP blocked by the GFW`)
}
}
if c.SniffingConfig != nil {
@@ -251,13 +250,13 @@ func validateOutboundTransportSecurity(rawConfig interface{}, senderSettings *pr
if vlessCfg.Encryption != "" && vlessCfg.Encryption != "none" {
return nil
}
if requiresTransportSecurity(vlessCfg.Vnext[0].Address) {
if requiresTransportSecurity(vlessCfg.Address) {
return errors.New("vless without TLS or other encryption is prohibited unless the server address is a private IP or domain")
}
}
if tjCfg, ok := rawConfig.(*TrojanClientConfig); ok {
if requiresTransportSecurity(tjCfg.Servers[0].Address) {
if requiresTransportSecurity(tjCfg.Address) {
return errors.New("trojan without TLS is prohibited unless the server address is a private IP or domain")
}
}
@@ -358,13 +357,13 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
if err != nil {
return nil, errors.New("failed to load outbound detour config for protocol ", c.Protocol).Base(err)
}
if err := validateOutboundTransportSecurity(rawConfig, senderSettings); err != nil {
return nil, err
}
ts, err := rawConfig.(Buildable).Build()
if err != nil {
return nil, errors.New("failed to build outbound handler for protocol ", c.Protocol).Base(err)
}
if err := validateOutboundTransportSecurity(rawConfig, senderSettings); err != nil {
return nil, err
}
return &core.OutboundHandlerConfig{
SenderSettings: serial.ToTypedMessage(senderSettings),
@@ -380,20 +379,11 @@ 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"`
@@ -449,12 +439,6 @@ 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
}
@@ -530,12 +514,6 @@ func (c *Config) Override(o *Config, fn string) {
// Build implements Buildable.
func (c *Config) Build() (*core.Config, error) {
for key, value := range c.Env {
if err := os.Setenv(key, value); err != nil {
return nil, errors.New("failed to apply environment configuration").Base(err)
}
}
if err := PostProcessConfigureFile(c); err != nil {
return nil, errors.New("failed to post-process configuration file").Base(err)
}
-4
View File
@@ -36,10 +36,6 @@ 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.
-78
View File
@@ -1,78 +0,0 @@
//go:build openbsd
// +build openbsd
package dokodemo
import (
"fmt"
"net"
"os"
"golang.org/x/sys/unix"
)
func FakeUDP(addr *net.UDPAddr, mark int) (net.PacketConn, error) {
domain := unix.AF_INET6
var sockaddr unix.Sockaddr
if ip4 := addr.IP.To4(); ip4 != nil {
domain = unix.AF_INET
sa := &unix.SockaddrInet4{Port: addr.Port}
copy(sa.Addr[:], ip4)
sockaddr = sa
} else if ip6 := addr.IP.To16(); ip6 != nil {
sa := &unix.SockaddrInet6{Port: addr.Port}
copy(sa.Addr[:], ip6)
if addr.Zone != "" {
iface, err := net.InterfaceByName(addr.Zone)
if err != nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("resolve zone %s: %w", addr.Zone, err)}
}
sa.ZoneId = uint32(iface.Index)
}
sockaddr = sa
} else {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("unsupported address %v", addr.IP)}
}
fd, err := unix.Socket(domain, unix.SOCK_DGRAM, 0)
if err != nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("socket open: %w", err)}
}
closeFD := true
defer func() {
if closeFD {
unix.Close(fd)
}
}()
if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_BINDANY, 1); err != nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("set socket option SO_BINDANY: %w", err)}
}
if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1); err != nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("set socket option SO_REUSEADDR: %w", err)}
}
// Several client sessions can be answered from the same original
// destination at the same time, so the address has to be shareable.
if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("set socket option SO_REUSEPORT: %w", err)}
}
if err = unix.Bind(fd, sockaddr); err != nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("bind %s: %w", addr.String(), err)}
}
fdFile := os.NewFile(uintptr(fd), fmt.Sprintf("net-udp-bindany-%s", addr.String()))
if fdFile == nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("convert descriptor to file")}
}
defer fdFile.Close()
packetConn, err := net.FilePacketConn(fdFile)
if err != nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("convert descriptor to packet connection: %w", err)}
}
closeFD = false
return packetConn, nil
}
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build !linux && !openbsd
// +build !linux,!openbsd
//go:build !linux
// +build !linux
package dokodemo
+8 -16
View File
@@ -5,7 +5,6 @@ import (
"crypto/rand"
"io"
"strings"
"sync/atomic"
"time"
"github.com/pires/go-proxyproto"
@@ -32,24 +31,12 @@ import (
)
var (
useSplice atomic.Bool
useSplice 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)
@@ -61,7 +48,12 @@ func init() {
return h, nil
}))
platform.RegisterEnvReload(reloadEnvSettings)
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue })
switch value {
case defaultFlagValue, "auto", "enable":
useSplice = true
}
for i := range allNetworks {
allNetworks[i] = true
@@ -430,7 +422,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.Load() && 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 && 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 {
+3 -6
View File
@@ -173,18 +173,15 @@ func fillRequestHeader(ctx context.Context, header []*Header) ([]*Header, error)
outbounds := session.OutboundsFromContext(ctx)
ob := outbounds[len(outbounds)-1]
var src net.Destination
if inbound != nil {
src = inbound.Source
} else {
src = net.TCPDestination(net.AnyIP, 0)
if inbound == nil || ob == nil {
return nil, errors.New("missing inbound or outbound metadata from context")
}
data := struct {
Source net.Destination
Target net.Destination
}{
Source: src,
Source: inbound.Source,
Target: ob.Target,
}
+2 -6
View File
@@ -31,17 +31,13 @@ Here is simple Xray config snippet to enable the inbound:
"port": 0,
"protocol": "tun",
"settings": {
"name": "utun10",
"desc": "Wintun",
"mtu": 1500
"name": "xray0",
"MTU": 1492
}
}
],
```
`desc` sets the Windows Wintun adapter tunnel type and defaults to `Wintun`.
It is ignored on other platforms.
## SUPPORTED FEATURES
- IPv4 and IPv6
+4 -14
View File
@@ -7,12 +7,11 @@
package tun
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
)
const (
@@ -31,7 +30,6 @@ type Config struct {
UserLevel uint32 `protobuf:"varint,5,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
AutoSystemRoutingTable []string `protobuf:"bytes,6,rep,name=auto_system_routing_table,json=autoSystemRoutingTable,proto3" json:"auto_system_routing_table,omitempty"`
AutoOutboundsInterface string `protobuf:"bytes,7,opt,name=auto_outbounds_interface,json=autoOutboundsInterface,proto3" json:"auto_outbounds_interface,omitempty"`
Desc string `protobuf:"bytes,8,opt,name=desc,proto3" json:"desc,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -115,18 +113,11 @@ func (x *Config) GetAutoOutboundsInterface() string {
return ""
}
func (x *Config) GetDesc() string {
if x != nil {
return x.Desc
}
return ""
}
var File_proxy_tun_config_proto protoreflect.FileDescriptor
const file_proxy_tun_config_proto_rawDesc = "" +
"\n" +
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\x82\x02\n" +
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\xee\x01\n" +
"\x06Config\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
"\x03MTU\x18\x02 \x01(\rR\x03MTU\x12\x18\n" +
@@ -135,8 +126,7 @@ const file_proxy_tun_config_proto_rawDesc = "" +
"\n" +
"user_level\x18\x05 \x01(\rR\tuserLevel\x129\n" +
"\x19auto_system_routing_table\x18\x06 \x03(\tR\x16autoSystemRoutingTable\x128\n" +
"\x18auto_outbounds_interface\x18\a \x01(\tR\x16autoOutboundsInterface\x12\x12\n" +
"\x04desc\x18\b \x01(\tR\x04descBL\n" +
"\x18auto_outbounds_interface\x18\a \x01(\tR\x16autoOutboundsInterfaceBL\n" +
"\x12com.xray.proxy.tunP\x01Z#github.com/xtls/xray-core/proxy/tun\xaa\x02\x0eXray.Proxy.Tunb\x06proto3"
var (
-1
View File
@@ -14,5 +14,4 @@ message Config {
uint32 user_level = 5;
repeated string auto_system_routing_table = 6;
string auto_outbounds_interface = 7;
string desc = 8;
}
+5 -136
View File
@@ -11,8 +11,6 @@ import (
"os"
"strconv"
"sync"
"sync/atomic"
"time"
"unsafe"
"github.com/xtls/xray-core/common/buf"
@@ -40,111 +38,21 @@ const (
ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h
)
//go:linkname procyield runtime.procyield
func procyield(cycles uint32)
type DarwinTun struct {
tunFile *os.File
options *Config
tunFd int
ownsFd bool // true for macOS (we created the fd), false for iOS (fd from system)
// Genuinely blocks Wait() until tunFd is readable, instead of the
// previous procyield-only busy-spin (dispatchLoop in
// stack_gvisor_endpoint.go calls ReadPacket() then Wait() in a tight
// loop with no other throttling whenever the queue is empty -- with
// only procyield(1), that pins a full CPU core for as long as the
// tunnel is up, observed causing severe device heating/thermal
// shutdown). nil if kqueue setup failed, in which case Wait() falls
// back to a bounded time.Sleep instead. See waitKqueue's own doc
// comment for why this is a dedicated type rather than a bare fd.
waitKq *waitKqueue
routeMonitor *os.File
routeMonitorOnce sync.Once
systemRoutes []netip.Prefix
gateway netip.Prefix
}
// waitKqueue owns a kqueue fd used by DarwinTun.Wait() to block on
// read-readiness. Closing and waiting can race from different goroutines
// (Close() from the caller that tears down the tunnel, Wait() from
// dispatchLoop's own goroutine) -- reviewer feedback on XTLS/Xray-core#6580
// found that a bare `int` fd field let Close() race Wait()'s use of the
// same fd number, and on Darwin a closed fd number can be reused by an
// unrelated concurrent open() before Wait() gets to call Kevent on it,
// so Wait() could end up polling (or Close() could end up closing) a
// completely unrelated file descriptor. This type makes closing
// idempotent (sync.Once) and gates every Kevent call behind an atomic
// "closed" flag checked immediately before the syscall, so Wait() never
// issues a kevent syscall against a fd number that Close() has already
// (or is concurrently) invalidated -- there's still a narrow window where
// Wait() checks-then-uses the fd, but Close() only actually closes it
// after Wait() cannot start a new syscall on it (the flag is set first,
// synchronized with acquire/release semantics), which is sufficient since
// Wait()'s Kevent call itself is what's being raced, not a fd read/write.
type waitKqueue struct {
fd int
closed atomic.Bool
once sync.Once
}
// newWaitKqueue creates a kqueue registered for read-readiness on fd, for
// Wait() to block on. Returns nil if anything fails, so callers can fall
// back to a bounded sleep rather than error out of NewTun over what is
// purely a CPU-efficiency concern.
func newWaitKqueue(fd int) *waitKqueue {
kq, err := unix.Kqueue()
if err != nil {
return nil
}
_, err = unix.Kevent(kq, []unix.Kevent_t{{
Ident: uint64(fd),
Filter: unix.EVFILT_READ,
Flags: unix.EV_ADD | unix.EV_ENABLE,
}}, nil, nil)
if err != nil {
_ = unix.Close(kq)
return nil
}
return &waitKqueue{fd: kq}
}
// wait blocks until the registered fd is readable, timeout elapses, or a
// benign interrupt occurs -- all three are "this kqueue is still healthy,
// the caller should just try again" and return true; the caller
// (DarwinTun.Wait) doesn't need to distinguish them since it always calls
// ReadPacket() right after anyway, and that already handles "nothing was
// actually there" via ErrQueueEmpty. Returns false only when the kqueue
// itself is no longer usable -- already closed, or the kevent syscall
// failed for a reason other than EINTR -- see its own call site in
// DarwinTun.Wait for why a persistent failure must not be silently
// retried forever (reviewer feedback, XTLS/Xray-core#6580 P2).
func (w *waitKqueue) wait(timeout time.Duration) (ok bool) {
if w.closed.Load() {
return false
}
events := make([]unix.Kevent_t, 1)
ts := unix.NsecToTimespec(timeout.Nanoseconds())
_, err := unix.Kevent(w.fd, nil, events, &ts)
if err != nil {
return errors.Is(err, unix.EINTR)
}
return true
}
// close marks the kqueue as unusable (so any Wait() call that hasn't yet
// entered the kevent syscall bails out instead) and closes the underlying
// fd exactly once, regardless of how many times close is called or
// whether it races a Wait() already inside its kevent syscall (that call
// either completes against the still-open fd or returns an error safely
// -- either way, no other goroutine can be handed this fd number in
// between the atomic flag flip and the actual close, since nothing else
// in this type ever creates a new kqueue with the same field).
func (w *waitKqueue) close() {
w.once.Do(func() {
w.closed.Store(true)
_ = unix.Close(w.fd)
})
}
var (
_ Tun = (*DarwinTun)(nil)
_ GVisorDevice = (*DarwinTun)(nil)
@@ -169,7 +77,6 @@ func NewTun(options *Config) (Tun, error) {
options: options,
tunFd: fd,
ownsFd: false,
waitKq: newWaitKqueue(fd),
}, nil
}
@@ -196,7 +103,6 @@ func NewTun(options *Config) (Tun, error) {
options: options,
tunFd: int(tunFile.Fd()),
ownsFd: true,
waitKq: newWaitKqueue(int(tunFile.Fd())),
gateway: gateway,
}, nil
}
@@ -228,9 +134,6 @@ func (t *DarwinTun) Close() error {
_ = t.routeMonitor.Close()
}
})
if t.waitKq != nil {
t.waitKq.close()
}
routeErr := t.unsetSystemRoutes()
if t.ownsFd {
return xerrors.Combine(routeErr, t.tunFile.Close())
@@ -339,43 +242,9 @@ func (t *DarwinTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
}), nil
}
// Wait blocks until tunFd is readable (or a short timeout elapses), rather
// than spinning the CPU -- see the waitKq field's own doc comment. A bounded
// timeout (not an indefinite wait) keeps this responsive to a Close() that
// happens to race a call already parked here.
//
// Reviewer feedback (XTLS/Xray-core#6580, P2): the original version
// discarded every error from the underlying kevent syscall. dispatchLoop
// (stack_gvisor_endpoint.go) calls ReadPacket() then Wait() in an
// unconditional tight loop -- if kevent started failing at runtime for a
// persistent reason (not just a benign EINTR), Wait() returning
// immediately every time reintroduces exactly the busy-spin this whole
// change exists to remove, just routed through a failing syscall instead
// of procyield. waitKq.wait's own bool return distinguishes "genuinely
// interrupted, try again" from "this kqueue is unusable now" -- Wait()
// permanently falls back to the sleep path once that happens, rather than
// retrying the same broken kqueue forever.
// Wait some cpu cycles
func (t *DarwinTun) Wait() {
if t.waitKq != nil && t.waitKq.wait(time.Second) {
return
}
if t.waitKq != nil {
// Persistent kevent failure (not a benign EINTR, and not just
// "the 1s timeout elapsed with nothing to read" -- wait() already
// returned true for both of those cases above). Stop trusting
// this kqueue for the rest of this DarwinTun's lifetime instead of
// re-attempting a syscall that's already shown it won't succeed.
t.waitKq.close()
t.waitKq = nil
}
// Reviewer feedback (XTLS/Xray-core#6580): procyield here is the same
// busy-spin this whole change exists to remove, just gated behind an
// edge case (kqueue setup failing, which practically never happens on
// real Darwin systems, or having just failed permanently above)
// instead of always -- a genuine bounded sleep actually yields the CPU
// instead of being a near-instant scheduler hint that lets the tight
// dispatchLoop caller spin just as hot as before.
time.Sleep(time.Millisecond)
procyield(1)
}
func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) {
-184
View File
@@ -3,11 +3,7 @@
package tun
import (
"sync"
"testing"
"time"
"golang.org/x/sys/unix"
)
func TestSelectDarwinGatewayDefault(t *testing.T) {
@@ -51,183 +47,3 @@ func TestSelectDarwinGatewayRequiresUsableLocalAddress(t *testing.T) {
t.Fatal("expected error")
}
}
// newTestSocketpair returns a connected AF_UNIX/SOCK_DGRAM pair -- a real
// fd DarwinTun.Wait's kqueue can register EVFILT_READ against, without
// needing an actual utun interface (which requires root/network
// entitlements this test environment doesn't have). Datagram sockets
// (unlike pipes) support both "write makes readable" and "close makes
// readable" the same way a tun fd's read-readiness behaves.
func newTestSocketpair(t *testing.T) (a, b int) {
t.Helper()
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_DGRAM, 0)
if err != nil {
t.Fatalf("socketpair: %v", err)
}
t.Cleanup(func() {
_ = unix.Close(fds[0])
_ = unix.Close(fds[1])
})
return fds[0], fds[1]
}
// Reviewer feedback, XTLS/Xray-core#6580: "blocking with no data" case --
// wait() must not return before the timeout when nothing is written.
func TestWaitKqueueBlocksWithNoData(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
defer kq.close()
start := time.Now()
ok := kq.wait(150 * time.Millisecond)
elapsed := time.Since(start)
if !ok {
t.Fatal("wait() returned false on a healthy kqueue with a plain timeout")
}
if elapsed < 100*time.Millisecond {
t.Fatalf("wait() returned after only %v, expected it to block close to the 150ms timeout", elapsed)
}
}
// Reviewer feedback: "wake up with a readable fd" case.
func TestWaitKqueueWakesOnReadable(t *testing.T) {
a, b := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
defer kq.close()
done := make(chan bool, 1)
go func() {
done <- kq.wait(5 * time.Second)
}()
time.Sleep(20 * time.Millisecond) // let wait() actually enter the syscall first
if _, err := unix.Write(b, []byte{0x1}); err != nil {
t.Fatalf("write: %v", err)
}
select {
case ok := <-done:
if !ok {
t.Fatal("wait() returned false after the fd became readable")
}
case <-time.After(2 * time.Second):
t.Fatal("wait() did not wake up within 2s of the fd becoming readable")
}
}
// Reviewer feedback: "timeout" case, explicitly (distinct from the
// no-data test above, which also checks blocking duration -- this one
// only checks the return value).
func TestWaitKqueueTimesOut(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
defer kq.close()
if !kq.wait(50 * time.Millisecond) {
t.Fatal("wait() returned false on a plain timeout with no error condition")
}
}
// Reviewer feedback: "Close() wakes a blocked wait" case, and the
// no-double-close/no-fd-reuse concern (P1) -- close() while wait() is
// parked in its syscall must not panic, must not leave wait() hung, and a
// second close() call (from a caller that, say, calls Close() twice on
// the same DarwinTun) must be safe.
func TestWaitKqueueCloseDuringWaitIsSafe(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
started := make(chan struct{})
done := make(chan bool, 1)
go func() {
close(started)
done <- kq.wait(5 * time.Second)
}()
<-started
time.Sleep(20 * time.Millisecond) // let wait() actually enter the syscall first
kq.close()
kq.close() // double-close must be idempotent, not panic or double-free the fd
select {
case <-done:
// Either true (the close-of-the-underlying-fd unblocked kevent, a
// real kqueue behavior) or false (wait() observed the closed flag
// first) is acceptable -- what matters is that it returned at all,
// promptly, without hanging or crashing.
case <-time.After(2 * time.Second):
t.Fatal("wait() did not return within 2s of close() being called")
}
// A wait() call *after* close() must return false immediately (the
// closed-flag fast path), not attempt a syscall against the
// already-closed (and potentially since-reused, on a real system) fd
// number.
if kq.wait(time.Second) {
t.Fatal("wait() returned true after close() -- should short-circuit via the closed flag")
}
}
// Reviewer feedback: "multiple/concurrent close guard" case -- many
// goroutines calling close() concurrently must close the underlying fd
// exactly once.
func TestWaitKqueueConcurrentCloseIsSafe(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
var wg sync.WaitGroup
for range 20 {
wg.Add(1)
go func() {
defer wg.Done()
kq.close()
}()
}
wg.Wait()
if !kq.closed.Load() {
t.Fatal("closed flag not set after concurrent close() calls")
}
}
// Reviewer feedback: "kevent runtime failure without spinning" case (P2).
// Simulates a kqueue that has gone bad (closed out from under it, as if a
// concurrent/erroneous close happened) and confirms wait() reports it as
// unusable (false) rather than silently returning true forever, which is
// what DarwinTun.Wait relies on to permanently fall back to the sleep
// path instead of re-entering a failing syscall on every dispatchLoop
// iteration.
func TestWaitKqueueReportsPersistentFailure(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
// Close the underlying kqueue fd directly (bypassing kq.close(), which
// would also set the closed flag) to simulate the fd going bad for a
// reason other than this type's own close() -- e.g. some other code
// path in the process closing it, or the kernel invalidating it.
_ = unix.Close(kq.fd)
for i := 0; i < 5; i++ {
if kq.wait(50 * time.Millisecond) {
t.Fatalf("wait() call %d returned true against a closed underlying fd -- should report failure, not spin", i)
}
}
}
+23 -727
View File
@@ -3,60 +3,28 @@
package tun
import (
"context"
"errors"
"net"
"net/netip"
"os"
"slices"
"sync"
"unsafe"
_ "unsafe"
"golang.zx2c4.com/wireguard/tun"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
"github.com/xtls/xray-core/common/buf"
xerrors "github.com/xtls/xray-core/common/errors"
)
const (
tunHeaderSize = 4
defaultFreeBSDGateway = "169.254.10.1/30"
// escapeFib is the routing table outbound sockets are switched to so
// their traffic bypasses the TUN routes installed in the default FIB
// (FreeBSD's substitute for the per-socket interface binding other
// platforms use). Requires the boot tunable net.fibs >= 2.
escapeFib = 1
)
const tunHeaderSize = 4
//go:linkname procyield runtime.procyield
func procyield(cycles uint32)
type FreeBSDTun struct {
device tun.Device
options *Config
tunIndex int
autoInterface bool
systemRoutes []netip.Prefix
escapeMu sync.Mutex
escapeRoutes []escapeRoute
routeMonitor *os.File
routeMonitorOnce sync.Once
}
// escapeRoute remembers one route written into the escape FIB, in the exact
// shape needed to delete it again. A zero gateway means an interface route.
type escapeRoute struct {
prefix netip.Prefix
ifIndex int
gateway netip.Addr
device tun.Device
mtu uint32
}
var (
@@ -66,172 +34,20 @@ var (
// NewTun builds new tun interface handler
func NewTun(options *Config) (Tun, error) {
gateway, local, err := selectFreeBSDGateway(options.Gateway)
if err != nil {
return nil, err
}
// net.fibs is a boot-time constant, so validate the escape routing table
// before the shared handler registers a dialer controller that would
// otherwise steer every outbound socket into a table that was never set up.
if options.AutoOutboundsInterface != "" {
if err := checkEscapeFib(); err != nil {
return nil, err
}
}
tunDev, err := tun.CreateTUN(options.Name, int(options.MTU))
if err != nil {
return nil, err
}
name, err := tunDev.Name()
if err != nil {
_ = tunDev.Close()
return nil, err
}
// From here the interface exists in the kernel; the wireguard library does
// not remove it on Close, so every failure path must destroy it too or the
// next start fails with "interface already exists".
iface, err := net.InterfaceByName(name)
if err != nil {
_ = tunDev.Close()
destroyInterface(name)
return nil, err
}
if err := setIPAddress(name, gateway, local, iface.Index); err != nil {
_ = tunDev.Close()
destroyInterface(name)
return nil, err
}
return &FreeBSDTun{
device: tunDev,
options: options,
tunIndex: iface.Index,
autoInterface: options.AutoOutboundsInterface != "",
}, nil
}
// selectFreeBSDGateway picks the first IPv4 prefix from the configured gateway
// list and the local address derived from it (the darwin semantics: the
// gateway is the remote side of the point-to-point pair, the local address is
// the next one after it), falling back to the same link-local default.
func selectFreeBSDGateway(configured []string) (netip.Prefix, netip.Addr, error) {
gateway := netip.MustParsePrefix(defaultFreeBSDGateway)
if len(configured) > 0 {
found := false
for _, value := range configured {
prefix, err := netip.ParsePrefix(value)
if err != nil {
return netip.Prefix{}, netip.Addr{}, xerrors.New("invalid FreeBSD gateway ", value).Base(err)
}
if prefix.Addr().Is4() {
gateway, found = prefix, true
break
}
}
if !found {
return netip.Prefix{}, netip.Addr{}, xerrors.New("FreeBSD gateway requires at least one IPv4 prefix")
}
}
local, ok := nextLocalIPv4(gateway)
if !ok || !gateway.Contains(local) {
return netip.Prefix{}, netip.Addr{}, xerrors.New("FreeBSD gateway ", gateway.String(), " must contain at least one usable local IPv4 address after the gateway address")
}
return gateway, local, nil
}
func nextLocalIPv4(gateway netip.Prefix) (netip.Addr, bool) {
local4 := gateway.Addr().As4()
for i := len(local4) - 1; i >= 0; i-- {
local4[i]++
if local4[i] != 0 {
return netip.AddrFrom4(local4), true
}
}
return netip.Addr{}, false
return &FreeBSDTun{device: tunDev, mtu: options.MTU}, nil
}
func (t *FreeBSDTun) Start() error {
if err := t.setSystemRoutes(); err != nil {
return err
}
// Gate on this instance's own option, not the package-global updater,
// which a previously-removed inbound may have left set. checkEscapeFib
// already ran in NewTun, before the dialer controller was registered.
if t.autoInterface {
if err := t.syncEscapeFib(); err != nil {
_ = t.unsetSystemRoutes()
return err
}
fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
if err != nil {
t.unsetEscapeFib()
_ = t.unsetSystemRoutes()
return err
}
t.routeMonitor = os.NewFile(uintptr(fd), "xray-route-monitor")
go t.monitorRouteChanges()
}
return nil
}
// monitorRouteChanges refreshes the outbound interface and the escape FIB
// mirror whenever the system routing table changes.
func (t *FreeBSDTun) monitorRouteChanges() {
buffer := make([]byte, 64*1024)
for {
if _, err := t.routeMonitor.Read(buffer); err != nil {
if !errors.Is(err, os.ErrClosed) {
xerrors.LogInfoInner(context.Background(), err, "[tun] failed to monitor route changes")
}
return
}
if updater != nil {
updater.Update()
}
if err := t.syncEscapeFib(); err != nil {
xerrors.LogInfoInner(context.Background(), err, "[tun] failed to refresh escape routes")
}
}
}
func (t *FreeBSDTun) Close() error {
t.routeMonitorOnce.Do(func() {
if t.routeMonitor != nil {
_ = t.routeMonitor.Close()
}
})
t.unsetEscapeFib()
routeErr := t.unsetSystemRoutes()
name, nameErr := t.Name()
closeErr := t.device.Close()
// The wireguard tun device does not tear the interface down on FreeBSD,
// so an unclean shutdown would leave utun<n> behind and the next start
// would fail with "interface already exists"; destroy it explicitly.
if nameErr == nil {
destroyInterface(name)
}
return xerrors.Combine(routeErr, closeErr)
}
func destroyInterface(name string) {
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
if err != nil {
return
}
defer unix.Close(fd)
// struct ifreq: 16-byte name + a 16-byte union (SIOCIFDESTROY's encoded
// length is 32 bytes on amd64, and the kernel copies in all of it).
var req struct {
Name [unix.IFNAMSIZ]byte
_ [16]byte
}
copy(req.Name[:], name)
_ = ioctlPtr(fd, unix.SIOCIFDESTROY, unsafe.Pointer(&req))
return t.device.Close()
}
func (t *FreeBSDTun) Name() (string, error) {
@@ -239,13 +55,21 @@ func (t *FreeBSDTun) Name() (string, error) {
}
func (t *FreeBSDTun) Index() (int, error) {
return t.tunIndex, nil
name, err := t.Name()
if err != nil {
return 0, err
}
iface, err := net.InterfaceByName(name)
if err != nil {
return 0, err
}
return iface.Index, nil
}
// WritePacket implements GVisorDevice method to write one packet to the tun device
func (t *FreeBSDTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
// request memory to write from reusable buffer pool
b := buf.NewWithSize(int32(t.options.MTU) + tunHeaderSize)
b := buf.NewWithSize(int32(t.mtu) + tunHeaderSize)
defer b.Release()
// prepare Unix specific packet header
@@ -280,7 +104,7 @@ func (t *FreeBSDTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
// which will make the stack call Wait which should implement desired push-back
func (t *FreeBSDTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
// request memory to write from reusable buffer pool
b := buf.NewWithSize(int32(t.options.MTU) + tunHeaderSize)
b := buf.NewWithSize(int32(t.mtu) + tunHeaderSize)
// read the bytes to the interface file
n, err := b.ReadFrom(t.device.File())
@@ -317,551 +141,23 @@ func (t *FreeBSDTun) Wait() {
}
func (t *FreeBSDTun) newEndpoint() (stack.LinkEndpoint, error) {
return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil
return &LinkEndpoint{deviceMTU: t.mtu, device: t}, nil
}
const (
IN6_IFF_NODAD = 0x0020 // netinet6/in6_var.h
ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h
)
// ifAliasReq4 is struct in_aliasreq from netinet/in_var.h in the 64-byte
// layout unix.SIOCAIFADDR encodes (name + addr/dstaddr/mask sockaddrs).
type ifAliasReq4 struct {
Name [unix.IFNAMSIZ]byte
Addr unix.RawSockaddrInet4
Dstaddr unix.RawSockaddrInet4
Mask unix.RawSockaddrInet4
}
// ifAliasReq6 is struct in6_aliasreq from netinet6/in6_var.h. The trailing
// Vhid field matters: unix.SIOCAIFADDR_IN6 is not in x/sys/unix, so
// siocaifaddrIn6 is derived from this struct's size, and the kernel only
// accepts the ioctl whose encoded length matches the real struct.
type ifAliasReq6 struct {
Name [unix.IFNAMSIZ]byte
Addr unix.RawSockaddrInet6
Dstaddr unix.RawSockaddrInet6
Prefixmask unix.RawSockaddrInet6
Flags int32
Lifetime addrLifetime6
Vhid int32
}
// addrLifetime6 is struct in6_addrlifetime (time_t is int64 on freebsd/amd64).
type addrLifetime6 struct {
Expire int64
Preferred int64
Vltime uint32
Pltime uint32
}
// SIOCAIFADDR_IN6 = _IOW('i', 27, struct in6_aliasreq); x/sys/unix does not
// carry the netinet6 ioctls, so encode it from the struct size like the
// header macro does.
const siocaifaddrIn6 = 0x80000000 | (uintptr(unsafe.Sizeof(ifAliasReq6{})) << 16) | ('i' << 8) | 27
// setIPAddress assigns the local/remote point-to-point IPv4 pair and a
// link-local IPv6 address to the interface, required for the routing to work
// (same scheme as the darwin implementation: local address is the one right
// after the gateway address).
func setIPAddress(name string, gateway netip.Prefix, local netip.Addr, ifIndex int) error {
socket4, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
if err != nil {
return err
}
defer unix.Close(socket4)
local4 := local.As4()
ifReq4 := ifAliasReq4{
Addr: unix.RawSockaddrInet4{
Len: unix.SizeofSockaddrInet4,
Family: unix.AF_INET,
Addr: local4,
},
Dstaddr: unix.RawSockaddrInet4{
Len: unix.SizeofSockaddrInet4,
Family: unix.AF_INET,
Addr: gateway.Addr().As4(),
},
Mask: unix.RawSockaddrInet4{
Len: unix.SizeofSockaddrInet4,
Family: unix.AF_INET,
Addr: prefixMask4(gateway.Bits()),
},
}
copy(ifReq4.Name[:], name)
if err = ioctlPtr(socket4, unix.SIOCAIFADDR, unsafe.Pointer(&ifReq4)); err != nil {
return os.NewSyscallError("SIOCAIFADDR", err)
}
socket6, err := unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, 0)
if err != nil {
return err
}
defer unix.Close(socket6)
// link-local ipv6 address with suffix from ipv4, enough for v6 interface
// routes to be attachable (darwin parity); a link-local address needs its
// scope, which for FreeBSD ioctls is the interface index
local6 := netip.AddrFrom16([16]byte{0: 0xfe, 1: 0x80, 12: local4[0], 13: local4[1], 14: local4[2], 15: local4[3]})
ifReq6 := ifAliasReq6{
Addr: unix.RawSockaddrInet6{
Len: unix.SizeofSockaddrInet6,
Family: unix.AF_INET6,
Addr: local6.As16(),
Scope_id: uint32(ifIndex),
},
Prefixmask: unix.RawSockaddrInet6{
Len: unix.SizeofSockaddrInet6,
Family: unix.AF_INET6,
Addr: prefixMask6(64),
},
Flags: IN6_IFF_NODAD,
Lifetime: addrLifetime6{
Vltime: ND6_INFINITE_LIFETIME,
Pltime: ND6_INFINITE_LIFETIME,
},
}
copy(ifReq6.Name[:], name)
if err = ioctlPtr(socket6, uint(siocaifaddrIn6), unsafe.Pointer(&ifReq6)); err != nil {
// non-fatal: FreeBSD auto-configures a link-local address on UP
// interfaces, which is all the v6 interface routes need
xerrors.LogInfoInner(context.Background(), os.NewSyscallError("SIOCAIFADDR_IN6", err), "[tun] failed to assign the IPv6 link-local address")
}
return nil
}
func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error {
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg))
if errno != 0 {
return errno
}
return nil
}
func prefixMask4(bits int) [4]byte {
var mask [4]byte
copy(mask[:], net.CIDRMask(bits, 32))
return mask
}
func prefixMask6(bits int) [16]byte {
var mask [16]byte
copy(mask[:], net.CIDRMask(bits, 128))
return mask
}
// setinterface is the per-socket half of autoOutboundsInterface. FreeBSD has
// no SO_BINDTODEVICE/IP_BOUND_IF equivalent, so the socket is pointed at the
// escape FIB instead, where Start() mirrors the physical default route; the
// iface argument is resolved by the shared updater but unused here (the escape
// is table-based, not a per-socket interface bind). checkEscapeFib in NewTun
// guarantees the FIB exists before this can run.
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
return unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_SETFIB, escapeFib)
}
func (t *FreeBSDTun) setSystemRoutes() error {
routes, err := buildSystemRoutes(t.options.AutoSystemRoutingTable)
if err != nil {
return err
}
// Route through the interface, not a gateway: the tun(4) device is a
// broadcast interface here, so its point-to-point peer address doubles as
// the subnet broadcast and the kernel refuses to route to it (EACCES).
// Interface routes sidestep the gateway entirely (what wg-quick does on
// FreeBSD).
for _, destination := range routes {
if err := execRoute(-1, unix.RTM_ADD, t.tunIndex, destination, netip.Addr{}); err != nil {
_ = t.unsetSystemRoutes()
return xerrors.New("failed to add system route ", destination).Base(err)
}
t.systemRoutes = append(t.systemRoutes, destination)
}
return nil
}
func (t *FreeBSDTun) unsetSystemRoutes() error {
var errs []error
for i := len(t.systemRoutes) - 1; i >= 0; i-- {
destination := t.systemRoutes[i]
if err := execRoute(-1, unix.RTM_DELETE, t.tunIndex, destination, netip.Addr{}); err != nil && !errors.Is(err, unix.ESRCH) {
errs = append(errs, xerrors.New("failed to delete system route ", destination).Base(err))
}
}
t.systemRoutes = nil
return xerrors.Combine(errs...)
}
func buildSystemRoutes(configured []string) ([]netip.Prefix, error) {
routes := make([]netip.Prefix, 0, len(configured))
seen := make(map[netip.Prefix]struct{})
appendRoute := func(prefix netip.Prefix) {
prefix = prefix.Masked()
if _, found := seen[prefix]; found {
return
}
seen[prefix] = struct{}{}
routes = append(routes, prefix)
}
for _, value := range configured {
prefix, err := netip.ParsePrefix(value)
if err != nil {
return nil, xerrors.New("invalid system route ", value).Base(err)
}
if prefix.Bits() == 0 {
for _, protected := range protectedDefaultRoutes(prefix.Addr().Is4()) {
appendRoute(protected)
}
continue
}
appendRoute(prefix)
}
return routes, nil
}
// protectedDefaultRoutes splits a full default route into eight more-specific
// prefixes covering everything but the zero /8, so the system's real default
// route stays in place for outbound interface discovery (darwin parity).
func protectedDefaultRoutes(ipv4 bool) []netip.Prefix {
routes := make([]netip.Prefix, 0, 8)
for i := 0; i < 8; i++ {
if ipv4 {
var address [4]byte
address[0] = 1 << i
routes = append(routes, netip.PrefixFrom(netip.AddrFrom4(address), 8-i))
} else {
var address [16]byte
address[0] = 1 << i
routes = append(routes, netip.PrefixFrom(netip.AddrFrom16(address), 8-i))
}
}
return routes
}
// execRoute writes one RTM message to a routing socket. fib >= 0 targets that
// routing table via SO_SETFIB on the routing socket (what route(8) -fib
// does); fib -1 leaves the process default table. An invalid gateway produces
// an interface route pinned to interfaceIndex instead of a gateway route.
func execRoute(fib int, messageType int, interfaceIndex int, destination netip.Prefix, gateway netip.Addr) error {
message := route.RouteMessage{
Type: messageType,
Version: unix.RTM_VERSION,
Flags: unix.RTF_STATIC | unix.RTF_GATEWAY,
Seq: 1,
}
if messageType == unix.RTM_ADD {
message.Flags |= unix.RTF_UP
}
var gatewayAddr route.Addr
switch {
case !gateway.IsValid():
message.Flags &^= unix.RTF_GATEWAY
message.Index = interfaceIndex
gatewayAddr = &route.LinkAddr{Index: interfaceIndex}
case gateway.Is4():
gatewayAddr = &route.Inet4Addr{IP: gateway.As4()}
default:
gatewayAddr = &route.Inet6Addr{IP: gateway.As16()}
}
if destination.Addr().Is4() {
message.Addrs = []route.Addr{
unix.RTAX_DST: &route.Inet4Addr{IP: destination.Addr().As4()},
unix.RTAX_NETMASK: &route.Inet4Addr{IP: prefixMask4(destination.Bits())},
unix.RTAX_GATEWAY: gatewayAddr,
}
} else {
message.Addrs = []route.Addr{
unix.RTAX_DST: &route.Inet6Addr{IP: destination.Addr().As16()},
unix.RTAX_NETMASK: &route.Inet6Addr{IP: prefixMask6(destination.Bits())},
unix.RTAX_GATEWAY: gatewayAddr,
}
}
request, err := message.Marshal()
if err != nil {
return err
}
fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
if err != nil {
return err
}
defer unix.Close(fd)
if fib >= 0 {
if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_SETFIB, fib); err != nil {
return err
}
}
_, err = unix.Write(fd, request)
return err
}
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
if fixedName != "" {
iface, err := net.InterfaceByName(fixedName)
if err != nil {
return nil, err
}
if iface.Index == tunIndex {
return nil, errors.New("outbound interface cannot be the TUN interface")
}
return iface, nil
if fixedName == "" {
return nil, errors.New("automatic outbound interface selection is not supported on this platform")
}
physical, err := physicalDefaultRoutes(tunIndex, 0)
iface, err := net.InterfaceByName(fixedName)
if err != nil {
return nil, err
}
for _, family := range []int{unix.AF_INET, unix.AF_INET6} {
for _, route := range physical {
if route.family == family {
return route.iface, nil
}
}
}
return nil, errors.New("default route not found")
}
// physicalRoute describes one physical default route: the interface it
// leaves through, its gateway, and the connected prefix that makes the
// gateway resolvable.
type physicalRoute struct {
family int
iface *net.Interface
gateway netip.Addr
connected netip.Prefix
}
// physicalDefaultRoutes scans the default routing table for default routes
// that do not go through the TUN interface, at most one per address family
// (the first usable one wins, matching the darwin implementation's
// preference order). A non-zero onlyIndex restricts the scan to that
// interface, for the fixed-name mode of autoOutboundsInterface.
func physicalDefaultRoutes(tunIndex int, onlyIndex int) ([]physicalRoute, error) {
rib, err := route.FetchRIB(unix.AF_UNSPEC, route.RIBTypeRoute, 0)
if err != nil {
return nil, err
}
messages, err := route.ParseRIB(route.RIBTypeRoute, rib)
if err != nil {
return nil, err
}
found := make([]physicalRoute, 0, 2)
seen := make(map[int]bool)
for _, message := range messages {
routeMessage, ok := message.(*route.RouteMessage)
if !ok || routeMessage.Index == tunIndex {
continue
}
if onlyIndex != 0 && routeMessage.Index != onlyIndex {
continue
}
if routeMessage.Flags&unix.RTF_UP == 0 || routeMessage.Flags&unix.RTF_GATEWAY == 0 {
continue
}
family, ok := defaultRouteFamily(routeMessage)
if !ok || seen[family] {
continue
}
iface, err := usableInterface(routeMessage.Index)
if err != nil {
continue
}
gatewayAddr, ok := routeAddrToNetip(routeMessage.Addrs[unix.RTAX_GATEWAY])
if !ok {
continue
}
connected, err := connectedPrefix(iface, gatewayAddr)
if err != nil {
continue
}
seen[family] = true
found = append(found, physicalRoute{
family: family,
iface: iface,
gateway: gatewayAddr,
connected: connected,
})
}
if len(found) == 0 {
return nil, errors.New("default route not found")
}
return found, nil
}
// defaultRouteFamily reports the address family of a RIB message that
// represents a true default route (unspecified destination, zero mask).
func defaultRouteFamily(message *route.RouteMessage) (int, bool) {
if len(message.Addrs) <= unix.RTAX_NETMASK {
return 0, false
}
switch destination := message.Addrs[unix.RTAX_DST].(type) {
case *route.Inet4Addr:
mask, ok := message.Addrs[unix.RTAX_NETMASK].(*route.Inet4Addr)
if !ok || destination.IP != netip.IPv4Unspecified().As4() {
return 0, false
}
ones, bits := net.IPMask(mask.IP[:]).Size()
return unix.AF_INET, ones == 0 && bits == 32
case *route.Inet6Addr:
mask, ok := message.Addrs[unix.RTAX_NETMASK].(*route.Inet6Addr)
if !ok || destination.IP != netip.IPv6Unspecified().As16() {
return 0, false
}
ones, bits := net.IPMask(mask.IP[:]).Size()
return unix.AF_INET6, ones == 0 && bits == 128
default:
return 0, false
}
}
func usableInterface(index int) (*net.Interface, error) {
iface, err := net.InterfaceByIndex(index)
if err != nil {
return nil, err
}
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
return nil, errors.New("default route interface is not usable")
if iface.Index == tunIndex {
return nil, errors.New("outbound interface cannot be the TUN interface")
}
return iface, nil
}
func routeAddrToNetip(addr route.Addr) (netip.Addr, bool) {
switch typed := addr.(type) {
case *route.Inet4Addr:
return netip.AddrFrom4(typed.IP), true
case *route.Inet6Addr:
return netip.AddrFrom16(typed.IP), true
default:
return netip.Addr{}, false
}
}
// checkEscapeFib verifies the system can host the escape routing table at
// all: FIBs are a boot-time resource on FreeBSD.
func checkEscapeFib() error {
fibs, err := unix.SysctlUint32("net.fibs")
if err != nil {
return xerrors.New("failed to read net.fibs").Base(err)
}
if fibs < 2 {
return errors.New("automatic outbound interface on FreeBSD needs a second routing table: add net.fibs=2 to /boot/loader.conf and reboot")
}
current, err := unix.SysctlUint32("net.my_fibnum")
if err != nil {
return xerrors.New("failed to read net.my_fibnum").Base(err)
}
if current == escapeFib {
return errors.New("xray runs inside routing table 1, which is reserved as the escape table; start it in another FIB")
}
return nil
}
// syncEscapeFib mirrors the physical default routes (and the connected
// prefixes their gateways resolve through) into the escape FIB, replacing
// whatever mirror a previous call installed. On discovery failure the old
// mirror is kept, since a stale escape route beats none during a transient
// route flap.
func (t *FreeBSDTun) syncEscapeFib() error {
var onlyIndex int
if t.options.AutoOutboundsInterface != "" && updater != nil {
if iface := updater.Get(); iface != nil {
onlyIndex = iface.Index
}
}
physical, err := physicalDefaultRoutes(t.tunIndex, onlyIndex)
if err != nil {
return err
}
desired := make([]escapeRoute, 0, 2*len(physical))
for _, p := range physical {
desired = append(desired,
escapeRoute{prefix: p.connected, ifIndex: p.iface.Index},
escapeRoute{prefix: defaultPrefix(p.family), ifIndex: p.iface.Index, gateway: p.gateway},
)
}
t.escapeMu.Lock()
defer t.escapeMu.Unlock()
// The route monitor hears our own escape FIB writes too; rewriting an
// unchanged mirror on every wake-up would ping-pong forever.
if slices.Equal(t.escapeRoutes, desired) {
return nil
}
t.unsetEscapeFibLocked()
for _, entry := range desired {
err := execRoute(escapeFib, unix.RTM_ADD, entry.ifIndex, entry.prefix, entry.gateway)
if err != nil && !errors.Is(err, unix.EEXIST) {
return xerrors.New("failed to add escape route ", entry.prefix).Base(err)
}
t.escapeRoutes = append(t.escapeRoutes, entry)
}
return nil
}
func (t *FreeBSDTun) unsetEscapeFib() {
t.escapeMu.Lock()
defer t.escapeMu.Unlock()
t.unsetEscapeFibLocked()
}
func (t *FreeBSDTun) unsetEscapeFibLocked() {
for i := len(t.escapeRoutes) - 1; i >= 0; i-- {
entry := t.escapeRoutes[i]
err := execRoute(escapeFib, unix.RTM_DELETE, entry.ifIndex, entry.prefix, entry.gateway)
if err != nil && !errors.Is(err, unix.ESRCH) {
xerrors.LogInfoInner(context.Background(), err, "[tun] failed to delete escape route ", entry.prefix)
}
}
t.escapeRoutes = nil
}
func defaultPrefix(family int) netip.Prefix {
if family == unix.AF_INET {
return netip.PrefixFrom(netip.IPv4Unspecified(), 0)
}
return netip.PrefixFrom(netip.IPv6Unspecified(), 0)
}
// connectedPrefix finds the interface's address prefix containing the
// gateway, which the escape FIB needs as an interface route so the mirrored
// default route's gateway is resolvable there.
func connectedPrefix(iface *net.Interface, gateway netip.Addr) (netip.Prefix, error) {
addrs, err := iface.Addrs()
if err != nil {
return netip.Prefix{}, err
}
for _, addr := range addrs {
ipNet, ok := addr.(*net.IPNet)
if !ok {
continue
}
ip, ok := netip.AddrFromSlice(ipNet.IP)
if !ok {
continue
}
ip = ip.Unmap()
ones, _ := ipNet.Mask.Size()
prefix := netip.PrefixFrom(ip, ones).Masked()
if prefix.Contains(gateway.WithZone("").Unmap()) {
return prefix, nil
}
}
return netip.Prefix{}, errors.New("no connected prefix contains the gateway")
}
-150
View File
@@ -1,150 +0,0 @@
//go:build freebsd
package tun
import (
"net/netip"
"testing"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
)
func TestSelectFreeBSDGatewayDefault(t *testing.T) {
gateway, local, err := selectFreeBSDGateway(nil)
if err != nil {
t.Fatal(err)
}
if gateway != netip.MustParsePrefix(defaultFreeBSDGateway) {
t.Fatal("expected default gateway, got ", gateway)
}
if local != netip.MustParseAddr("169.254.10.2") {
t.Fatal("wrong local address: ", local)
}
}
func TestSelectFreeBSDGatewayPicksFirstIPv4(t *testing.T) {
gateway, local, err := selectFreeBSDGateway([]string{"fd00::1/64", "10.0.0.1/30", "10.9.9.9/24"})
if err != nil {
t.Fatal(err)
}
if gateway != netip.MustParsePrefix("10.0.0.1/30") {
t.Fatal("wrong gateway: ", gateway)
}
if local != netip.MustParseAddr("10.0.0.2") {
t.Fatal("wrong local address: ", local)
}
}
func TestSelectFreeBSDGatewayRequiresIPv4(t *testing.T) {
if _, _, err := selectFreeBSDGateway([]string{"fd00::1/64"}); err == nil {
t.Fatal("expected error when no IPv4 gateway is configured")
}
}
func TestSelectFreeBSDGatewayRejectsGarbage(t *testing.T) {
if _, _, err := selectFreeBSDGateway([]string{"not-a-prefix"}); err == nil {
t.Fatal("expected error for invalid gateway")
}
}
func TestSelectFreeBSDGatewayRejectsFullPrefix(t *testing.T) {
// 10.0.0.255/30: the "next" local address falls outside the prefix
if _, _, err := selectFreeBSDGateway([]string{"10.0.0.255/30"}); err == nil {
t.Fatal("expected error when no usable local address follows the gateway")
}
}
func TestNextLocalIPv4(t *testing.T) {
local, ok := nextLocalIPv4(netip.MustParsePrefix("169.254.10.1/30"))
if !ok || local != netip.MustParseAddr("169.254.10.2") {
t.Fatal("wrong local address: ", local)
}
}
func TestBuildSystemRoutesSplitsDefault(t *testing.T) {
routes, err := buildSystemRoutes([]string{"0.0.0.0/0"})
if err != nil {
t.Fatal(err)
}
expected := []string{
"1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5",
"16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1",
}
if len(routes) != len(expected) {
t.Fatal("expected ", len(expected), " routes, got ", routes)
}
for i, want := range expected {
if routes[i] != netip.MustParsePrefix(want) {
t.Fatal("route ", i, ": expected ", want, ", got ", routes[i])
}
}
}
func TestBuildSystemRoutesSplitsDefaultIPv6(t *testing.T) {
routes, err := buildSystemRoutes([]string{"::/0"})
if err != nil {
t.Fatal(err)
}
if len(routes) != 8 || routes[7] != netip.MustParsePrefix("8000::/1") {
t.Fatal("unexpected v6 split: ", routes)
}
}
func TestBuildSystemRoutesDeduplicates(t *testing.T) {
routes, err := buildSystemRoutes([]string{"10.0.0.0/8", "10.1.2.3/8", "0.0.0.0/0", "0.0.0.0/0"})
if err != nil {
t.Fatal(err)
}
if len(routes) != 9 { // 10.0.0.0/8 once + 8 splits once
t.Fatal("expected 9 routes, got ", routes)
}
}
func TestBuildSystemRoutesRejectsGarbage(t *testing.T) {
if _, err := buildSystemRoutes([]string{"10.0.0.0/33"}); err == nil {
t.Fatal("expected error for invalid route")
}
}
func routeMessage(dst, mask route.Addr) *route.RouteMessage {
addrs := make([]route.Addr, unix.RTAX_NETMASK+1)
addrs[unix.RTAX_DST] = dst
addrs[unix.RTAX_NETMASK] = mask
return &route.RouteMessage{Addrs: addrs}
}
func TestDefaultRouteFamilyMatchesIPv4Default(t *testing.T) {
family, ok := defaultRouteFamily(routeMessage(&route.Inet4Addr{}, &route.Inet4Addr{}))
if !ok || family != unix.AF_INET {
t.Fatal("expected IPv4 default route match")
}
}
func TestDefaultRouteFamilyMatchesIPv6Default(t *testing.T) {
family, ok := defaultRouteFamily(routeMessage(&route.Inet6Addr{}, &route.Inet6Addr{}))
if !ok || family != unix.AF_INET6 {
t.Fatal("expected IPv6 default route match")
}
}
func TestDefaultRouteFamilyRejectsNonDefault(t *testing.T) {
if _, ok := defaultRouteFamily(routeMessage(
&route.Inet4Addr{IP: [4]byte{10, 0, 0, 0}},
&route.Inet4Addr{IP: [4]byte{255, 0, 0, 0}},
)); ok {
t.Fatal("non-default destination must not match")
}
if _, ok := defaultRouteFamily(routeMessage(
&route.Inet4Addr{},
&route.Inet4Addr{IP: [4]byte{255, 0, 0, 0}},
)); ok {
t.Fatal("non-zero mask must not match")
}
}
func TestDefaultRouteFamilyRejectsShortAddrs(t *testing.T) {
if _, ok := defaultRouteFamily(&route.RouteMessage{}); ok {
t.Fatal("message without addresses must not match")
}
}
+3 -3
View File
@@ -50,7 +50,7 @@ var _ GVisorDevice = (*WindowsTun)(nil)
// interface with the same name exist, it tried to be reused.
func NewTun(options *Config) (Tun, error) {
// instantiate wintun adapter
adapter, err := open(options.Name, options.Desc)
adapter, err := open(options.Name)
if err != nil {
return nil, err
}
@@ -73,12 +73,12 @@ func NewTun(options *Config) (Tun, error) {
return tun, nil
}
func open(name, desc string) (*wintun.Adapter, error) {
func open(name string) (*wintun.Adapter, error) {
// generate a deterministic GUID from the adapter name
id := md5.Sum([]byte(name))
guid := (*windows.GUID)(unsafe.Pointer(&id[0]))
// try to create adapter anew
adapter, err := wintun.CreateAdapter(name, desc, guid)
adapter, err := wintun.CreateAdapter(name, "Xray", guid)
if err == nil {
return adapter, nil
}
+8 -11
View File
@@ -5,7 +5,6 @@ import (
"crypto/hmac"
"crypto/sha256"
"hash/crc64"
"sync/atomic"
"time"
"github.com/xtls/xray-core/common"
@@ -219,17 +218,10 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
return nil
}
var enablePadding atomic.Bool
var enablePadding = false
func shouldEnablePadding(s protocol.SecurityType) bool {
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
return enablePadding || s == protocol.SecurityType_AES128_GCM || s == protocol.SecurityType_CHACHA20_POLY1305 || s == protocol.SecurityType_AUTO
}
func init() {
@@ -237,5 +229,10 @@ func init() {
return New(ctx, config.(*Config))
}))
platform.RegisterEnvReload(reloadEnvSettings)
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
paddingValue := platform.NewEnvFlag(platform.UseVmessPadding).GetValue(func() string { return defaultFlagValue })
if paddingValue != defaultFlagValue {
enablePadding = true
}
}
+25 -61
View File
@@ -5,10 +5,9 @@ import (
"fmt"
gonet "net"
"net/netip"
"reflect"
reflect "reflect"
"strings"
"sync"
"time"
"golang.zx2c4.com/wireguard/tun"
@@ -31,11 +30,6 @@ import (
"golang.zx2c4.com/wireguard/device"
)
type entry struct {
got []net.IP
time time.Time
}
type Handler struct {
conf *DeviceConfig
policyManager policy.Manager
@@ -49,11 +43,6 @@ type Handler struct {
tnet *Net
dev *device.Device
mu sync.Mutex
// TODO: cache cleanup loop
local bool
cache map[string]entry
cacheMu sync.Mutex
}
func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
@@ -109,20 +98,6 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
return nil, err
}
local := false
dns := conf.DNS
if len(dns) == 0 {
dns = []string{"1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"}
}
if len(dns) == 1 && dns[0] == "local" {
local = true
dns = nil
}
dnses := make([]netip.Addr, 0, len(dns))
for _, dns := range dns {
dnses = append(dnses, netip.MustParseAddr(dns))
}
kernelTunSupported, err := KernelTunSupported()
if err != nil {
errors.LogWarningInner(context.Background(), err, "Failed to check kernel TUN support")
@@ -131,10 +106,10 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
var tnet *Net
if !conf.NoKernelTun && kernelTunSupported {
errors.LogWarning(context.Background(), "Using kernel TUN")
tun, tnet, err = createKernelTun(localAddresses, dnses, int(conf.Mtu))
tun, tnet, err = createKernelTun(localAddresses, []netip.Addr{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("1.0.0.1"), netip.MustParseAddr("2606:4700:4700::1111"), netip.MustParseAddr("2606:4700:4700::1001")}, int(conf.Mtu))
} else {
errors.LogWarning(context.Background(), "Using gVisor TUN")
tun, tnet, _, err = CreateNetTUN(localAddresses, dnses, int(conf.Mtu), true)
tun, tnet, _, err = CreateNetTUN(localAddresses, []netip.Addr{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("1.0.0.1"), netip.MustParseAddr("2606:4700:4700::1111"), netip.MustParseAddr("2606:4700:4700::1001")}, int(conf.Mtu), true)
}
if err != nil {
return nil, err
@@ -151,9 +126,6 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
tun: tun,
tnet: tnet,
local: local,
cache: make(map[string]entry),
}, nil
}
@@ -166,9 +138,14 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
}
ob.Name = "wireguard"
ob.CanSpliceCopy = 3
dialer.SetOutboundGateway(ctx, ob)
if err := h.init(ctx); err != nil {
if h.dev == nil {
if err := h.init(ctx); err != nil {
return err
}
}
if err := h.dev.Up(); err != nil {
return err
}
@@ -281,11 +258,8 @@ 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 h.dev.Up()
return nil
}
resolveFunc := h.resolveLocal
listenFunc := func() (net.PacketConn, error) {
@@ -371,34 +345,31 @@ func (h *Handler) init(ctx context.Context) error {
}
func (h *Handler) resolveLocal(host string) (net.IP, error) {
return h.resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, uint32, error) {
return h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
return resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, error) {
ips, _, err := h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
return ips, err
})
}
func (h *Handler) resolveRemote(host string) (net.IP, error) {
return h.resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, uint32, error) {
if h.local {
return h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
return resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, error) {
addrs, err := h.tnet.LookupHost(host)
if err != nil {
return nil, err
}
return h.tnet.LookupHost(host)
ips := make([]net.IP, 0, len(addrs))
for _, addr := range addrs {
ips = append(ips, net.ParseIP(addr))
}
return ips, nil
})
}
func (h *Handler) resolveDomain(host string, strategy DeviceConfig_DomainStrategy, lookupIP func(host string) ([]net.IP, uint32, error)) (net.IP, error) {
func resolveDomain(host string, strategy DeviceConfig_DomainStrategy, lookupIP func(host string) ([]net.IP, error)) (net.IP, error) {
if ip := net.ParseIP(host); ip != nil {
return ip, nil
}
h.cacheMu.Lock()
if entry, ok := h.cache[host]; ok {
if time.Now().Before(entry.time) {
h.cacheMu.Unlock()
return entry.got[dice.Roll(len(entry.got))], nil
}
delete(h.cache, host)
}
h.cacheMu.Unlock()
ips, ttl, err := lookupIP(host)
ips, err := lookupIP(host)
if err != nil {
return nil, err
}
@@ -438,13 +409,6 @@ func (h *Handler) resolveDomain(host string, strategy DeviceConfig_DomainStrateg
if len(got) == 0 {
return nil, dns.ErrEmptyResponse
}
entry := entry{
got: got,
time: time.Now().Add(time.Duration(ttl) * time.Second),
}
h.cacheMu.Lock()
h.cache[host] = entry
h.cacheMu.Unlock()
return got[dice.Roll(len(got))], nil
}
+2 -12
View File
@@ -164,7 +164,6 @@ type DeviceConfig struct {
DomainStrategy DeviceConfig_DomainStrategy `protobuf:"varint,7,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.proxy.wireguard.DeviceConfig_DomainStrategy" json:"domain_strategy,omitempty"`
IsClient bool `protobuf:"varint,8,opt,name=is_client,json=isClient,proto3" json:"is_client,omitempty"`
NoKernelTun bool `protobuf:"varint,9,opt,name=no_kernel_tun,json=noKernelTun,proto3" json:"no_kernel_tun,omitempty"`
DNS []string `protobuf:"bytes,10,rep,name=DNS,proto3" json:"DNS,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -262,13 +261,6 @@ func (x *DeviceConfig) GetNoKernelTun() bool {
return false
}
func (x *DeviceConfig) GetDNS() []string {
if x != nil {
return x.DNS
}
return nil
}
var File_proxy_wireguard_config_proto protoreflect.FileDescriptor
const file_proxy_wireguard_config_proto_rawDesc = "" +
@@ -283,7 +275,7 @@ const file_proxy_wireguard_config_proto_rawDesc = "" +
"\n" +
"keep_alive\x18\x04 \x01(\tR\tkeepAlive\x12\x1f\n" +
"\vallowed_ips\x18\x05 \x03(\tR\n" +
"allowedIps\"\xee\x03\n" +
"allowedIps\"\xdc\x03\n" +
"\fDeviceConfig\x12\x1d\n" +
"\n" +
"secret_key\x18\x01 \x01(\tR\tsecretKey\x12\x1a\n" +
@@ -294,9 +286,7 @@ const file_proxy_wireguard_config_proto_rawDesc = "" +
"\breserved\x18\x06 \x01(\fR\breserved\x12Z\n" +
"\x0fdomain_strategy\x18\a \x01(\x0e21.xray.proxy.wireguard.DeviceConfig.DomainStrategyR\x0edomainStrategy\x12\x1b\n" +
"\tis_client\x18\b \x01(\bR\bisClient\x12\"\n" +
"\rno_kernel_tun\x18\t \x01(\bR\vnoKernelTun\x12\x10\n" +
"\x03DNS\x18\n" +
" \x03(\tR\x03DNS\"\\\n" +
"\rno_kernel_tun\x18\t \x01(\bR\vnoKernelTun\"\\\n" +
"\x0eDomainStrategy\x12\f\n" +
"\bFORCE_IP\x10\x00\x12\r\n" +
"\tFORCE_IP4\x10\x01\x12\r\n" +
-1
View File
@@ -34,5 +34,4 @@ message DeviceConfig {
DomainStrategy domain_strategy = 7;
bool is_client = 8;
bool no_kernel_tun = 9;
repeated string DNS = 10;
}
+9 -12
View File
@@ -248,7 +248,7 @@ var (
errTimeout = errors.New("i/o timeout")
)
func (net *Net) LookupHost(host string) (addrs []net.IP, ttl uint32, err error) {
func (net *Net) LookupHost(host string) (addrs []string, err error) {
return net.LookupContextHost(context.Background(), host)
}
@@ -567,9 +567,9 @@ func (tnet *Net) tryOneName(ctx context.Context, name string, qtype dnsmessage.T
return dnsmessage.Parser{}, "", lastErr
}
func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]net.IP, uint32, error) {
func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string, error) {
if host == "" || (!tnet.hasV6 && !tnet.hasV4) {
return nil, 0, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
}
zlen := len(host)
if strings.IndexByte(host, ':') != -1 {
@@ -578,11 +578,11 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]net.IP,
}
}
if ip, err := netip.ParseAddr(host[:zlen]); err == nil {
return []net.IP{ip.AsSlice()}, 0, nil
return []string{ip.String()}, nil
}
if !isDomainName(host) {
return nil, 0, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
}
type result struct {
p dnsmessage.Parser
@@ -611,7 +611,6 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]net.IP,
lane <- result{p, server, err}
}()
}
ttl := uint32(300)
for l := 0; l < lanes; l++ {
result := <-lane
if result.error != nil {
@@ -645,7 +644,6 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]net.IP,
}
break loop
}
ttl = min(ttl, h.TTL)
addrsV4 = append(addrsV4, netip.AddrFrom4(a.A))
case dnsmessage.TypeAAAA:
@@ -658,7 +656,6 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]net.IP,
}
break loop
}
ttl = min(ttl, h.TTL)
addrsV6 = append(addrsV6, netip.AddrFrom16(aaaa.AAAA))
default:
@@ -683,11 +680,11 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]net.IP,
}
if len(addrs) == 0 && lastErr != nil {
return nil, 0, lastErr
return nil, lastErr
}
ips := make([]net.IP, 0, len(addrs))
saddrs := make([]string, 0, len(addrs))
for _, ip := range addrs {
ips = append(ips, ip.AsSlice())
saddrs = append(saddrs, ip.String())
}
return ips, ttl, nil
return saddrs, nil
}
+4 -14
View File
@@ -27,10 +27,9 @@ type task struct {
}
var (
conns chan *websocket.Conn
server *http.Server
currentAddr string
mu sync.Mutex
conns chan *websocket.Conn
server *http.Server
mu sync.Mutex
)
var upgrader = &websocket.Upgrader{
@@ -48,13 +47,8 @@ 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 {
@@ -66,7 +60,6 @@ func Reload() {
}
conns = nil
}
currentAddr = addr
if addr != "" {
token := uuid.New()
csrfToken := token.String()
@@ -227,8 +220,5 @@ func CheckOK(conn *websocket.Conn) error {
}
func init() {
platform.RegisterEnvReload(func() error {
Reload()
return nil
})
Reload()
}
+28 -66
View File
@@ -443,26 +443,22 @@ func (x *UdpHop) GetIntervalMax() int64 {
}
type QuicParams struct {
state protoimpl.MessageState `protogen:"open.v1"`
Congestion string `protobuf:"bytes,1,opt,name=congestion,proto3" json:"congestion,omitempty"`
BbrProfile string `protobuf:"bytes,2,opt,name=bbr_profile,json=bbrProfile,proto3" json:"bbr_profile,omitempty"`
BrutalUp uint64 `protobuf:"varint,3,opt,name=brutal_up,json=brutalUp,proto3" json:"brutal_up,omitempty"`
BrutalDown uint64 `protobuf:"varint,4,opt,name=brutal_down,json=brutalDown,proto3" json:"brutal_down,omitempty"`
BrutalDisableLossCompensation bool `protobuf:"varint,5,opt,name=brutal_disable_loss_compensation,json=brutalDisableLossCompensation,proto3" json:"brutal_disable_loss_compensation,omitempty"`
UdpHop *UdpHop `protobuf:"bytes,6,opt,name=udp_hop,json=udpHop,proto3" json:"udp_hop,omitempty"`
InitStreamReceiveWindow uint64 `protobuf:"varint,7,opt,name=init_stream_receive_window,json=initStreamReceiveWindow,proto3" json:"init_stream_receive_window,omitempty"`
MaxStreamReceiveWindow uint64 `protobuf:"varint,8,opt,name=max_stream_receive_window,json=maxStreamReceiveWindow,proto3" json:"max_stream_receive_window,omitempty"`
InitConnReceiveWindow uint64 `protobuf:"varint,9,opt,name=init_conn_receive_window,json=initConnReceiveWindow,proto3" json:"init_conn_receive_window,omitempty"`
MaxConnReceiveWindow uint64 `protobuf:"varint,10,opt,name=max_conn_receive_window,json=maxConnReceiveWindow,proto3" json:"max_conn_receive_window,omitempty"`
MaxIdleTimeout int64 `protobuf:"varint,11,opt,name=max_idle_timeout,json=maxIdleTimeout,proto3" json:"max_idle_timeout,omitempty"`
KeepAlivePeriod int64 `protobuf:"varint,12,opt,name=keep_alive_period,json=keepAlivePeriod,proto3" json:"keep_alive_period,omitempty"`
DisablePathMtuDiscovery bool `protobuf:"varint,13,opt,name=disable_path_mtu_discovery,json=disablePathMtuDiscovery,proto3" json:"disable_path_mtu_discovery,omitempty"`
DisableChromeParrot bool `protobuf:"varint,14,opt,name=disable_chrome_parrot,json=disableChromeParrot,proto3" json:"disable_chrome_parrot,omitempty"`
DisableGSO bool `protobuf:"varint,15,opt,name=disableGSO,proto3" json:"disableGSO,omitempty"`
MaxIncomingStreams int64 `protobuf:"varint,16,opt,name=max_incoming_streams,json=maxIncomingStreams,proto3" json:"max_incoming_streams,omitempty"`
DisableStatelessReset bool `protobuf:"varint,17,opt,name=disable_stateless_reset,json=disableStatelessReset,proto3" json:"disable_stateless_reset,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Congestion string `protobuf:"bytes,1,opt,name=congestion,proto3" json:"congestion,omitempty"`
BbrProfile string `protobuf:"bytes,2,opt,name=bbr_profile,json=bbrProfile,proto3" json:"bbr_profile,omitempty"`
BrutalUp uint64 `protobuf:"varint,3,opt,name=brutal_up,json=brutalUp,proto3" json:"brutal_up,omitempty"`
BrutalDown uint64 `protobuf:"varint,4,opt,name=brutal_down,json=brutalDown,proto3" json:"brutal_down,omitempty"`
UdpHop *UdpHop `protobuf:"bytes,5,opt,name=udp_hop,json=udpHop,proto3" json:"udp_hop,omitempty"`
InitStreamReceiveWindow uint64 `protobuf:"varint,6,opt,name=init_stream_receive_window,json=initStreamReceiveWindow,proto3" json:"init_stream_receive_window,omitempty"`
MaxStreamReceiveWindow uint64 `protobuf:"varint,7,opt,name=max_stream_receive_window,json=maxStreamReceiveWindow,proto3" json:"max_stream_receive_window,omitempty"`
InitConnReceiveWindow uint64 `protobuf:"varint,8,opt,name=init_conn_receive_window,json=initConnReceiveWindow,proto3" json:"init_conn_receive_window,omitempty"`
MaxConnReceiveWindow uint64 `protobuf:"varint,9,opt,name=max_conn_receive_window,json=maxConnReceiveWindow,proto3" json:"max_conn_receive_window,omitempty"`
MaxIdleTimeout int64 `protobuf:"varint,10,opt,name=max_idle_timeout,json=maxIdleTimeout,proto3" json:"max_idle_timeout,omitempty"`
KeepAlivePeriod int64 `protobuf:"varint,11,opt,name=keep_alive_period,json=keepAlivePeriod,proto3" json:"keep_alive_period,omitempty"`
DisablePathMtuDiscovery bool `protobuf:"varint,12,opt,name=disable_path_mtu_discovery,json=disablePathMtuDiscovery,proto3" json:"disable_path_mtu_discovery,omitempty"`
MaxIncomingStreams int64 `protobuf:"varint,13,opt,name=max_incoming_streams,json=maxIncomingStreams,proto3" json:"max_incoming_streams,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *QuicParams) Reset() {
@@ -523,13 +519,6 @@ func (x *QuicParams) GetBrutalDown() uint64 {
return 0
}
func (x *QuicParams) GetBrutalDisableLossCompensation() bool {
if x != nil {
return x.BrutalDisableLossCompensation
}
return false
}
func (x *QuicParams) GetUdpHop() *UdpHop {
if x != nil {
return x.UdpHop
@@ -586,20 +575,6 @@ func (x *QuicParams) GetDisablePathMtuDiscovery() bool {
return false
}
func (x *QuicParams) GetDisableChromeParrot() bool {
if x != nil {
return x.DisableChromeParrot
}
return false
}
func (x *QuicParams) GetDisableGSO() bool {
if x != nil {
return x.DisableGSO
}
return false
}
func (x *QuicParams) GetMaxIncomingStreams() int64 {
if x != nil {
return x.MaxIncomingStreams
@@ -607,13 +582,6 @@ func (x *QuicParams) GetMaxIncomingStreams() int64 {
return 0
}
func (x *QuicParams) GetDisableStatelessReset() bool {
if x != nil {
return x.DisableStatelessReset
}
return false
}
type ProxyConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"`
@@ -1052,7 +1020,7 @@ const file_transport_internet_config_proto_rawDesc = "" +
"\x06UdpHop\x12\x14\n" +
"\x05ports\x18\x01 \x03(\rR\x05ports\x12!\n" +
"\finterval_min\x18\x02 \x01(\x03R\vintervalMin\x12!\n" +
"\finterval_max\x18\x03 \x01(\x03R\vintervalMax\"\xc7\x06\n" +
"\finterval_max\x18\x03 \x01(\x03R\vintervalMax\"\xf2\x04\n" +
"\n" +
"QuicParams\x12\x1e\n" +
"\n" +
@@ -1062,23 +1030,17 @@ const file_transport_internet_config_proto_rawDesc = "" +
"bbrProfile\x12\x1b\n" +
"\tbrutal_up\x18\x03 \x01(\x04R\bbrutalUp\x12\x1f\n" +
"\vbrutal_down\x18\x04 \x01(\x04R\n" +
"brutalDown\x12G\n" +
" brutal_disable_loss_compensation\x18\x05 \x01(\bR\x1dbrutalDisableLossCompensation\x128\n" +
"\audp_hop\x18\x06 \x01(\v2\x1f.xray.transport.internet.UdpHopR\x06udpHop\x12;\n" +
"\x1ainit_stream_receive_window\x18\a \x01(\x04R\x17initStreamReceiveWindow\x129\n" +
"\x19max_stream_receive_window\x18\b \x01(\x04R\x16maxStreamReceiveWindow\x127\n" +
"\x18init_conn_receive_window\x18\t \x01(\x04R\x15initConnReceiveWindow\x125\n" +
"\x17max_conn_receive_window\x18\n" +
" \x01(\x04R\x14maxConnReceiveWindow\x12(\n" +
"\x10max_idle_timeout\x18\v \x01(\x03R\x0emaxIdleTimeout\x12*\n" +
"\x11keep_alive_period\x18\f \x01(\x03R\x0fkeepAlivePeriod\x12;\n" +
"\x1adisable_path_mtu_discovery\x18\r \x01(\bR\x17disablePathMtuDiscovery\x122\n" +
"\x15disable_chrome_parrot\x18\x0e \x01(\bR\x13disableChromeParrot\x12\x1e\n" +
"\n" +
"disableGSO\x18\x0f \x01(\bR\n" +
"disableGSO\x120\n" +
"\x14max_incoming_streams\x18\x10 \x01(\x03R\x12maxIncomingStreams\x126\n" +
"\x17disable_stateless_reset\x18\x11 \x01(\bR\x15disableStatelessReset\"Q\n" +
"brutalDown\x128\n" +
"\audp_hop\x18\x05 \x01(\v2\x1f.xray.transport.internet.UdpHopR\x06udpHop\x12;\n" +
"\x1ainit_stream_receive_window\x18\x06 \x01(\x04R\x17initStreamReceiveWindow\x129\n" +
"\x19max_stream_receive_window\x18\a \x01(\x04R\x16maxStreamReceiveWindow\x127\n" +
"\x18init_conn_receive_window\x18\b \x01(\x04R\x15initConnReceiveWindow\x125\n" +
"\x17max_conn_receive_window\x18\t \x01(\x04R\x14maxConnReceiveWindow\x12(\n" +
"\x10max_idle_timeout\x18\n" +
" \x01(\x03R\x0emaxIdleTimeout\x12*\n" +
"\x11keep_alive_period\x18\v \x01(\x03R\x0fkeepAlivePeriod\x12;\n" +
"\x1adisable_path_mtu_discovery\x18\f \x01(\bR\x17disablePathMtuDiscovery\x120\n" +
"\x14max_incoming_streams\x18\r \x01(\x03R\x12maxIncomingStreams\"Q\n" +
"\vProxyConfig\x12\x10\n" +
"\x03tag\x18\x01 \x01(\tR\x03tag\x120\n" +
"\x13transportLayerProxy\x18\x02 \x01(\bR\x13transportLayerProxy\"\x93\x01\n" +
+9 -13
View File
@@ -75,19 +75,15 @@ message QuicParams {
string bbr_profile = 2;
uint64 brutal_up = 3;
uint64 brutal_down = 4;
bool brutal_disable_loss_compensation = 5;
UdpHop udp_hop = 6;
uint64 init_stream_receive_window = 7;
uint64 max_stream_receive_window = 8;
uint64 init_conn_receive_window = 9;
uint64 max_conn_receive_window = 10;
int64 max_idle_timeout = 11;
int64 keep_alive_period = 12;
bool disable_path_mtu_discovery = 13;
bool disable_chrome_parrot = 14;
bool disableGSO = 15;
int64 max_incoming_streams = 16;
bool disable_stateless_reset = 17;
UdpHop udp_hop = 5;
uint64 init_stream_receive_window = 6;
uint64 max_stream_receive_window = 7;
uint64 init_conn_receive_window = 8;
uint64 max_conn_receive_window = 9;
int64 max_idle_timeout = 10;
int64 keep_alive_period = 11;
bool disable_path_mtu_discovery = 12;
int64 max_incoming_streams = 13;
}
message ProxyConfig {
+4 -89
View File
@@ -7,7 +7,6 @@ import (
"net/netip"
"slices"
"strings"
"sync"
"time"
"github.com/pion/stun/v3"
@@ -16,67 +15,35 @@ import (
)
type realmConnClient struct {
wg sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
net.PacketConn
peer *net.UDPAddr
realmClient *Client
realmID string
stunServers []string
family Family
mapper *PortMapper
stunTimeout time.Duration
punchTimeout time.Duration
punchInterval time.Duration
}
func NewConnClient(config *Config, raw net.PacketConn) (net.PacketConn, error) {
ctx, cancel := context.WithCancel(context.Background())
family := Family_Dual
switch config.IPMode {
case "dual":
case "v4":
family = Family_V4
case "v6":
family = Family_V6
}
var mapper *PortMapper
if config.PortMapping != nil && config.PortMapping.Enabled {
var err error
start := time.Now()
mapper, err = NewPortMapper(context.Background(), raw.LocalAddr().(*net.UDPAddr).Port, PortMapConfig{Timeout: time.Duration(config.PortMapping.Timeout) * time.Second, Lifetime: time.Duration(config.PortMapping.Lifetime) * time.Second})
if err != nil {
errors.LogErrorInner(context.Background(), err, "[realm] [port mapping] [", raw.LocalAddr().(*net.UDPAddr).Port, "] init failed after ", time.Since(start))
} else {
errors.LogDebug(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] gateway ", mapper.GatewayType(), ", external ", mapper.ExternalAddr())
errors.LogDebug(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] init success with ", time.Since(start))
}
}
conn := &realmConnClient{
ctx: ctx,
cancel: cancel,
PacketConn: raw,
realmClient: NewClient(config.Scheme, config.Host, config.Port, config.Token, config.TlsConfig),
realmID: config.ID,
stunServers: config.StunServers,
family: family,
mapper: mapper,
stunTimeout: defaultSTUNTimeout,
punchTimeout: defaultPunchTimeout,
punchInterval: defaultPunchInterval,
}
return conn.getpeer()
}
func (c *realmConnClient) getpeer() (net.PacketConn, error) {
start := time.Now()
servers := resolveSTUNServers(c.PacketConn.LocalAddr().(*net.UDPAddr).IP, c.stunServers, c.family)
servers := resolveSTUNServers(c.PacketConn.LocalAddr().(*net.UDPAddr).IP, c.stunServers)
errors.LogDebug(context.Background(), "[realm] update stun servers ", servers, " with ", time.Since(start))
if len(servers) == 0 {
return nil, errors.New("empty locals")
@@ -103,7 +70,7 @@ func (c *realmConnClient) getpeer() (net.PacketConn, error) {
peers, _ := parseAddrPorts(resp.Addresses)
errors.LogDebug(context.Background(), "[realm] update peers ", peers)
filteredPeers, seen := candidatePunchAddrs(locals, peers, c.family)
filteredPeers, seen := candidatePunchAddrs(locals, peers)
errors.LogDebug(context.Background(), "[realm] filtered peers ", filteredPeers)
expandedPeers := expandSymmetricNATCandidates(filteredPeers, seen)
errors.LogDebug(context.Background(), "[realm] expanded peers ", expandedPeers)
@@ -119,11 +86,6 @@ func (c *realmConnClient) getpeer() (net.PacketConn, error) {
}
errors.LogDebug(context.Background(), "[realm] punch peer ", peer, " with ", time.Since(start))
if c.mapper != nil {
c.wg.Add(1)
go portMapLoop(c.ctx, c.mapper, c.wg.Done)
}
c.peer = peer
return c, nil
}
@@ -154,12 +116,10 @@ func (c *realmConnClient) discover(servers []*net.UDPAddr) []netip.AddrPort {
}
}
c.PacketConn.SetReadDeadline(time.Time{})
if c.mapper != nil {
results = insertAddr(results, c.mapper.ExternalAddr())
}
slices.SortFunc(results, func(a, b netip.AddrPort) int {
return strings.Compare(a.String(), b.String())
})
return results
}
@@ -209,48 +169,3 @@ func (c *realmConnClient) punch(meta PunchMetadata, peers []netip.AddrPort) (*ne
func (c *realmConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) {
return c.PacketConn.WriteTo(p, c.peer)
}
func (c *realmConnClient) Close() error {
// Sadly, closing the core does not first close the sockets created for outbound connections
c.cancel()
c.wg.Wait()
return nil
}
func portMapLoop(ctx context.Context, mapper *PortMapper, done func()) {
defer func() {
err := mapper.Close()
done()
errors.LogDebug(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] removed with ", err)
}()
interval := mapper.Lifetime() / 2
if interval <= 0 {
interval = time.Minute
}
t := time.NewTicker(interval)
defer t.Stop()
failing := false
for {
select {
case <-ctx.Done():
return
case <-t.C:
changed, err := mapper.Renew(ctx)
if err != nil {
if ctx.Err() != nil {
return
}
if !failing {
errors.LogError(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] renewal failed")
failing = true
}
continue
}
errors.LogDebug(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] external ", mapper.ExternalAddr(), ", changed ", changed)
if failing {
errors.LogError(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] recovered")
failing = false
}
}
}
}
+16 -156
View File
@@ -22,115 +22,6 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Family int32
const (
Family_Dual Family = 0
Family_V4 Family = 1
Family_V6 Family = 2
)
// Enum value maps for Family.
var (
Family_name = map[int32]string{
0: "Dual",
1: "V4",
2: "V6",
}
Family_value = map[string]int32{
"Dual": 0,
"V4": 1,
"V6": 2,
}
)
func (x Family) Enum() *Family {
p := new(Family)
*p = x
return p
}
func (x Family) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Family) Descriptor() protoreflect.EnumDescriptor {
return file_transport_internet_finalmask_realm_config_proto_enumTypes[0].Descriptor()
}
func (Family) Type() protoreflect.EnumType {
return &file_transport_internet_finalmask_realm_config_proto_enumTypes[0]
}
func (x Family) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Family.Descriptor instead.
func (Family) EnumDescriptor() ([]byte, []int) {
return file_transport_internet_finalmask_realm_config_proto_rawDescGZIP(), []int{0}
}
type PortMapping struct {
state protoimpl.MessageState `protogen:"open.v1"`
Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"`
Timeout int64 `protobuf:"varint,2,opt,name=timeout,proto3" json:"timeout,omitempty"`
Lifetime int64 `protobuf:"varint,3,opt,name=lifetime,proto3" json:"lifetime,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *PortMapping) Reset() {
*x = PortMapping{}
mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *PortMapping) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PortMapping) ProtoMessage() {}
func (x *PortMapping) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_finalmask_realm_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 PortMapping.ProtoReflect.Descriptor instead.
func (*PortMapping) Descriptor() ([]byte, []int) {
return file_transport_internet_finalmask_realm_config_proto_rawDescGZIP(), []int{0}
}
func (x *PortMapping) GetEnabled() bool {
if x != nil {
return x.Enabled
}
return false
}
func (x *PortMapping) GetTimeout() int64 {
if x != nil {
return x.Timeout
}
return 0
}
func (x *PortMapping) GetLifetime() int64 {
if x != nil {
return x.Lifetime
}
return 0
}
type Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
Scheme string `protobuf:"bytes,1,opt,name=scheme,proto3" json:"scheme,omitempty"`
@@ -140,15 +31,13 @@ type Config struct {
ID string `protobuf:"bytes,5,opt,name=ID,proto3" json:"ID,omitempty"`
StunServers []string `protobuf:"bytes,6,rep,name=stun_servers,json=stunServers,proto3" json:"stun_servers,omitempty"`
TlsConfig *tls.Config `protobuf:"bytes,7,opt,name=tls_config,json=tlsConfig,proto3" json:"tls_config,omitempty"`
IPMode string `protobuf:"bytes,8,opt,name=IPMode,proto3" json:"IPMode,omitempty"`
PortMapping *PortMapping `protobuf:"bytes,9,opt,name=port_mapping,json=portMapping,proto3" json:"port_mapping,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Config) Reset() {
*x = Config{}
mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[1]
mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -160,7 +49,7 @@ func (x *Config) String() string {
func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[1]
mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -173,7 +62,7 @@ func (x *Config) ProtoReflect() protoreflect.Message {
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) {
return file_transport_internet_finalmask_realm_config_proto_rawDescGZIP(), []int{1}
return file_transport_internet_finalmask_realm_config_proto_rawDescGZIP(), []int{0}
}
func (x *Config) GetScheme() string {
@@ -225,29 +114,11 @@ func (x *Config) GetTlsConfig() *tls.Config {
return nil
}
func (x *Config) GetIPMode() string {
if x != nil {
return x.IPMode
}
return ""
}
func (x *Config) GetPortMapping() *PortMapping {
if x != nil {
return x.PortMapping
}
return nil
}
var File_transport_internet_finalmask_realm_config_proto protoreflect.FileDescriptor
const file_transport_internet_finalmask_realm_config_proto_rawDesc = "" +
"\n" +
"/transport/internet/finalmask/realm/config.proto\x12'xray.transport.internet.finalmask.realm\x1a#transport/internet/tls/config.proto\"]\n" +
"\vPortMapping\x12\x18\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\x12\x18\n" +
"\atimeout\x18\x02 \x01(\x03R\atimeout\x12\x1a\n" +
"\blifetime\x18\x03 \x01(\x03R\blifetime\"\xc6\x02\n" +
"/transport/internet/finalmask/realm/config.proto\x12'xray.transport.internet.finalmask.realm\x1a#transport/internet/tls/config.proto\"\xd5\x01\n" +
"\x06Config\x12\x16\n" +
"\x06scheme\x18\x01 \x01(\tR\x06scheme\x12\x12\n" +
"\x04host\x18\x02 \x01(\tR\x04host\x12\x12\n" +
@@ -256,13 +127,7 @@ const file_transport_internet_finalmask_realm_config_proto_rawDesc = "" +
"\x02ID\x18\x05 \x01(\tR\x02ID\x12!\n" +
"\fstun_servers\x18\x06 \x03(\tR\vstunServers\x12B\n" +
"\n" +
"tls_config\x18\a \x01(\v2#.xray.transport.internet.tls.ConfigR\ttlsConfig\x12\x16\n" +
"\x06IPMode\x18\b \x01(\tR\x06IPMode\x12W\n" +
"\fport_mapping\x18\t \x01(\v24.xray.transport.internet.finalmask.realm.PortMappingR\vportMapping*\"\n" +
"\x06Family\x12\b\n" +
"\x04Dual\x10\x00\x12\x06\n" +
"\x02V4\x10\x01\x12\x06\n" +
"\x02V6\x10\x02B\x97\x01\n" +
"tls_config\x18\a \x01(\v2#.xray.transport.internet.tls.ConfigR\ttlsConfigB\x97\x01\n" +
"+com.xray.transport.internet.finalmask.realmP\x01Z<github.com/xtls/xray-core/transport/internet/finalmask/realm\xaa\x02'Xray.Transport.Internet.Finalmask.Realmb\x06proto3"
var (
@@ -277,22 +142,18 @@ func file_transport_internet_finalmask_realm_config_proto_rawDescGZIP() []byte {
return file_transport_internet_finalmask_realm_config_proto_rawDescData
}
var file_transport_internet_finalmask_realm_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_transport_internet_finalmask_realm_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_transport_internet_finalmask_realm_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_transport_internet_finalmask_realm_config_proto_goTypes = []any{
(Family)(0), // 0: xray.transport.internet.finalmask.realm.Family
(*PortMapping)(nil), // 1: xray.transport.internet.finalmask.realm.PortMapping
(*Config)(nil), // 2: xray.transport.internet.finalmask.realm.Config
(*tls.Config)(nil), // 3: xray.transport.internet.tls.Config
(*Config)(nil), // 0: xray.transport.internet.finalmask.realm.Config
(*tls.Config)(nil), // 1: xray.transport.internet.tls.Config
}
var file_transport_internet_finalmask_realm_config_proto_depIdxs = []int32{
3, // 0: xray.transport.internet.finalmask.realm.Config.tls_config:type_name -> xray.transport.internet.tls.Config
1, // 1: xray.transport.internet.finalmask.realm.Config.port_mapping:type_name -> xray.transport.internet.finalmask.realm.PortMapping
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
1, // 0: xray.transport.internet.finalmask.realm.Config.tls_config:type_name -> xray.transport.internet.tls.Config
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_realm_config_proto_init() }
@@ -305,14 +166,13 @@ func file_transport_internet_finalmask_realm_config_proto_init() {
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_realm_config_proto_rawDesc), len(file_transport_internet_finalmask_realm_config_proto_rawDesc)),
NumEnums: 1,
NumMessages: 2,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_transport_internet_finalmask_realm_config_proto_goTypes,
DependencyIndexes: file_transport_internet_finalmask_realm_config_proto_depIdxs,
EnumInfos: file_transport_internet_finalmask_realm_config_proto_enumTypes,
MessageInfos: file_transport_internet_finalmask_realm_config_proto_msgTypes,
}.Build()
File_transport_internet_finalmask_realm_config_proto = out.File
@@ -8,18 +8,6 @@ option java_multiple_files = true;
import "transport/internet/tls/config.proto";
enum Family {
Dual = 0;
V4 = 1;
V6 = 2;
}
message PortMapping {
bool enabled = 1;
int64 timeout = 2;
int64 lifetime = 3;
}
message Config {
string scheme = 1;
string host = 2;
@@ -28,6 +16,4 @@ message Config {
string ID = 5;
repeated string stun_servers = 6;
xray.transport.internet.tls.Config tls_config = 7;
string IPMode = 8;
PortMapping port_mapping = 9;
}
@@ -1,139 +0,0 @@
package realm
import (
"context"
"errors"
"fmt"
"net/netip"
"sync"
"time"
"github.com/libp2p/go-nat"
)
const (
defaultPortMapTimeout = 10 * time.Second
defaultPortMapLifetime = 10 * time.Minute
portMapDescription = "hysteria-realm"
portMapProtocol = "udp"
)
var ErrInvalidPortMapConfig = errors.New("invalid port mapping config")
type PortMapConfig struct {
Timeout time.Duration
Lifetime time.Duration
}
func (c PortMapConfig) withDefaults() (PortMapConfig, error) {
if c.Timeout == 0 {
c.Timeout = defaultPortMapTimeout
}
if c.Timeout < 0 {
return c, fmt.Errorf("%w: timeout must not be negative", ErrInvalidPortMapConfig)
}
if c.Lifetime == 0 {
c.Lifetime = defaultPortMapLifetime
}
if c.Lifetime < 0 {
return c, fmt.Errorf("%w: lifetime must not be negative", ErrInvalidPortMapConfig)
}
return c, nil
}
// PortMapper maintains a UDP port mapping on the local gateway via UPnP or
// NAT-PMP. It does not renew the mapping by itself; the caller is expected
// to call Renew periodically (typically every Lifetime/2).
type PortMapper struct {
gateway nat.NAT
internalPort int
config PortMapConfig
mu sync.Mutex
externalAddr netip.AddrPort
}
// NewPortMapper discovers the local gateway and maps internalPort for UDP.
// It blocks for up to 2x config.Timeout (discovery + mapping).
func NewPortMapper(ctx context.Context, internalPort int, config PortMapConfig) (*PortMapper, error) {
if internalPort <= 0 || internalPort > 65535 {
return nil, fmt.Errorf("%w: invalid internal port %d", ErrInvalidPortMapConfig, internalPort)
}
config, err := config.withDefaults()
if err != nil {
return nil, err
}
discoverCtx, cancel := context.WithTimeout(ctx, config.Timeout)
gateway, err := nat.DiscoverGateway(discoverCtx)
cancel()
if err != nil {
return nil, fmt.Errorf("gateway discovery failed: %w", err)
}
m := &PortMapper{
gateway: gateway,
internalPort: internalPort,
config: config,
}
if _, err := m.Renew(ctx); err != nil {
return nil, err
}
return m, nil
}
// Renew (re-)requests the port mapping and refreshes the external address.
// It reports whether the external address changed since the last call.
func (m *PortMapper) Renew(ctx context.Context) (bool, error) {
opCtx, cancel := context.WithTimeout(ctx, m.config.Timeout)
defer cancel()
externalPort, err := m.gateway.AddPortMapping(opCtx, portMapProtocol, m.internalPort, portMapDescription, m.config.Lifetime)
if err != nil {
return false, fmt.Errorf("add port mapping failed: %w", err)
}
externalIP, err := m.gateway.GetExternalAddress()
if err != nil {
return false, fmt.Errorf("get external address failed: %w", err)
}
addr, ok := netip.AddrFromSlice(externalIP)
if !ok || addr.IsUnspecified() || addr.IsLoopback() {
return false, fmt.Errorf("gateway returned unusable external address: %s", externalIP)
}
externalAddr := netip.AddrPortFrom(addr.Unmap(), uint16(externalPort))
m.mu.Lock()
changed := externalAddr != m.externalAddr
m.externalAddr = externalAddr
m.mu.Unlock()
return changed, nil
}
// ExternalAddr returns the gateway's external IP and the mapped external port.
func (m *PortMapper) ExternalAddr() netip.AddrPort {
m.mu.Lock()
defer m.mu.Unlock()
return m.externalAddr
}
// InternalPort returns the mapped local UDP port.
func (m *PortMapper) InternalPort() int {
return m.internalPort
}
// Lifetime returns the effective mapping lease duration.
func (m *PortMapper) Lifetime() time.Duration {
return m.config.Lifetime
}
// GatewayType returns the protocol used to talk to the gateway ("UPnP" or "NAT-PMP").
func (m *PortMapper) GatewayType() string {
return m.gateway.Type()
}
// Close removes the port mapping from the gateway. Best-effort.
func (m *PortMapper) Close() error {
ctx, cancel := context.WithTimeout(context.Background(), m.config.Timeout)
defer cancel()
return m.gateway.DeletePortMapping(ctx, portMapProtocol, m.internalPort)
}
+9 -45
View File
@@ -33,16 +33,14 @@ type STUNPacketEvent struct {
}
type realmConnServer struct {
wg sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
cleaned chan struct{}
ctx context.Context
cancel context.CancelFunc
net.PacketConn
realmClient *Client
realmID string
stunServers []string
family Family
mapper *PortMapper
stunTimeout time.Duration
punchTimeout time.Duration
punchInterval time.Duration
@@ -59,29 +57,8 @@ type realmConnServer struct {
func NewConnServer(config *Config, raw net.PacketConn) (net.PacketConn, error) {
ctx, cancel := context.WithCancel(context.Background())
family := Family_Dual
switch config.IPMode {
case "dual":
case "v4":
family = Family_V4
case "v6":
family = Family_V6
}
var mapper *PortMapper
if config.PortMapping != nil && config.PortMapping.Enabled {
var err error
start := time.Now()
mapper, err = NewPortMapper(context.Background(), raw.LocalAddr().(*net.UDPAddr).Port, PortMapConfig{Timeout: time.Duration(config.PortMapping.Timeout) * time.Second, Lifetime: time.Duration(config.PortMapping.Lifetime) * time.Second})
if err != nil {
errors.LogErrorInner(context.Background(), err, "[realm] [port mapping] [", raw.LocalAddr().(*net.UDPAddr).Port, "] init failed after ", time.Since(start))
} else {
errors.LogDebug(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] gateway ", mapper.GatewayType(), ", external ", mapper.ExternalAddr())
errors.LogDebug(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] init success with ", time.Since(start))
}
}
conn := &realmConnServer{
cleaned: make(chan struct{}),
ctx: ctx,
cancel: cancel,
PacketConn: raw,
@@ -89,8 +66,6 @@ func NewConnServer(config *Config, raw net.PacketConn) (net.PacketConn, error) {
realmClient: NewClient(config.Scheme, config.Host, config.Port, config.Token, config.TlsConfig),
realmID: config.ID,
stunServers: config.StunServers,
family: family,
mapper: mapper,
stunTimeout: defaultSTUNTimeout,
punchTimeout: defaultPunchTimeout,
punchInterval: defaultPunchInterval,
@@ -99,12 +74,6 @@ func NewConnServer(config *Config, raw net.PacketConn) (net.PacketConn, error) {
stun: make(chan STUNPacketEvent, defaultEventBuffer),
}
if mapper != nil {
conn.wg.Add(1)
go portMapLoop(ctx, mapper, conn.wg.Done)
}
conn.wg.Add(1)
go conn.run()
return conn, nil
@@ -168,8 +137,6 @@ func (c *realmConnServer) discover(servers []*net.UDPAddr) []netip.AddrPort {
results := make([]netip.AddrPort, 0, len(servers))
for len(transactionIDs) > 0 {
select {
case <-c.ctx.Done():
goto end
case <-deadline.C:
goto end
case ev := <-c.stun:
@@ -181,9 +148,6 @@ func (c *realmConnServer) discover(servers []*net.UDPAddr) []netip.AddrPort {
}
end:
deadline.Stop()
if c.mapper != nil {
results = insertAddr(results, c.mapper.ExternalAddr())
}
slices.SortFunc(results, func(a, b netip.AddrPort) int {
return strings.Compare(a.String(), b.String())
})
@@ -195,7 +159,7 @@ func (c *realmConnServer) getlocals(force bool) []netip.AddrPort {
c.localsMu.Lock()
if force || time.Since(c.localsLast) > defaultStunCacheTTL {
start := time.Now()
servers := resolveSTUNServers(c.PacketConn.LocalAddr().(*net.UDPAddr).IP, c.stunServers, c.family)
servers := resolveSTUNServers(c.PacketConn.LocalAddr().(*net.UDPAddr).IP, c.stunServers)
errors.LogDebug(context.Background(), "[realm] update stun servers ", servers, " with ", time.Since(start))
if len(servers) > 0 {
start = time.Now()
@@ -268,7 +232,7 @@ retry:
if err != nil {
errors.LogErrorInner(context.Background(), err, "[realm] ", c.realmID, " register session err retry in ", backoff)
if c.waitctx(c.ctx, backoff) {
c.wg.Done()
close(c.cleaned)
return
}
backoff *= 2
@@ -295,7 +259,7 @@ retry:
case <-c.ctx.Done():
_ = c.realmClient.Deregister(context.Background(), c.realmID, resp.SessionID)
errors.LogDebug(context.Background(), "[realm] ", c.realmID, " ", resp.SessionID, " deregistered")
c.wg.Done()
close(c.cleaned)
return
default:
goto retry
@@ -396,7 +360,7 @@ func (c *realmConnServer) punchEvent(ctx context.Context, sid string, ev *PunchE
peers, _ := parseAddrPorts(ev.Addresses)
errors.LogDebug(context.Background(), "[realm] ", ev.Nonce, " update peers ", peers)
filteredPeers, seen := candidatePunchAddrs(locals, peers, c.family)
filteredPeers, seen := candidatePunchAddrs(locals, peers)
errors.LogDebug(context.Background(), "[realm] ", ev.Nonce, " filtered peers ", filteredPeers)
expandedPeers := expandSymmetricNATCandidates(filteredPeers, seen)
errors.LogDebug(context.Background(), "[realm] ", ev.Nonce, " expanded peers ", expandedPeers)
@@ -434,6 +398,6 @@ func (c *realmConnServer) ReadFrom(p []byte) (int, net.Addr, error) {
func (c *realmConnServer) Close() error {
c.cancel()
c.wg.Wait()
<-c.cleaned
return c.PacketConn.Close()
}
+6 -35
View File
@@ -23,23 +23,14 @@ const (
symmetricNATMaxPortsPerHost = 32
)
func resolveSTUNServers(local net.IP, servers []string, family Family) []*net.UDPAddr {
func resolveSTUNServers(local net.IP, servers []string) []*net.UDPAddr {
var network string
if family == Family_Dual {
if local.IsUnspecified() {
network = "ip"
} else {
if local.To4() != nil {
network = "ip4"
} else {
network = "ip6"
}
}
if local.IsUnspecified() {
network = "ip"
} else {
if family == Family_V4 {
if local.To4() != nil {
network = "ip4"
}
if family == Family_V6 {
} else {
network = "ip6"
}
}
@@ -113,7 +104,7 @@ func netIPPortToAddrPort(ip net.IP, port int) (netip.AddrPort, error) {
return netip.AddrPortFrom(netip.AddrFrom16(addr), uint16(port)), nil
}
func candidatePunchAddrs(locals, peers []netip.AddrPort, family Family) ([]netip.AddrPort, map[netip.AddrPort]struct{}) {
func candidatePunchAddrs(locals, peers []netip.AddrPort) ([]netip.AddrPort, map[netip.AddrPort]struct{}) {
var allow4, allow6 bool
for _, local := range locals {
if local.Addr().Is4() {
@@ -125,12 +116,6 @@ func candidatePunchAddrs(locals, peers []netip.AddrPort, family Family) ([]netip
break
}
}
if family == Family_V4 {
allow6 = false
}
if family == Family_V6 {
allow4 = false
}
seen := make(map[netip.AddrPort]struct{}, len(peers))
candidates := make([]netip.AddrPort, 0, len(peers))
for _, peer := range peers {
@@ -233,17 +218,3 @@ func parseAddrPorts(addrs []string) ([]netip.AddrPort, error) {
}
return out, nil
}
func insertAddr(addrs []netip.AddrPort, addr netip.AddrPort) []netip.AddrPort {
if !addr.IsValid() {
return addrs
}
out := append([]netip.AddrPort(nil), addrs...)
i, found := slices.BinarySearchFunc(out, addr, func(a, b netip.AddrPort) int {
return strings.Compare(a.String(), b.String())
})
if found {
return out
}
return slices.Insert(out, i, addr)
}
-138
View File
@@ -1,138 +0,0 @@
package xmc
// Copied from https://github.com/Tnze/go-mc/blob/539b4a3a7f030332eb58b8a946116ae7907630d2/net/CFB8/cfb8.go
import (
"crypto/cipher"
"crypto/subtle"
"unsafe"
)
type cfb8 struct {
c cipher.Block
blockSize int
ivPos int
iv []byte
de bool
}
func newCFB8Decrypt(c cipher.Block, iv []byte) *cfb8 {
return newCFB8(c, iv, true)
}
func newCFB8Encrypt(c cipher.Block, iv []byte) *cfb8 {
return newCFB8(c, iv, false)
}
func newCFB8(c cipher.Block, iv []byte, de bool) *cfb8 {
cp := make([]byte, len(iv)*3)
copy(cp, iv)
return &cfb8{
c: c,
blockSize: c.BlockSize(),
iv: cp,
de: de,
}
}
func (cf *cfb8) XORKeyStream(dst, src []byte) {
if len(src) == 0 {
return
}
if len(dst) < len(src) {
panic("cfb8: output smaller than input")
}
// If dst and src does not overlap in first block size,
// and the length of src is greater than 2*blockSize,
// we can use an optimized implementation.
if len(src) > cf.blockSize<<1 &&
(uintptr(unsafe.Pointer(&dst[0]))+uintptr(cf.blockSize) <= uintptr(unsafe.Pointer(&src[0])) ||
uintptr(unsafe.Pointer(&src[0]))+uintptr(len(src)) <= uintptr(unsafe.Pointer(&dst[0]))) {
// encrypt/decrypt first blockSize bytes
// After this, the IV will come to the same as
// the last blockSize of ciphertext, so
// we can reuse them without copy.
cf.xorKeyStream(dst, src[:cf.blockSize])
var ciphertext []byte
if cf.de {
ciphertext = src
} else {
ciphertext = dst
}
dst = dst[cf.blockSize:]
src = src[cf.blockSize:]
iv := cf.iv
_ = iv[0] // bounds check hint to compiler; see golang.org/issue/14808
var (
i int
val byte
)
dst = dst[:len(src)]
if cf.de && // and requires to be non-overlapping at all
uintptr(unsafe.Pointer(&dst[0])) <= uintptr(unsafe.Pointer(&src[len(src)-1])) &&
uintptr(unsafe.Pointer(&src[0])) <= uintptr(unsafe.Pointer(&dst[len(dst)-1])) {
for i = 0; i < len(src)-cf.blockSize; i += 1 {
cf.c.Encrypt(dst[i:], ciphertext[i:])
}
subtle.XORBytes(dst, src[:i], dst)
for ; i < len(src); i += 1 {
cf.c.Encrypt(iv, ciphertext[i:])
dst[i] = src[i] ^ iv[0]
}
} else {
_ = ciphertext[len(src)]
for i, val = range src {
cf.c.Encrypt(iv, ciphertext[i:])
dst[i] = val ^ iv[0]
}
// for-range does not increase i in the last loop,
// compared to the classic for clause
i += 1
}
// copy the current IV for next operation
copy(iv, ciphertext[i:i+cf.blockSize])
cf.ivPos = 0
return
}
cf.xorKeyStream(dst, src)
}
func (cf *cfb8) xorKeyStream(dst, src []byte) {
dst = dst[:len(src)] // remove bounds check in loop
for i, val := range src {
posPlusBlockSize := cf.ivPos + cf.blockSize
// fast mod; 2*blockSize must be a non-negative integer power of 2
tempPos := posPlusBlockSize & (cf.blockSize<<1 - 1)
// reuse space to store encrypted block
cf.c.Encrypt(cf.iv[tempPos:], cf.iv[cf.ivPos:])
// Only the first byte of the encrypted block is used
// for encryption/decryption, other bytes are ignored.
val ^= cf.iv[tempPos]
if cf.ivPos == cf.blockSize<<1 {
// bound reached; move to next round for next operation
// copy next block to the start of the ring buffer
copy(cf.iv, cf.iv[cf.ivPos+1:])
// insert the encrypted byte to the end of IV
if cf.de {
cf.iv[cf.blockSize-1] = src[i]
} else {
cf.iv[cf.blockSize-1] = val
}
cf.ivPos = 0
} else {
// insert the encrypted byte to the end of IV
if cf.de {
cf.iv[posPlusBlockSize] = src[i]
} else {
cf.iv[posPlusBlockSize] = val
}
// move to next block
cf.ivPos += 1
}
dst[i] = val
}
}
-287
View File
@@ -1,287 +0,0 @@
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)
}
@@ -1,35 +0,0 @@
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
}
@@ -1,238 +0,0 @@
// 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
}
@@ -1,26 +0,0 @@
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;
}
@@ -1,85 +0,0 @@
package xmc
import (
"net"
"sync"
"time"
)
const handshakeTimeout = 2 * time.Minute
type connectionDeadlines struct {
mu sync.Mutex
c net.Conn
read time.Time
write time.Time
handshake time.Time
}
func newConnectionDeadlines(c net.Conn) *connectionDeadlines {
return &connectionDeadlines{c: c}
}
func (d *connectionDeadlines) beginHandshake() error {
d.mu.Lock()
defer d.mu.Unlock()
d.handshake = time.Now().Add(handshakeTimeout)
if err := d.applyLocked(); err != nil {
d.handshake = time.Time{}
_ = d.applyLocked()
return err
}
return nil
}
func (d *connectionDeadlines) endHandshake() error {
d.mu.Lock()
defer d.mu.Unlock()
d.handshake = time.Time{}
return d.applyLocked()
}
func (d *connectionDeadlines) setDeadline(t time.Time) error {
d.mu.Lock()
defer d.mu.Unlock()
d.read = t
d.write = t
return d.applyLocked()
}
func (d *connectionDeadlines) setReadDeadline(t time.Time) error {
d.mu.Lock()
defer d.mu.Unlock()
d.read = t
return d.c.SetReadDeadline(earlierDeadline(d.read, d.handshake))
}
func (d *connectionDeadlines) setWriteDeadline(t time.Time) error {
d.mu.Lock()
defer d.mu.Unlock()
d.write = t
return d.c.SetWriteDeadline(earlierDeadline(d.write, d.handshake))
}
func (d *connectionDeadlines) applyLocked() error {
if err := d.c.SetReadDeadline(earlierDeadline(d.read, d.handshake)); err != nil {
return err
}
return d.c.SetWriteDeadline(earlierDeadline(d.write, d.handshake))
}
func earlierDeadline(user, internal time.Time) time.Time {
if internal.IsZero() {
return user
}
if user.IsZero() || internal.Before(user) {
return internal
}
return user
}
@@ -1,81 +0,0 @@
package xmc
import (
"net"
"sync"
"testing"
"time"
)
func TestConnectionDeadlinesRestoreCallerValues(t *testing.T) {
client, server := net.Pipe()
defer client.Close()
defer server.Close()
recording := &deadlineRecordingConn{Conn: client}
deadlines := newConnectionDeadlines(recording)
callerDeadline := time.Now().Add(10 * time.Minute)
if err := deadlines.setDeadline(callerDeadline); err != nil {
t.Fatal(err)
}
if err := deadlines.beginHandshake(); err != nil {
t.Fatal(err)
}
read, write := recording.currentDeadlines()
if !read.Before(callerDeadline) || !write.Before(callerDeadline) {
t.Fatalf("handshake deadlines = %s/%s, caller = %s", read, write, callerDeadline)
}
shortReadDeadline := time.Now().Add(time.Second)
if err := deadlines.setReadDeadline(shortReadDeadline); err != nil {
t.Fatal(err)
}
read, _ = recording.currentDeadlines()
if !read.Equal(shortReadDeadline) {
t.Fatalf("read deadline = %s, want %s", read, shortReadDeadline)
}
if err := deadlines.endHandshake(); err != nil {
t.Fatal(err)
}
read, write = recording.currentDeadlines()
if !read.Equal(shortReadDeadline) || !write.Equal(callerDeadline) {
t.Fatalf("restored deadlines = %s/%s, want %s/%s", read, write, shortReadDeadline, callerDeadline)
}
}
type deadlineRecordingConn struct {
net.Conn
mu sync.Mutex
read time.Time
write time.Time
}
func (c *deadlineRecordingConn) SetDeadline(t time.Time) error {
c.mu.Lock()
c.read = t
c.write = t
c.mu.Unlock()
return c.Conn.SetDeadline(t)
}
func (c *deadlineRecordingConn) SetReadDeadline(t time.Time) error {
c.mu.Lock()
c.read = t
c.mu.Unlock()
return c.Conn.SetReadDeadline(t)
}
func (c *deadlineRecordingConn) SetWriteDeadline(t time.Time) error {
c.mu.Lock()
c.write = t
c.mu.Unlock()
return c.Conn.SetWriteDeadline(t)
}
func (c *deadlineRecordingConn) currentDeadlines() (time.Time, time.Time) {
c.mu.Lock()
defer c.mu.Unlock()
return c.read, c.write
}
@@ -1,103 +0,0 @@
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
}
@@ -1,58 +0,0 @@
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)
}
}
@@ -1,390 +0,0 @@
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")
}
}
@@ -1,184 +0,0 @@
package xmc
import (
"bytes"
"fmt"
"io"
"sync"
"sync/atomic"
"time"
)
const (
configurationClientboundCustomPayload = 0x01
configurationServerboundCustomPayload = 0x02
configurationKeepAlive = 0x04
packetChannel = "xmc:data"
maxPacketData = 24 * 1024
keepAlivePeriod = 15 * time.Second
)
// packetStream carries the raw proxy byte stream in Minecraft configuration
// custom payload packets. The configuration state provides bidirectional
// payload packets and keep-alives without requiring version-specific world data.
type packetStream struct {
reader io.Reader
writer io.Writer
isClient bool
readMu sync.Mutex
writeMu sync.Mutex
pending []byte
keepAliveID atomic.Int64
done chan struct{}
stopOnce sync.Once
}
func newPacketStream(reader io.Reader, writer io.Writer, isClient bool) *packetStream {
s := &packetStream{
reader: reader,
writer: writer,
isClient: isClient,
done: make(chan struct{}),
}
if !isClient {
go s.keepAliveLoop()
}
return s
}
func (s *packetStream) Read(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
s.readMu.Lock()
defer s.readMu.Unlock()
if len(s.pending) > 0 {
n := copy(p, s.pending)
s.pending = s.pending[n:]
return n, nil
}
for {
packet, err := readPacket(s.reader)
if err != nil {
return 0, fmt.Errorf("read minecraft packet stream: %w", err)
}
if packet.packetID == s.remoteCustomPayloadID() {
payload, ok, err := parseCustomPayload(packet)
if err != nil {
return 0, err
}
if !ok || len(payload) == 0 {
continue
}
n := copy(p, payload)
if n < len(payload) {
s.pending = append(s.pending[:0], payload[n:]...)
}
return n, nil
}
if packet.packetID == configurationKeepAlive {
var id Long
if err := packet.readFields(&id); err != nil {
return 0, fmt.Errorf("read minecraft keep-alive: %w", err)
}
if s.isClient {
if err := s.writeKeepAlive(id); err != nil {
return 0, err
}
}
}
}
}
func (s *packetStream) Write(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
s.writeMu.Lock()
defer s.writeMu.Unlock()
written := 0
for written < len(p) {
end := written + maxPacketData
if end > len(p) {
end = len(p)
}
channel := String(packetChannel)
payload := RestBytes(p[written:end])
if err := writePacket(s.writer, s.localCustomPayloadID(), &channel, &payload); err != nil {
return written, fmt.Errorf("write minecraft custom payload: %w", err)
}
written = end
}
return written, nil
}
func (s *packetStream) Stop() {
s.stopOnce.Do(func() { close(s.done) })
}
func (s *packetStream) localCustomPayloadID() int {
if s.isClient {
return configurationServerboundCustomPayload
}
return configurationClientboundCustomPayload
}
func (s *packetStream) remoteCustomPayloadID() int {
if s.isClient {
return configurationClientboundCustomPayload
}
return configurationServerboundCustomPayload
}
func parseCustomPayload(packet *mcPacket) ([]byte, bool, error) {
r := bytes.NewReader(packet.data)
var channel String
if err := channel.readFrom(r); err != nil {
return nil, false, fmt.Errorf("read minecraft custom payload channel: %w", err)
}
if string(channel) != packetChannel {
return nil, false, nil
}
payload := make([]byte, r.Len())
if _, err := io.ReadFull(r, payload); err != nil {
return nil, false, fmt.Errorf("read minecraft custom payload data: %w", err)
}
return payload, true, nil
}
func (s *packetStream) writeKeepAlive(id Long) error {
s.writeMu.Lock()
defer s.writeMu.Unlock()
if err := writePacket(s.writer, configurationKeepAlive, &id); err != nil {
return fmt.Errorf("write minecraft keep-alive: %w", err)
}
return nil
}
func (s *packetStream) keepAliveLoop() {
ticker := time.NewTicker(keepAlivePeriod)
defer ticker.Stop()
for {
select {
case <-ticker.C:
id := Long(s.keepAliveID.Add(1))
if err := s.writeKeepAlive(id); err != nil {
return
}
case <-s.done:
return
}
}
}
@@ -1,102 +0,0 @@
package xmc
import (
"bytes"
"io"
"net"
"testing"
)
func TestPacketStreamUsesPlainFraming(t *testing.T) {
payload := []byte("hello")
var wire bytes.Buffer
stream := newPacketStream(bytes.NewReader(nil), &wire, true)
written, err := stream.Write(payload)
if err != nil {
t.Fatalf("write payload: %v", err)
}
if written != len(payload) {
t.Fatalf("written = %d, want %d", written, len(payload))
}
wantOutbound := []byte{0x0f, 0x02, 0x08, 'x', 'm', 'c', ':', 'd', 'a', 't', 'a', 'h', 'e', 'l', 'l', 'o'}
if !bytes.Equal(wire.Bytes(), wantOutbound) {
t.Fatalf("wire frame = %x, want %x", wire.Bytes(), wantOutbound)
}
wantInbound := append([]byte(nil), wantOutbound...)
wantInbound[1] = configurationClientboundCustomPayload
reader := newPacketStream(bytes.NewReader(wantInbound), io.Discard, true)
got := make([]byte, len(payload))
if _, err = io.ReadFull(reader, got); err != nil {
t.Fatalf("read payload: %v", err)
}
if !bytes.Equal(got, payload) {
t.Fatalf("payload = %q, want %q", got, payload)
}
}
func TestPacketStreamRoundTrip(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
const password = "packet-stream-shared-key"
privateKey, publicKey := deriveTestRSAKey(t, password)
profiles := []loginProfile{testLoginProfile("packet_user")}
clientPayload := bytes.Repeat([]byte("client-payload-"), 5000)
serverPayload := bytes.Repeat([]byte("server-payload-"), 5000)
serverDone := make(chan error, 1)
go func() {
rawConn, acceptErr := ln.Accept()
if acceptErr != nil {
serverDone <- acceptErr
return
}
defer rawConn.Close()
server, wrapErr := wrapConnServer(rawConn, profiles, password, privateKey, publicKey)
if wrapErr != nil {
serverDone <- wrapErr
return
}
got := make([]byte, len(clientPayload))
if _, readErr := io.ReadFull(server, got); readErr != nil {
serverDone <- readErr
return
}
if !bytes.Equal(got, clientPayload) {
serverDone <- io.ErrUnexpectedEOF
return
}
_, writeErr := server.Write(serverPayload)
serverDone <- writeErr
}()
rawClient, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
t.Fatal(err)
}
defer rawClient.Close()
client, err := newClientConn(rawClient, profiles, password, publicKey, "localhost")
if err != nil {
t.Fatal(err)
}
if _, err = client.Write(clientPayload); err != nil {
t.Fatalf("write payload: %v", err)
}
got := make([]byte, len(serverPayload))
if _, err = io.ReadFull(client, got); err != nil {
t.Fatalf("read payload: %v", err)
}
if !bytes.Equal(got, serverPayload) {
t.Fatal("server payload mismatch")
}
if err = <-serverDone; err != nil {
t.Fatalf("server: %v", err)
}
}
-432
View File
@@ -1,432 +0,0 @@
package xmc
import (
"bytes"
"crypto/rand"
"fmt"
"io"
"math/big"
"time"
)
type paddingDirection uint8
const (
paddingClientToServer paddingDirection = iota + 1
paddingServerToClient
paddingBufferLength = 16 * 1024
maxPaddingChunkLength = 48 * 1024
maxPaddingTurnLength = 8 * 1024 * 1024
)
type paddingVariant struct {
chunks []int
delays []paddingDelayRange
}
type paddingDelayRange struct {
min time.Duration
max time.Duration
}
type paddingTurn struct {
direction paddingDirection
minLength int
maxLength int
variants []paddingVariant
startDelay paddingDelayRange
chunkDelay paddingDelayRange
writeChunkMinLength int
writeChunkLength int
sendMinLength int
sendMaxLength int
sendVariants []int
}
func runPaddingSchedule(reader io.Reader, writer io.Writer, isClient bool, firstTurnPrefixLength int, schedule []paddingTurn) error {
if err := validatePaddingSchedule(schedule, firstTurnPrefixLength); err != nil {
return err
}
var writeBuffer []byte
for i, turn := range schedule {
prefixLength := 0
if i == 0 {
prefixLength = firstTurnPrefixLength
}
localSends := isClient == (turn.direction == paddingClientToServer)
if localSends {
if err := writePaddingTurnWithBuffer(writer, turn, prefixLength, time.Sleep, &writeBuffer); err != nil {
return fmt.Errorf("write padding turn %d: %w", i, err)
}
continue
}
if err := readPaddingTurn(reader, turn, prefixLength); err != nil {
return fmt.Errorf("read padding turn %d: %w", i, err)
}
}
return nil
}
func validatePaddingSchedule(schedule []paddingTurn, firstTurnPrefixLength int) error {
if len(schedule) == 0 {
return fmt.Errorf("empty padding schedule")
}
if firstTurnPrefixLength < 0 {
return fmt.Errorf("negative first turn prefix length: %d", firstTurnPrefixLength)
}
if firstTurnPrefixLength > 0 && schedule[0].direction != paddingClientToServer {
return fmt.Errorf("first prefixed padding turn is not client-to-server")
}
for i, turn := range schedule {
if turn.direction != paddingClientToServer && turn.direction != paddingServerToClient {
return fmt.Errorf("padding turn %d has invalid direction: %d", i, turn.direction)
}
if err := validatePaddingDelayRange(turn.startDelay); err != nil {
return fmt.Errorf("padding turn %d has an invalid start delay: %w", i, err)
}
if err := validatePaddingDelayRange(turn.chunkDelay); err != nil {
return fmt.Errorf("padding turn %d has an invalid chunk delay: %w", i, err)
}
if turn.writeChunkMinLength < 0 || turn.writeChunkLength < turn.writeChunkMinLength || turn.writeChunkLength > maxPaddingChunkLength {
return fmt.Errorf("padding turn %d has an invalid write chunk range: %d-%d", i, turn.writeChunkMinLength, turn.writeChunkLength)
}
if len(turn.variants) > 0 && turn.writeChunkLength != 0 {
return fmt.Errorf("padding turn %d combines variants with generated write chunks", i)
}
minLength, maxLength, err := paddingTurnBounds(turn)
if err != nil {
return fmt.Errorf("padding turn %d: %w", i, err)
}
hasSendRange := turn.sendMinLength != 0 || turn.sendMaxLength != 0
if hasSendRange {
if len(turn.variants) > 0 {
return fmt.Errorf("padding turn %d combines variants with a send range", i)
}
if turn.sendMinLength < minLength || turn.sendMaxLength < turn.sendMinLength || turn.sendMaxLength > maxLength {
return fmt.Errorf("padding turn %d has an invalid send range: %d-%d", i, turn.sendMinLength, turn.sendMaxLength)
}
}
if i == 0 && minLength-firstTurnPrefixLength < 1 {
return fmt.Errorf("padding turn 0 is too short for %d prefix bytes", firstTurnPrefixLength)
}
if i == 0 && len(turn.variants) > 0 {
for j, variant := range turn.variants {
if _, _, err = trimPaddingPrefix(variant, firstTurnPrefixLength); err != nil {
return fmt.Errorf("padding turn 0 variant %d: %w", j, err)
}
}
}
if i > 0 && turn.direction == schedule[i-1].direction {
return fmt.Errorf("padding turns %d and %d have the same direction", i-1, i)
}
}
return nil
}
func writePaddingTurn(w io.Writer, turn paddingTurn, prefixLength int) error {
return writePaddingTurnWithSleep(w, turn, prefixLength, time.Sleep)
}
func writePaddingTurnWithSleep(w io.Writer, turn paddingTurn, prefixLength int, sleep func(time.Duration)) error {
return writePaddingTurnWithBuffer(w, turn, prefixLength, sleep, nil)
}
func writePaddingTurnWithBuffer(w io.Writer, turn paddingTurn, prefixLength int, sleep func(time.Duration), reusableBuffer *[]byte) error {
startDelay, err := randomPaddingDelay(turn.startDelay)
if err != nil {
return fmt.Errorf("select padding start delay: %w", err)
}
if startDelay > 0 {
sleep(startDelay)
}
targetLength, chunks, delays, err := selectPaddingVariant(turn, prefixLength)
if err != nil {
return err
}
recordLength := targetLength - prefixLength
if recordLength < 1 {
return fmt.Errorf("target length %d leaves an invalid record length %d", targetLength, recordLength)
}
encodedLength := Varint(recordLength)
var header bytes.Buffer
if err = encodedLength.writeTo(&header); err != nil {
return fmt.Errorf("write padding header: %w", err)
}
if len(chunks) == 0 {
writeChunkLength := turn.writeChunkLength
if writeChunkLength == 0 {
writeChunkLength = paddingBufferLength
} else if turn.writeChunkMinLength > 0 {
writeChunkLength, err = randomPaddingTarget(turn.writeChunkMinLength, writeChunkLength)
if err != nil {
return fmt.Errorf("select padding write chunk length: %w", err)
}
}
chunks = defaultPaddingChunks(recordLength, writeChunkLength)
delays = make([]paddingDelayRange, len(chunks))
for i := 1; i < len(delays); i++ {
delays[i] = turn.chunkDelay
}
}
if chunks[0] < header.Len() {
return fmt.Errorf("first padding chunk %d is shorter than header %d", chunks[0], header.Len())
}
maxChunkLength := 0
for _, chunkLength := range chunks {
if chunkLength < 1 || chunkLength > maxPaddingChunkLength {
return fmt.Errorf("invalid padding chunk length: %d", chunkLength)
}
maxChunkLength = max(maxChunkLength, chunkLength)
}
var buffer []byte
if reusableBuffer == nil {
buffer = make([]byte, maxChunkLength)
} else {
if cap(*reusableBuffer) < maxChunkLength {
*reusableBuffer = make([]byte, maxChunkLength)
}
buffer = (*reusableBuffer)[:maxChunkLength]
clear(buffer)
}
copy(buffer, header.Bytes())
written := 0
for i, chunkLength := range chunks {
if i < len(delays) {
delay, delayErr := randomPaddingDelay(delays[i])
if delayErr != nil {
return fmt.Errorf("select padding chunk %d delay: %w", i, delayErr)
}
if delay > 0 {
sleep(delay)
}
}
if err = writeFull(w, buffer[:chunkLength]); err != nil {
return fmt.Errorf("write padding chunk %d: %w", i, err)
}
written += chunkLength
if i == 0 {
clear(buffer[:header.Len()])
}
}
if written != recordLength {
return fmt.Errorf("padding chunks total %d, want %d", written, recordLength)
}
return nil
}
func readPaddingTurn(r io.Reader, turn paddingTurn, prefixLength int) error {
encodedLength, headerLength, err := readVarintWithLength(r)
if err != nil {
return fmt.Errorf("read padding header: %w", err)
}
recordLength := int(encodedLength)
if recordLength < headerLength || recordLength > maxPaddingTurnLength {
return fmt.Errorf("invalid padding record length: %d", recordLength)
}
totalLength := prefixLength + recordLength
if !paddingTurnAcceptsLength(turn, totalLength) {
if len(turn.variants) > 0 {
return fmt.Errorf("padding turn length %d is not an allowed variant", totalLength)
}
return fmt.Errorf("padding turn length %d is outside %d-%d", totalLength, turn.minLength, turn.maxLength)
}
var buffer [paddingBufferLength]byte
remaining := recordLength - headerLength
for remaining > 0 {
chunkLength := min(remaining, len(buffer))
if _, err := io.ReadFull(r, buffer[:chunkLength]); err != nil {
return fmt.Errorf("read padding body: %w", err)
}
remaining -= chunkLength
}
return nil
}
func selectPaddingVariant(turn paddingTurn, prefixLength int) (int, []int, []paddingDelayRange, error) {
if len(turn.variants) == 0 {
minimum, maximum := turn.minLength, turn.maxLength
if turn.sendMinLength != 0 || turn.sendMaxLength != 0 {
minimum, maximum = turn.sendMinLength, turn.sendMaxLength
}
targetLength, err := randomPaddingTarget(minimum, maximum)
return targetLength, nil, nil, err
}
indices := turn.sendVariants
if len(indices) == 0 {
indices = make([]int, len(turn.variants))
for i := range indices {
indices[i] = i
}
}
selected, err := randomPaddingIndex(len(indices))
if err != nil {
return 0, nil, nil, err
}
variantIndex := indices[selected]
if variantIndex < 0 || variantIndex >= len(turn.variants) {
return 0, nil, nil, fmt.Errorf("invalid send variant index: %d", variantIndex)
}
variant := turn.variants[variantIndex]
targetLength := paddingVariantLength(variant)
chunks, delays, err := trimPaddingPrefix(variant, prefixLength)
if err != nil {
return 0, nil, nil, err
}
return targetLength, chunks, delays, nil
}
func trimPaddingPrefix(variant paddingVariant, prefixLength int) ([]int, []paddingDelayRange, error) {
remainingPrefix := prefixLength
firstChunk := 0
for firstChunk < len(variant.chunks) && remainingPrefix > 0 {
chunkLength := variant.chunks[firstChunk]
if remainingPrefix < chunkLength {
return nil, nil, fmt.Errorf("prefix length %d splits chunk %d", prefixLength, firstChunk)
}
remainingPrefix -= chunkLength
firstChunk++
}
if remainingPrefix != 0 || firstChunk == len(variant.chunks) {
return nil, nil, fmt.Errorf("prefix length %d leaves no padding record", prefixLength)
}
chunks := append([]int(nil), variant.chunks[firstChunk:]...)
delays := make([]paddingDelayRange, len(chunks))
if len(variant.delays) > 0 {
copy(delays, variant.delays[firstChunk:])
}
return chunks, delays, nil
}
func defaultPaddingChunks(recordLength, writeChunkLength int) []int {
chunks := make([]int, 0, (recordLength+writeChunkLength-1)/writeChunkLength)
for remaining := recordLength; remaining > 0; {
chunkLength := min(remaining, writeChunkLength)
chunks = append(chunks, chunkLength)
remaining -= chunkLength
}
return chunks
}
func paddingTurnBounds(turn paddingTurn) (int, int, error) {
if len(turn.variants) == 0 {
if turn.minLength < 1 || turn.maxLength < turn.minLength || turn.maxLength > maxPaddingTurnLength {
return 0, 0, fmt.Errorf("invalid range: %d-%d", turn.minLength, turn.maxLength)
}
return turn.minLength, turn.maxLength, nil
}
if turn.minLength != 0 || turn.maxLength != 0 {
return 0, 0, fmt.Errorf("variants cannot be combined with a length range")
}
minLength := maxPaddingTurnLength + 1
maxLength := 0
for i, variant := range turn.variants {
if len(variant.chunks) == 0 {
return 0, 0, fmt.Errorf("variant %d has no chunks", i)
}
if len(variant.delays) != 0 && len(variant.delays) != len(variant.chunks) {
return 0, 0, fmt.Errorf("variant %d has %d chunks and %d delays", i, len(variant.chunks), len(variant.delays))
}
for j, chunkLength := range variant.chunks {
if chunkLength < 1 || chunkLength > maxPaddingChunkLength {
return 0, 0, fmt.Errorf("variant %d chunk %d has invalid length: %d", i, j, chunkLength)
}
if len(variant.delays) > 0 {
if err := validatePaddingDelayRange(variant.delays[j]); err != nil {
return 0, 0, fmt.Errorf("variant %d chunk %d has an invalid delay: %w", i, j, err)
}
}
}
length := paddingVariantLength(variant)
if length > maxPaddingTurnLength {
return 0, 0, fmt.Errorf("variant %d is too long: %d", i, length)
}
minLength = min(minLength, length)
maxLength = max(maxLength, length)
}
for _, index := range turn.sendVariants {
if index < 0 || index >= len(turn.variants) {
return 0, 0, fmt.Errorf("invalid send variant index: %d", index)
}
}
return minLength, maxLength, nil
}
func paddingTurnAcceptsLength(turn paddingTurn, length int) bool {
if len(turn.variants) == 0 {
return length >= turn.minLength && length <= turn.maxLength
}
for _, variant := range turn.variants {
if paddingVariantLength(variant) == length {
return true
}
}
return false
}
func paddingVariantLength(variant paddingVariant) int {
total := 0
for _, chunkLength := range variant.chunks {
total += chunkLength
}
return total
}
func validatePaddingDelayRange(delay paddingDelayRange) error {
if delay.min < 0 || delay.max < delay.min {
return fmt.Errorf("invalid range: %s-%s", delay.min, delay.max)
}
return nil
}
func randomPaddingDelay(delay paddingDelayRange) (time.Duration, error) {
if err := validatePaddingDelayRange(delay); err != nil {
return 0, err
}
if delay.min == delay.max {
return delay.min, nil
}
span := int64(delay.max-delay.min) + 1
offset, err := rand.Int(rand.Reader, big.NewInt(span))
if err != nil {
return 0, fmt.Errorf("select padding delay: %w", err)
}
return delay.min + time.Duration(offset.Int64()), nil
}
func randomPaddingIndex(length int) (int, error) {
if length < 1 {
return 0, fmt.Errorf("select from empty padding choices")
}
if length == 1 {
return 0, nil
}
index, err := rand.Int(rand.Reader, big.NewInt(int64(length)))
if err != nil {
return 0, fmt.Errorf("select padding choice: %w", err)
}
return int(index.Int64()), nil
}
func randomPaddingTarget(minLength, maxLength int) (int, error) {
if minLength == maxLength {
return minLength, nil
}
span := int64(maxLength-minLength) + 1
offset, err := rand.Int(rand.Reader, big.NewInt(span))
if err != nil {
return 0, fmt.Errorf("select padding length: %w", err)
}
return minLength + int(offset.Int64()), nil
}
@@ -1,282 +0,0 @@
package xmc
import (
"time"
)
// Length and write-boundary templates come from controlled Minecraft 26.1.2
// logins. Timing deliberately uses broad random bands that preserve only the
// rough ordering of short and long phases; it does not replay captured delays.
var startupPaddingSchedule2612 = []paddingTurn{
{
direction: paddingClientToServer,
variants: []paddingVariant{
paddingVariantFromChunks(2, 26, 16),
},
},
{
direction: paddingServerToClient,
variants: []paddingVariant{
paddingVariantFromChunks(26, 21, 25),
},
startDelay: millisecondRange(0, 20),
},
{
direction: paddingClientToServer,
variants: []paddingVariant{
paddingVariantFromChunks(25),
},
startDelay: millisecondRange(2, 22),
},
{
direction: paddingServerToClient,
variants: []paddingVariant{
registryPaddingVariant(),
},
startDelay: millisecondRange(20, 50),
},
{
direction: paddingClientToServer,
variants: []paddingVariant{
paddingVariantFromChunks(2),
},
startDelay: millisecondRange(10, 35),
},
{
direction: paddingServerToClient,
variants: []paddingVariant{
playStartPaddingVariant(4941, 252, 259, 267, 268, 251, 303, 259, 264, 54, 346),
playStartPaddingVariant(4941, 262, 284, 272, 260, 260, 313, 264, 151, 224, 207, 215, 224, 390),
playStartPaddingVariant(4941, 257, 272, 275, 260, 260, 313, 283, 274, 226, 207, 230, 215, 204, 221, 352),
playStartPaddingVariant(4941, 259, 272, 288, 260, 260, 311, 270, 70, 236, 223, 201, 210, 352),
playStartPaddingVariant(4941, 255, 269, 277, 263, 260, 136, 207, 210, 232, 325),
playStartPaddingVariant(4941, 259, 270, 274, 263, 258, 327, 170, 210, 375),
playStartPaddingVariant(4941, 257, 275, 291, 260, 260, 325, 269, 70, 230, 226, 207, 221, 352),
playStartPaddingVariant(4941, 252, 273, 262, 252, 254, 306, 93),
playStartPaddingVariant(4941, 273, 270, 269, 258, 256, 322, 221, 207, 215, 438),
playStartPaddingVariant(4941, 259, 275, 274, 250, 258, 308, 267, 154, 233, 209, 207, 213, 393),
playStartPaddingVariant(4941, 254, 267, 272, 260, 253, 311, 167, 204, 232, 207, 481, 8),
playStartPaddingVariant(4941, 259, 269, 272, 261, 313, 207, 213, 500, 19),
playStartPaddingVariant(4941, 262, 269, 274, 263, 274, 311, 270, 242, 210, 229, 221, 210, 431),
playStartPaddingVariant(4941, 259, 265, 277, 263, 277, 316, 269, 156, 204, 210, 226, 207, 413),
playStartPaddingVariant(4941, 215, 251, 249, 317, 260, 270, 249, 52),
playStartPaddingVariant(4941, 224, 263, 277, 316, 267, 272, 260, 138, 230, 226, 207, 204, 352),
playStartPaddingVariant(4941, 221, 258, 263, 319, 269, 288, 263, 136, 204, 210, 220, 207, 378),
playStartPaddingVariant(4941, 221, 258, 260, 316, 273, 291, 226, 204, 229, 213, 489, 8),
playStartPaddingVariant(4941, 238, 260, 261, 306, 272, 277, 260, 224, 241, 212, 207, 204, 393),
playStartPaddingVariant(4941, 224, 260, 260, 309, 272, 277, 277, 138, 207, 207, 212, 241, 352),
},
startDelay: millisecondRange(35, 50),
},
}
// These turns cover the finite Play-state tail through the client's
// player_loaded packet. Bounds are the observed per-turn minima and maxima
// across 20 controlled 26.1.2 logins; payload bytes remain opaque padding.
var playJoinPaddingSchedule2612 = []paddingTurn{
clientPlayPaddingTurn(6, 883),
serverPlayPaddingTurn(346, 58638),
clientPlayPaddingTurn(6, 887),
serverPlayPaddingTurn(388, 61077),
clientPlayPaddingTurn(2, 50),
serverPlayPaddingTurn(575, 65584),
clientPlayPaddingTurn(6, 45),
serverPlayPaddingTurn(86, 63563),
clientPlayPaddingTurn(2, 44),
serverPlayPaddingTurn(42, 51983),
clientPlayPaddingTurn(2, 851),
serverPlayPaddingTurn(309, 25083),
clientPlayPaddingTurn(2, 19),
serverPlayPaddingTurn(74, 63885),
clientPlayPaddingTurn(8, 24),
serverPlayPaddingTurn(30, 66128),
clientPlayPaddingTurn(2, 19),
serverPlayPaddingTurn(26, 35818),
clientPlayPaddingTurn(6, 19),
serverPlayPaddingTurn(35, 59407),
clientPlayPaddingTurn(6, 19),
serverPlayPaddingTurn(37, 65328),
clientPlayPaddingTurn(2, 19),
serverPlayPaddingTurn(26, 60622),
clientPlayPaddingTurn(6, 19),
serverPlayPaddingTurn(11, 60808),
clientPlayPaddingTurn(8, 43),
serverPlayPaddingTurn(55, 62027),
clientPlayPaddingTurn(2, 19),
serverPlayPaddingTurn(427, 65622),
clientPlayPaddingTurn(5, 19),
serverPlayPaddingTurn(35, 59401),
clientPlayPaddingTurn(6, 19),
}
type paddingLengthRange2612 struct {
minimum int
maximum int
}
type serverPlayLengthBranches2612 struct {
small paddingLengthRange2612
large paddingLengthRange2612
}
var serverPlayBranches2612 = []serverPlayLengthBranches2612{
{small: paddingLengthRange2612{346, 18812}, large: paddingLengthRange2612{51702, 58638}},
{small: paddingLengthRange2612{388, 20689}, large: paddingLengthRange2612{51445, 61077}},
{small: paddingLengthRange2612{575, 20915}, large: paddingLengthRange2612{41428, 65584}},
{small: paddingLengthRange2612{86, 2772}, large: paddingLengthRange2612{41428, 63563}},
{small: paddingLengthRange2612{42, 26813}, large: paddingLengthRange2612{51983, 51983}},
{small: paddingLengthRange2612{309, 19484}, large: paddingLengthRange2612{24837, 25083}},
{small: paddingLengthRange2612{74, 40686}, large: paddingLengthRange2612{63885, 63885}},
{small: paddingLengthRange2612{30, 44114}, large: paddingLengthRange2612{66128, 66128}},
{small: paddingLengthRange2612{26, 1464}, large: paddingLengthRange2612{9941, 35818}},
{small: paddingLengthRange2612{35, 42885}, large: paddingLengthRange2612{52194, 59407}},
{small: paddingLengthRange2612{37, 47553}, large: paddingLengthRange2612{61765, 65328}},
{small: paddingLengthRange2612{26, 1121}, large: paddingLengthRange2612{16162, 60622}},
{small: paddingLengthRange2612{11, 45629}, large: paddingLengthRange2612{60808, 60808}},
{small: paddingLengthRange2612{55, 10035}, large: paddingLengthRange2612{30237, 62027}},
{small: paddingLengthRange2612{427, 52536}, large: paddingLengthRange2612{64014, 65622}},
{small: paddingLengthRange2612{35, 22708}, large: paddingLengthRange2612{38987, 59401}},
}
// Each mask preserves only the small/large branch order from one baseline
// login. Actual lengths and timing are selected randomly inside each branch.
var serverPlayBranchMasks2612 = []uint32{
0x011c, 0x090a, 0x0821, 0xe921, 0x2102,
0x0844, 0xa101, 0x1106, 0x2e00, 0xab01,
0xe900, 0xac01, 0xab01, 0x8b80, 0x0808,
0x2001, 0x0901, 0x000a, 0x2c01, 0x0801,
}
type clientPlayBurst2612 struct {
playIndex int
regular paddingLengthRange2612
burst paddingLengthRange2612
}
var clientPlayBursts2612 = []clientPlayBurst2612{
{playIndex: 0, regular: paddingLengthRange2612{6, 44}, burst: paddingLengthRange2612{877, 883}},
{playIndex: 2, regular: paddingLengthRange2612{6, 45}, burst: paddingLengthRange2612{884, 887}},
{playIndex: 10, regular: paddingLengthRange2612{2, 19}, burst: paddingLengthRange2612{851, 851}},
}
// The 20 samples placed the one client initialization burst in these slots.
var clientPlayBurstChoices2612 = []int{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1,
2,
}
var paddingSchedule2612 = buildPaddingSchedule2612()
func buildPaddingSchedule2612() []paddingTurn {
schedule := make([]paddingTurn, 0, len(startupPaddingSchedule2612)+len(playJoinPaddingSchedule2612))
schedule = append(schedule, startupPaddingSchedule2612...)
schedule = append(schedule, playJoinPaddingSchedule2612...)
return schedule
}
func clientPlayPaddingTurn(minimum, maximum int) paddingTurn {
return paddingTurn{
direction: paddingClientToServer,
minLength: minimum,
maxLength: maximum,
startDelay: millisecondRange(1, 30),
writeChunkLength: 1024,
}
}
func serverPlayPaddingTurn(minimum, maximum int) paddingTurn {
return paddingTurn{
direction: paddingServerToClient,
minLength: minimum,
maxLength: maximum,
startDelay: millisecondRange(1, 45),
chunkDelay: millisecondRange(1, 4),
writeChunkMinLength: 32 * 1024,
writeChunkLength: maxPaddingChunkLength,
}
}
func newClientPaddingSchedule2612() ([]paddingTurn, error) {
choice, err := randomPaddingIndex(len(clientPlayBurstChoices2612))
if err != nil {
return nil, err
}
selectedBurst := clientPlayBurstChoices2612[choice]
schedule := append([]paddingTurn(nil), paddingSchedule2612...)
for i, burst := range clientPlayBursts2612 {
lengthRange := burst.regular
if i == selectedBurst {
lengthRange = burst.burst
}
turn := &schedule[len(startupPaddingSchedule2612)+burst.playIndex]
turn.sendMinLength = lengthRange.minimum
turn.sendMaxLength = lengthRange.maximum
}
return schedule, nil
}
type paddingPause struct {
chunk int
delay paddingDelayRange
}
func paddingVariantFromChunks(chunks ...int) paddingVariant {
return paddingVariant{chunks: chunks}
}
func pacedPaddingVariant(chunks []int, pauses ...paddingPause) paddingVariant {
delays := make([]paddingDelayRange, len(chunks))
for _, pause := range pauses {
if pause.chunk < 0 || pause.chunk >= len(delays) {
panic("xmc: padding pause index is outside its chunk template")
}
delays[pause.chunk] = pause.delay
}
return paddingVariant{chunks: chunks, delays: delays}
}
func registryPaddingVariant() paddingVariant {
return pacedPaddingVariant(
[]int{1590, 226, 329, 229, 186, 151, 78, 81, 79, 235, 67, 67, 78, 71, 82, 74, 982, 117, 1118, 1038, 970, 400, 239, 49, 50, 95, 65, 104, 32320, 2},
paddingPause{28, millisecondRange(1, 4)},
paddingPause{29, millisecondRange(44, 61)},
)
}
func playStartPaddingVariant(chunks ...int) paddingVariant {
if len(chunks) < 2 {
panic("xmc: play start padding variant needs at least two chunks")
}
return pacedPaddingVariant(
chunks,
paddingPause{len(chunks) / 2, millisecondRange(1, 5)},
paddingPause{len(chunks) - 1, millisecondRange(9, 20)},
)
}
func millisecondRange(minimum, maximum int) paddingDelayRange {
return paddingDelayRange{
min: time.Duration(minimum) * time.Millisecond,
max: time.Duration(maximum) * time.Millisecond,
}
}
func newServerPaddingSchedule2612() ([]paddingTurn, error) {
schedule := append([]paddingTurn(nil), paddingSchedule2612...)
profileIndex, err := randomPaddingIndex(len(serverPlayBranchMasks2612))
if err != nil {
return nil, err
}
profile := serverPlayBranchMasks2612[profileIndex]
for i, branches := range serverPlayBranches2612 {
lengthRange := branches.small
if profile&(1<<i) != 0 {
lengthRange = branches.large
}
turn := &schedule[len(startupPaddingSchedule2612)+1+i*2]
turn.sendMinLength = lengthRange.minimum
turn.sendMaxLength = lengthRange.maximum
}
return schedule, nil
}
@@ -1,618 +0,0 @@
package xmc
import (
"bytes"
"errors"
"io"
"net"
"strconv"
"strings"
"testing"
"time"
)
func TestPaddingTurnReachesFinalTargetLength(t *testing.T) {
turn := paddingTurn{direction: paddingClientToServer, minLength: 128, maxLength: 128}
const prefixLength = 3
var encoded bytes.Buffer
if err := writePaddingTurn(&encoded, turn, prefixLength); err != nil {
t.Fatal(err)
}
if got := prefixLength + encoded.Len(); got != turn.minLength {
t.Fatalf("total turn length = %d, want %d", got, turn.minLength)
}
encodedReader := bytes.NewReader(encoded.Bytes())
var recordLength Varint
if err := recordLength.readFrom(encodedReader); err != nil {
t.Fatal(err)
}
if got := int(recordLength); got != encoded.Len() {
t.Fatalf("record length = %d, encoded = %d", got, encoded.Len())
}
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, prefixLength); err != nil {
t.Fatal(err)
}
}
func TestPaddingTurnSupportsThreeByteTarget(t *testing.T) {
turn := paddingTurn{direction: paddingClientToServer, minLength: 3, maxLength: 3}
var encoded bytes.Buffer
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
t.Fatal(err)
}
if got := encoded.Len(); got != 3 {
t.Fatalf("padding length = %d, want 3", got)
}
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, 0); err != nil {
t.Fatal(err)
}
}
func TestPaddingTurnVarintBoundaries(t *testing.T) {
for _, targetLength := range []int{127, 128, 16383, 16384} {
t.Run(strconv.Itoa(targetLength), func(t *testing.T) {
turn := paddingTurn{direction: paddingClientToServer, minLength: targetLength, maxLength: targetLength}
var encoded bytes.Buffer
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
t.Fatal(err)
}
if encoded.Len() != targetLength {
t.Fatalf("padding length = %d, want %d", encoded.Len(), targetLength)
}
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, 0); err != nil {
t.Fatal(err)
}
})
}
}
func TestPaddingTurnRandomRange(t *testing.T) {
turn := paddingTurn{direction: paddingServerToClient, minLength: 127, maxLength: 129}
seen := make(map[int]bool)
for range 100 {
var encoded bytes.Buffer
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
t.Fatal(err)
}
if encoded.Len() < turn.minLength || encoded.Len() > turn.maxLength {
t.Fatalf("padding length = %d", encoded.Len())
}
seen[encoded.Len()] = true
}
if len(seen) < 2 {
t.Fatalf("padding range did not vary: %v", seen)
}
}
func TestPaddingTurnUsesRestrictedSendRange(t *testing.T) {
turn := paddingTurn{
direction: paddingServerToClient,
minLength: 3,
maxLength: 100,
sendMinLength: 90,
sendMaxLength: 100,
}
seen := make(map[int]bool)
for range 100 {
var encoded bytes.Buffer
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
t.Fatal(err)
}
if encoded.Len() < turn.sendMinLength || encoded.Len() > turn.sendMaxLength {
t.Fatalf("padding length = %d", encoded.Len())
}
seen[encoded.Len()] = true
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, 0); err != nil {
t.Fatal(err)
}
}
if len(seen) < 2 {
t.Fatalf("restricted send range did not vary: %v", seen)
}
}
func TestPaddingScheduleSynchronizesDirections(t *testing.T) {
client, server := net.Pipe()
defer client.Close()
defer server.Close()
schedule := []paddingTurn{
{direction: paddingClientToServer, minLength: 33, maxLength: 33},
{direction: paddingServerToClient, minLength: 4097, maxLength: 4097},
{direction: paddingClientToServer, minLength: 16385, maxLength: 16385},
}
serverDone := make(chan error, 1)
go func() {
serverDone <- runPaddingSchedule(server, server, false, 3, schedule)
}()
if err := runPaddingSchedule(client, client, true, 3, schedule); err != nil {
t.Fatal(err)
}
select {
case err := <-serverDone:
if err != nil {
t.Fatal(err)
}
case <-time.After(time.Second):
t.Fatal("server padding schedule did not complete")
}
}
func TestReadPaddingTurnHandlesFragmentedInput(t *testing.T) {
turn := paddingTurn{direction: paddingClientToServer, minLength: 1024, maxLength: 1024}
var encoded bytes.Buffer
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
t.Fatal(err)
}
if err := readPaddingTurn(&oneByteReader{reader: bytes.NewReader(encoded.Bytes())}, turn, 0); err != nil {
t.Fatal(err)
}
}
func TestReadPaddingTurnRejectsInvalidLength(t *testing.T) {
turn := paddingTurn{direction: paddingClientToServer, minLength: 64, maxLength: 96}
data := encodePaddingLength(t, 63)
if err := readPaddingTurn(bytes.NewReader(data), turn, 0); err == nil || !strings.Contains(err.Error(), "outside") {
t.Fatalf("error = %v", err)
}
}
func TestReadPaddingTurnRejectsNonCanonicalHeader(t *testing.T) {
turn := paddingTurn{direction: paddingClientToServer, minLength: 3, maxLength: 3}
err := readPaddingTurn(bytes.NewReader([]byte{0x83, 0x00, 0x00}), turn, 0)
if err == nil || !strings.Contains(err.Error(), "non-canonical") {
t.Fatalf("error = %v", err)
}
}
func TestReadPaddingTurnRejectsTruncatedBody(t *testing.T) {
turn := paddingTurn{direction: paddingClientToServer, minLength: 64, maxLength: 64}
data := encodePaddingLength(t, 64)
if err := readPaddingTurn(bytes.NewReader(data), turn, 0); err == nil || !strings.Contains(err.Error(), "body") {
t.Fatalf("error = %v", err)
}
}
func TestReadPaddingTurnHonorsConnectionTimeout(t *testing.T) {
client, server := net.Pipe()
defer client.Close()
defer server.Close()
if err := server.SetReadDeadline(time.Now().Add(20 * time.Millisecond)); err != nil {
t.Fatal(err)
}
turn := paddingTurn{direction: paddingClientToServer, minLength: 64, maxLength: 64}
err := readPaddingTurn(server, turn, 0)
var netErr net.Error
if !errors.As(err, &netErr) || !netErr.Timeout() {
t.Fatalf("error = %v, want network timeout", err)
}
}
func TestValidatePaddingSchedule(t *testing.T) {
tests := []struct {
name string
schedule []paddingTurn
prefix int
}{
{name: "empty"},
{name: "bad direction", schedule: []paddingTurn{{direction: 99, minLength: 4, maxLength: 4}}},
{name: "too small", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 0, maxLength: 4}}},
{name: "reversed range", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 7}}},
{name: "wrong first direction", prefix: 3, schedule: []paddingTurn{{direction: paddingServerToClient, minLength: 8, maxLength: 8}}},
{name: "prefix leaves no header", prefix: 8, schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8}}},
{name: "same direction", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8}, {direction: paddingClientToServer, minLength: 8, maxLength: 8}}},
{name: "range with variants", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, variants: []paddingVariant{paddingVariantFromChunks(8)}}}},
{name: "empty variant", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{{}}}}},
{name: "bad chunk", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(maxPaddingChunkLength + 1)}}}},
{
name: "delay mismatch",
schedule: []paddingTurn{{
direction: paddingClientToServer,
variants: []paddingVariant{{
chunks: []int{4, 4},
delays: []paddingDelayRange{{min: time.Millisecond, max: time.Millisecond}},
}},
}},
},
{name: "reversed start delay", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, startDelay: paddingDelayRange{min: 2 * time.Millisecond, max: time.Millisecond}}}},
{name: "reversed generated chunk delay", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, chunkDelay: paddingDelayRange{min: 2 * time.Millisecond, max: time.Millisecond}}}},
{name: "oversized generated chunk", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, writeChunkLength: maxPaddingChunkLength + 1}}},
{name: "reversed generated chunk range", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, writeChunkMinLength: 9, writeChunkLength: 8}}},
{name: "variant with generated chunks", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8)}, writeChunkLength: 8}}},
{name: "send range outside accepted range", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 16, sendMinLength: 7, sendMaxLength: 12}}},
{name: "variant with send range", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8)}, sendMinLength: 8, sendMaxLength: 8}}},
{name: "negative chunk delay", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{{chunks: []int{8}, delays: []paddingDelayRange{{min: -time.Millisecond}}}}}}},
{name: "bad send variant", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8)}, sendVariants: []int{1}}}},
{name: "prefix splits chunk", prefix: 3, schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8, 4)}}}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if err := validatePaddingSchedule(test.schedule, test.prefix); err == nil {
t.Fatal("expected invalid padding schedule")
}
})
}
}
func TestPaddingSchedule2612MatchesCapturedTemplates(t *testing.T) {
wantDirections := []paddingDirection{
paddingClientToServer,
paddingServerToClient,
paddingClientToServer,
paddingServerToClient,
paddingClientToServer,
paddingServerToClient,
}
wantLengths := [][]int{
{44},
{72},
{25},
{41172},
{2},
{7464, 8267, 8790, 8153, 7375, 7347, 8184, 6633, 7670, 8241, 7857, 7254, 8407, 8283, 6804, 8177, 8177, 7929, 8296, 8177},
}
if len(paddingSchedule2612) != len(wantDirections)+33 {
t.Fatalf("padding schedule has %d turns, want %d", len(paddingSchedule2612), len(wantDirections)+33)
}
for i, turn := range paddingSchedule2612[:len(wantDirections)] {
if turn.direction != wantDirections[i] {
t.Fatalf("padding turn %d direction = %d, want %d", i, turn.direction, wantDirections[i])
}
if len(turn.variants) != len(wantLengths[i]) {
t.Fatalf("padding turn %d has %d variants, want %d", i, len(turn.variants), len(wantLengths[i]))
}
for j, variant := range turn.variants {
if got := paddingVariantLength(variant); got != wantLengths[i][j] {
t.Fatalf("padding turn %d variant %d length = %d, want %d", i, j, got, wantLengths[i][j])
}
}
}
wantPlayBounds := [][2]int{
{6, 883},
{346, 58638},
{6, 887},
{388, 61077},
{2, 50},
{575, 65584},
{6, 45},
{86, 63563},
{2, 44},
{42, 51983},
{2, 851},
{309, 25083},
{2, 19},
{74, 63885},
{8, 24},
{30, 66128},
{2, 19},
{26, 35818},
{6, 19},
{35, 59407},
{6, 19},
{37, 65328},
{2, 19},
{26, 60622},
{6, 19},
{11, 60808},
{8, 43},
{55, 62027},
{2, 19},
{427, 65622},
{5, 19},
{35, 59401},
{6, 19},
}
for i, want := range wantPlayBounds {
turn := paddingSchedule2612[len(wantDirections)+i]
wantDirection := paddingClientToServer
if i%2 == 1 {
wantDirection = paddingServerToClient
}
if turn.direction != wantDirection {
t.Fatalf("play turn %d direction = %d, want %d", i, turn.direction, wantDirection)
}
if turn.minLength != want[0] || turn.maxLength != want[1] {
t.Fatalf("play turn %d bounds = %d-%d, want %d-%d", i, turn.minLength, turn.maxLength, want[0], want[1])
}
if len(turn.variants) != 0 {
t.Fatalf("play turn %d unexpectedly has captured variants", i)
}
}
if got := len(paddingSchedule2612[3].variants[0].chunks); got != 30 {
t.Fatalf("registry turn chunks = %d, want 30", got)
}
minimumPlayStart := maxPaddingTurnLength
maximumPlayStart := 0
for _, variant := range paddingSchedule2612[5].variants {
length := paddingVariantLength(variant)
minimumPlayStart = min(minimumPlayStart, length)
maximumPlayStart = max(maximumPlayStart, length)
if variant.chunks[0] != 4941 {
t.Fatalf("play start first chunk = %d, want 4941", variant.chunks[0])
}
}
if minimumPlayStart != 6633 || maximumPlayStart != 8790 {
t.Fatalf("play start bounds = %d-%d, want 6633-8790", minimumPlayStart, maximumPlayStart)
}
if err := validatePaddingSchedule(paddingSchedule2612, 2); err != nil {
t.Fatalf("captured schedule is invalid: %v", err)
}
serverSchedule, err := newServerPaddingSchedule2612()
if err != nil {
t.Fatal(err)
}
if err = validatePaddingSchedule(serverSchedule, 2); err != nil {
t.Fatalf("server schedule is invalid: %v", err)
}
for i, branches := range serverPlayBranches2612 {
turn := serverSchedule[len(startupPaddingSchedule2612)+1+i*2]
got := paddingLengthRange2612{turn.sendMinLength, turn.sendMaxLength}
if got != branches.small && got != branches.large {
t.Fatalf("server play turn %d send range = %v, want %v or %v", i, got, branches.small, branches.large)
}
}
for range 20 {
clientSchedule, clientErr := newClientPaddingSchedule2612()
if clientErr != nil {
t.Fatal(clientErr)
}
if clientErr = validatePaddingSchedule(clientSchedule, 2); clientErr != nil {
t.Fatalf("client schedule is invalid: %v", clientErr)
}
burstCount := 0
for _, burst := range clientPlayBursts2612 {
turn := clientSchedule[len(startupPaddingSchedule2612)+burst.playIndex]
got := paddingLengthRange2612{turn.sendMinLength, turn.sendMaxLength}
switch got {
case burst.regular:
case burst.burst:
burstCount++
default:
t.Fatalf("client play turn %d send range = %v", burst.playIndex, got)
}
}
if burstCount != 1 {
t.Fatalf("client schedule has %d initialization bursts, want 1", burstCount)
}
}
for variantIndex := range paddingSchedule2612[3].variants {
turn := paddingSchedule2612[3]
turn.sendVariants = []int{variantIndex}
var encoded bytes.Buffer
if err = writePaddingTurnWithSleep(&encoded, turn, 0, func(time.Duration) {}); err != nil {
t.Fatal(err)
}
if err = readPaddingTurn(bytes.NewReader(encoded.Bytes()), paddingSchedule2612[3], 0); err != nil {
t.Fatalf("registry variant %d was rejected: %v", variantIndex, err)
}
}
}
func TestPaddingVariantPreservesWriteBoundaries(t *testing.T) {
tests := []struct {
name string
turn paddingTurn
prefix int
want []int
}{
{name: "login acknowledged turn", turn: paddingSchedule2612[0], prefix: 2, want: []int{26, 16}},
{name: "server response turn", turn: paddingSchedule2612[1], want: []int{26, 21, 25}},
{name: "single packet turn", turn: paddingSchedule2612[2], want: []int{25}},
{name: "fixed registry profile", turn: paddingSchedule2612[3], want: paddingSchedule2612[3].variants[0].chunks},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var writer recordingWriter
if err := writePaddingTurnWithSleep(&writer, test.turn, test.prefix, func(time.Duration) {}); err != nil {
t.Fatal(err)
}
if len(writer.writes) != len(test.want) {
t.Fatalf("writes = %v, want %v", writer.writes, test.want)
}
for i := range test.want {
if writer.writes[i] != test.want[i] {
t.Fatalf("writes = %v, want %v", writer.writes, test.want)
}
}
if err := readPaddingTurn(bytes.NewReader(writer.Bytes()), test.turn, test.prefix); err != nil {
t.Fatal(err)
}
})
}
}
func TestPaddingVariantAppliesPacing(t *testing.T) {
turn := paddingTurn{
direction: paddingClientToServer,
startDelay: paddingDelayRange{min: 3 * time.Millisecond, max: 3 * time.Millisecond},
variants: []paddingVariant{{
chunks: []int{3, 5, 7},
delays: []paddingDelayRange{
{},
{min: 2 * time.Millisecond, max: 2 * time.Millisecond},
{min: 4 * time.Millisecond, max: 4 * time.Millisecond},
},
}},
}
var slept []time.Duration
var writer recordingWriter
if err := writePaddingTurnWithSleep(&writer, turn, 3, func(delay time.Duration) {
slept = append(slept, delay)
}); err != nil {
t.Fatal(err)
}
want := []time.Duration{3 * time.Millisecond, 2 * time.Millisecond, 4 * time.Millisecond}
if len(slept) != len(want) {
t.Fatalf("delays = %v, want %v", slept, want)
}
for i := range want {
if slept[i] != want[i] {
t.Fatalf("delays = %v, want %v", slept, want)
}
}
}
func TestGeneratedPaddingChunksApplyPacing(t *testing.T) {
turn := paddingTurn{
direction: paddingServerToClient,
minLength: 100,
maxLength: 100,
writeChunkLength: 32,
chunkDelay: paddingDelayRange{min: 2 * time.Millisecond, max: 2 * time.Millisecond},
}
var slept []time.Duration
var writer recordingWriter
if err := writePaddingTurnWithSleep(&writer, turn, 0, func(delay time.Duration) {
slept = append(slept, delay)
}); err != nil {
t.Fatal(err)
}
wantWrites := []int{32, 32, 32, 4}
if !slicesEqual(writer.writes, wantWrites) {
t.Fatalf("writes = %v, want %v", writer.writes, wantWrites)
}
wantSleeps := []time.Duration{2 * time.Millisecond, 2 * time.Millisecond, 2 * time.Millisecond}
if !slicesEqual(slept, wantSleeps) {
t.Fatalf("delays = %v, want %v", slept, wantSleeps)
}
if err := readPaddingTurn(bytes.NewReader(writer.Bytes()), turn, 0); err != nil {
t.Fatal(err)
}
}
func TestGeneratedPaddingChunkLengthIsRandomized(t *testing.T) {
turn := paddingTurn{
direction: paddingServerToClient,
minLength: 100,
maxLength: 100,
writeChunkMinLength: 16,
writeChunkLength: 32,
}
seen := make(map[int]bool)
for range 100 {
var writer recordingWriter
if err := writePaddingTurnWithSleep(&writer, turn, 0, func(time.Duration) {}); err != nil {
t.Fatal(err)
}
firstWrite := writer.writes[0]
if firstWrite < turn.writeChunkMinLength || firstWrite > turn.writeChunkLength {
t.Fatalf("first write = %d", firstWrite)
}
seen[firstWrite] = true
}
if len(seen) < 2 {
t.Fatalf("generated write chunk length did not vary: %v", seen)
}
}
func TestPaddingDelayRangeIsRandomized(t *testing.T) {
delayRange := millisecondRange(25, 40)
seen := make(map[time.Duration]bool)
for range 100 {
delay, err := randomPaddingDelay(delayRange)
if err != nil {
t.Fatal(err)
}
if delay < delayRange.min || delay > delayRange.max {
t.Fatalf("delay = %s, want %s-%s", delay, delayRange.min, delayRange.max)
}
seen[delay] = true
}
if len(seen) < 2 {
t.Fatalf("padding delay did not vary: %v", seen)
}
}
func TestPaddingSchedule2612UsesCoarseTimingBands(t *testing.T) {
assertDelayRange(t, "turn 3 to 4", paddingSchedule2612[3].startDelay, 20*time.Millisecond, 50*time.Millisecond)
assertDelayRange(t, "turn 5 to 6", paddingSchedule2612[5].startDelay, 35*time.Millisecond, 50*time.Millisecond)
assertDelayRange(t, "first play client turn", paddingSchedule2612[6].startDelay, time.Millisecond, 30*time.Millisecond)
assertDelayRange(t, "first play server turn", paddingSchedule2612[7].startDelay, time.Millisecond, 45*time.Millisecond)
assertDelayRange(t, "play server chunk pacing", paddingSchedule2612[7].chunkDelay, time.Millisecond, 4*time.Millisecond)
if paddingSchedule2612[6].writeChunkLength != 1024 {
t.Fatalf("play client write chunk = %d, want 1024", paddingSchedule2612[6].writeChunkLength)
}
if paddingSchedule2612[7].writeChunkLength != maxPaddingChunkLength {
t.Fatalf("play server write chunk = %d, want %d", paddingSchedule2612[7].writeChunkLength, maxPaddingChunkLength)
}
if paddingSchedule2612[7].writeChunkMinLength != 32*1024 {
t.Fatalf("play server minimum write chunk = %d, want %d", paddingSchedule2612[7].writeChunkMinLength, 32*1024)
}
for i, variant := range paddingSchedule2612[3].variants {
minimum, maximum := paddingVariantDelayBounds(variant)
if minimum != 45*time.Millisecond || maximum != 65*time.Millisecond {
t.Fatalf("turn 4 variant %d duration = %s-%s, want 45ms-65ms", i, minimum, maximum)
}
}
for i, variant := range paddingSchedule2612[5].variants {
minimum, maximum := paddingVariantDelayBounds(variant)
if minimum != 10*time.Millisecond || maximum != 25*time.Millisecond {
t.Fatalf("turn 6 variant %d duration = %s-%s, want 10ms-25ms", i, minimum, maximum)
}
}
}
func assertDelayRange(t *testing.T, name string, got paddingDelayRange, minimum, maximum time.Duration) {
t.Helper()
if got.min != minimum || got.max != maximum {
t.Fatalf("%s delay = %s-%s, want %s-%s", name, got.min, got.max, minimum, maximum)
}
}
func paddingVariantDelayBounds(variant paddingVariant) (time.Duration, time.Duration) {
var minimum time.Duration
var maximum time.Duration
for _, delay := range variant.delays {
minimum += delay.min
maximum += delay.max
}
return minimum, maximum
}
func slicesEqual[T comparable](left, right []T) bool {
if len(left) != len(right) {
return false
}
for i := range left {
if left[i] != right[i] {
return false
}
}
return true
}
func encodePaddingLength(t *testing.T, length int) []byte {
t.Helper()
var encoded bytes.Buffer
value := Varint(length)
if err := value.writeTo(&encoded); err != nil {
t.Fatal(err)
}
return encoded.Bytes()
}
type oneByteReader struct {
reader io.Reader
}
func (r *oneByteReader) Read(p []byte) (int, error) {
if len(p) > 1 {
p = p[:1]
}
return r.reader.Read(p)
}
type recordingWriter struct {
bytes.Buffer
writes []int
}
func (w *recordingWriter) Write(p []byte) (int, error) {
w.writes = append(w.writes, len(p))
return w.Buffer.Write(p)
}
@@ -1,69 +0,0 @@
package xmc
import "fmt"
type loginProfile struct {
Username string
UUID UUID
TexturesValue string
TexturesSignature string
}
func profilesFromConfig(configured []*Profile) ([]loginProfile, error) {
if len(configured) == 0 {
return nil, fmt.Errorf("empty profiles")
}
profiles := make([]loginProfile, 0, len(configured))
for _, configuredProfile := range configured {
if configuredProfile == nil || configuredProfile.Username == "" {
return nil, fmt.Errorf("invalid profile")
}
if len(configuredProfile.Uuid) != len(UUID{}) {
return nil, fmt.Errorf("bad profile UUID length: %d", len(configuredProfile.Uuid))
}
if configuredProfile.TexturesValue == "" || configuredProfile.TexturesSignature == "" {
return nil, fmt.Errorf("incomplete profile textures")
}
profile := loginProfile{
Username: configuredProfile.Username,
TexturesValue: configuredProfile.TexturesValue,
TexturesSignature: configuredProfile.TexturesSignature,
}
copy(profile.UUID[:], configuredProfile.Uuid)
profiles = append(profiles, profile)
}
return profiles, nil
}
func findProfile(profiles []loginProfile, username string, uuid UUID) (loginProfile, bool) {
for _, profile := range profiles {
if profile.Username == username && profile.UUID == uuid {
return profile, true
}
}
return loginProfile{}, false
}
func readLoginSuccess(packet *mcPacket) (loginProfile, error) {
var (
profile loginProfile
username String
propertyCount Varint
propertyName String
value String
signed Boolean
signature String
)
if err := packet.readFields(&profile.UUID, &username, &propertyCount, &propertyName, &value, &signed, &signature); err != nil {
return loginProfile{}, err
}
if propertyCount != 1 || propertyName != "textures" || !signed {
return loginProfile{}, fmt.Errorf("invalid login profile properties")
}
profile.Username = string(username)
profile.TexturesValue = string(value)
profile.TexturesSignature = string(signature)
return profile, nil
}
@@ -1,33 +0,0 @@
package xmc
import (
"bytes"
"testing"
)
func TestProfilesFromConfigRejectsEmpty(t *testing.T) {
if _, err := profilesFromConfig(nil); err == nil {
t.Fatal("expected empty profiles error")
}
}
func TestProfilesFromConfig(t *testing.T) {
uuid := bytes.Repeat([]byte{0x2a}, 16)
profiles, err := profilesFromConfig([]*Profile{
{
Username: "SignedUser",
Uuid: uuid,
TexturesValue: "textures-value",
TexturesSignature: "textures-signature",
},
})
if err != nil {
t.Fatalf("build explicit profile: %v", err)
}
if len(profiles) != 1 || profiles[0].Username != "SignedUser" {
t.Fatalf("unexpected profile: %+v", profiles)
}
if profiles[0].TexturesValue != "textures-value" || profiles[0].TexturesSignature != "textures-signature" {
t.Fatalf("textures were not preserved: %+v", profiles[0])
}
}
@@ -1,444 +0,0 @@
// 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)))
}
@@ -1,56 +0,0 @@
package xmc
import (
"bytes"
"strings"
"testing"
)
func TestReadPacketDoesNotConsumeFollowingPacket(t *testing.T) {
data := []byte{0x01, 0x80, 0x01, 0x00}
r := bytes.NewReader(data)
if _, err := readPacket(r); err == nil {
t.Fatal("expected truncated packet ID to fail")
}
pkt, err := readPacket(r)
if err != nil {
t.Fatalf("read following packet: %v", err)
}
if pkt.packetID != 0 {
t.Fatalf("packet ID = %d", pkt.packetID)
}
}
func TestPacketWithLengthReportsWireBytes(t *testing.T) {
var wire bytes.Buffer
written, err := writePacketWithLength(&wire, 0x03)
if err != nil {
t.Fatal(err)
}
if written != 2 || !bytes.Equal(wire.Bytes(), []byte{0x01, 0x03}) {
t.Fatalf("wire = %x, length = %d", wire.Bytes(), written)
}
packet, read, err := readPacketWithLength(bytes.NewReader(wire.Bytes()))
if err != nil {
t.Fatal(err)
}
if packet.packetID != 0x03 || read != written {
t.Fatalf("packet ID = %d, read = %d, written = %d", packet.packetID, read, written)
}
}
func TestReadPacketRejectsNonCanonicalLengthVarint(t *testing.T) {
_, _, err := readPacketWithLength(bytes.NewReader([]byte{0x81, 0x00, 0x03}))
if err == nil || !strings.Contains(err.Error(), "non-canonical") {
t.Fatalf("error = %v", err)
}
}
func TestVarintRejectsOversizedFifthByte(t *testing.T) {
var value Varint
err := value.readFrom(bytes.NewReader([]byte{0xff, 0xff, 0xff, 0xff, 0x1f}))
if err == nil || !strings.Contains(err.Error(), "too large") {
t.Fatalf("error = %v", err)
}
}
-364
View File
@@ -1,364 +0,0 @@
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
}
@@ -1,79 +0,0 @@
// 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
}

Some files were not shown because too many files have changed in this diff Show More