mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-09-16 06:20:28 +00:00
Compare commits
53
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8cdf7d238 | ||
|
|
5e245b082e | ||
|
|
d9c54026c5 | ||
|
|
c1958dba04 | ||
|
|
540b9070f5 | ||
|
|
ada99a4eb0 | ||
|
|
65458e919f | ||
|
|
aa3d6589da | ||
|
|
25c11e2d2b | ||
|
|
dffc7ada5e | ||
|
|
77f98eba09 | ||
|
|
f124daf5a3 | ||
|
|
598bde7412 | ||
|
|
9b373e39ca | ||
|
|
c7e569b037 | ||
|
|
f02a357861 | ||
|
|
5fe6d6217a | ||
|
|
0604ffa957 | ||
|
|
d3f1a24285 | ||
|
|
2323273e37 | ||
|
|
09107b71dc | ||
|
|
7021606ad3 | ||
|
|
7d214f8b09 | ||
|
|
8b419d833d | ||
|
|
a12801c13b | ||
|
|
a000371b2a | ||
|
|
bc6e966af8 | ||
|
|
fc5620de98 | ||
|
|
b02bdcf4cc | ||
|
|
2b329b3675 | ||
|
|
5ca6f4b7d4 | ||
|
|
18e283909c | ||
|
|
6ab123bf8f | ||
|
|
4aba687dd3 | ||
|
|
5b1b41058e | ||
|
|
6e3322d219 | ||
|
|
1d8eb81d70 | ||
|
|
e78d8ef184 | ||
|
|
6ce924ad56 | ||
|
|
035d438979 | ||
|
|
50231eaff9 | ||
|
|
1f74c480d6 | ||
|
|
af7eb68028 | ||
|
|
35387572e0 | ||
|
|
8f15190c23 | ||
|
|
64fada32b5 | ||
|
|
0bafca9486 | ||
|
|
d5bc58dc6b | ||
|
|
c18b39ed80 | ||
|
|
e2ad0acf60 | ||
|
|
c320e89108 | ||
|
|
412898fed7 | ||
|
|
5c62d50d43 |
@@ -37,6 +37,7 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
|
||||
|
||||
# Create log files
|
||||
|
||||
@@ -37,6 +37,7 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
|
||||
|
||||
# Create log files
|
||||
|
||||
@@ -64,6 +64,14 @@ jobs:
|
||||
echo "Latest: '$LATEST'."
|
||||
echo "LATEST=$LATEST" >>${GITHUB_ENV}
|
||||
|
||||
NEWEST=false
|
||||
if [[ "${{ github.event_name }}" == "release" ]]; then
|
||||
NEWEST=true
|
||||
fi
|
||||
|
||||
echo "Newest: '$NEWEST'."
|
||||
echo "NEWEST=$NEWEST" >>${GITHUB_ENV}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
|
||||
@@ -74,7 +82,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -124,6 +132,13 @@ jobs:
|
||||
${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
|
||||
fi
|
||||
|
||||
if [[ "${{ env.NEWEST }}" == "true" ]]; then
|
||||
echo "Adding 'pre-release' tag to manifest: '${{ env.FULL_IMAGE_NAME }}:pre-release'."
|
||||
docker buildx imagetools create \
|
||||
--tag ${{ env.FULL_IMAGE_NAME }}:pre-release \
|
||||
${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
|
||||
fi
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
|
||||
@@ -131,3 +146,7 @@ jobs:
|
||||
if [[ "${{ env.LATEST }}" == "true" ]]; then
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:latest
|
||||
fi
|
||||
|
||||
if [[ "${{ env.NEWEST }}" == "true" ]]; then
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:pre-release
|
||||
fi
|
||||
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
@@ -193,7 +193,7 @@ jobs:
|
||||
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v7
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v7
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
@@ -162,7 +162,7 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran
|
||||
p := d.policy.ForLevel(user.Level)
|
||||
if p.Stats.UserUplink {
|
||||
name := "user>>>" + user.Email + ">>>traffic>>>uplink"
|
||||
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
|
||||
if c, _ := d.stats.GetOrRegisterCounter(name); c != nil {
|
||||
inboundLink.Writer = &SizeStatWriter{
|
||||
Counter: c,
|
||||
Writer: inboundLink.Writer,
|
||||
@@ -171,7 +171,7 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran
|
||||
}
|
||||
if p.Stats.UserDownlink {
|
||||
name := "user>>>" + user.Email + ">>>traffic>>>downlink"
|
||||
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
|
||||
if c, _ := d.stats.GetOrRegisterCounter(name); c != nil {
|
||||
outboundLink.Writer = &SizeStatWriter{
|
||||
Counter: c,
|
||||
Writer: outboundLink.Writer,
|
||||
@@ -200,13 +200,13 @@ func WrapLink(ctx context.Context, policyManager policy.Manager, statsManager st
|
||||
p := policyManager.ForLevel(user.Level)
|
||||
if p.Stats.UserUplink {
|
||||
name := "user>>>" + user.Email + ">>>traffic>>>uplink"
|
||||
if c, _ := stats.GetOrRegisterCounter(statsManager, name); c != nil {
|
||||
if c, _ := statsManager.GetOrRegisterCounter(name); c != nil {
|
||||
link.Reader.(*buf.TimeoutWrapperReader).Counter = c
|
||||
}
|
||||
}
|
||||
if p.Stats.UserDownlink {
|
||||
name := "user>>>" + user.Email + ">>>traffic>>>downlink"
|
||||
if c, _ := stats.GetOrRegisterCounter(statsManager, name); c != nil {
|
||||
if c, _ := statsManager.GetOrRegisterCounter(name); c != nil {
|
||||
link.Writer = &SizeStatWriter{
|
||||
Counter: c,
|
||||
Writer: link.Writer,
|
||||
@@ -223,7 +223,7 @@ func WrapLink(ctx context.Context, policyManager policy.Manager, statsManager st
|
||||
|
||||
func trackOnlineIP(ctx context.Context, sm stats.Manager, email, ip string) {
|
||||
name := "user>>>" + email + ">>>online"
|
||||
if om, _ := stats.GetOrRegisterOnlineMap(sm, name); om != nil {
|
||||
if om, _ := sm.GetOrRegisterOnlineMap(name); om != nil {
|
||||
om.AddIP(ip)
|
||||
context.AfterFunc(ctx, func() { om.RemoveIP(ip) })
|
||||
}
|
||||
|
||||
@@ -78,6 +78,12 @@ 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 {
|
||||
|
||||
@@ -26,7 +26,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
|
||||
if len(tag) > 0 && policy.ForSystem().Stats.InboundUplink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + tag + ">>>traffic>>>uplink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
uplinkCounter = c
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
|
||||
if len(tag) > 0 && policy.ForSystem().Stats.InboundDownlink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + tag + ">>>traffic>>>downlink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
downlinkCounter = c
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
|
||||
if len(tag) > 0 && policy.ForSystem().Stats.OutboundUplink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "outbound>>>" + tag + ">>>traffic>>>uplink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
uplinkCounter = c
|
||||
}
|
||||
@@ -47,7 +47,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
|
||||
if len(tag) > 0 && policy.ForSystem().Stats.OutboundDownlink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "outbound>>>" + tag + ">>>traffic>>>downlink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
downlinkCounter = c
|
||||
}
|
||||
@@ -347,7 +347,6 @@ func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound)
|
||||
// case addr.Family().IsDomain():
|
||||
default:
|
||||
ob.Gateway = addr
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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[tag]; ok {
|
||||
if b, ok := (*r.balancers.Load())[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[tag]; ok {
|
||||
if b, ok := (*r.balancers.Load())[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[tag]; ok {
|
||||
if b, ok := (*r.balancers.Load())[tag]; ok {
|
||||
return b.override.Get(), nil
|
||||
}
|
||||
return "", errors.New("cannot find tag")
|
||||
|
||||
@@ -2,25 +2,8 @@ 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
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
@@ -393,3 +394,22 @@ 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
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package router_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/xtls/xray-core/app/router"
|
||||
@@ -343,6 +345,31 @@ 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)
|
||||
|
||||
@@ -33,6 +33,10 @@ 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))
|
||||
}
|
||||
|
||||
+14
-4
@@ -107,8 +107,10 @@ 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"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
// 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
|
||||
}
|
||||
|
||||
func (x *RoutingRule) Reset() {
|
||||
@@ -278,6 +280,13 @@ 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()
|
||||
}
|
||||
@@ -637,7 +646,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\"\xc1\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\"\xdc\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" +
|
||||
@@ -661,7 +670,8 @@ 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\x1a=\n" +
|
||||
"\awebhook\x18\x16 \x01(\v2\x1e.xray.app.router.WebhookConfigR\awebhook\x12\x19\n" +
|
||||
"\blocal_os\x18\x17 \x03(\tR\alocalOs\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" +
|
||||
|
||||
@@ -56,6 +56,9 @@ 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 {
|
||||
|
||||
+59
-114
@@ -2,7 +2,9 @@ package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
@@ -17,8 +19,8 @@ import (
|
||||
// Router is an implementation of routing.Router.
|
||||
type Router struct {
|
||||
domainStrategy Config_DomainStrategy
|
||||
rules []*Rule
|
||||
balancers map[string]*Balancer
|
||||
rules atomic.Pointer[[]*Rule]
|
||||
balancers atomic.Pointer[map[string]*Balancer]
|
||||
dns dns.Client
|
||||
|
||||
ctx context.Context
|
||||
@@ -43,52 +45,9 @@ func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm out
|
||||
r.ohm = ohm
|
||||
r.dispatcher = dispatcher
|
||||
|
||||
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
|
||||
r.rules.Store(new([]*Rule))
|
||||
r.balancers.Store(&map[string]*Balancer{})
|
||||
return r.ReloadRules(config, false)
|
||||
}
|
||||
|
||||
// PickRoute implements routing.Router.
|
||||
@@ -124,18 +83,22 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if !shouldAppend {
|
||||
for _, rule := range r.rules {
|
||||
if rule.Webhook != nil {
|
||||
rule.Webhook.Close()
|
||||
}
|
||||
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
|
||||
}
|
||||
r.balancers = make(map[string]*Balancer, len(config.BalancingRule))
|
||||
r.rules = make([]*Rule, 0, len(config.Rule))
|
||||
}
|
||||
|
||||
for _, rule := range config.BalancingRule {
|
||||
_, found := r.balancers[rule.Tag]
|
||||
if found {
|
||||
if _, found := newBalancers[rule.Tag]; found {
|
||||
return errors.New("duplicate balancer tag")
|
||||
}
|
||||
balancer, err := rule.Build(r.ohm, r.dispatcher)
|
||||
@@ -143,27 +106,12 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
|
||||
return err
|
||||
}
|
||||
balancer.InjectContext(r.ctx)
|
||||
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]
|
||||
newBalancers[rule.Tag] = balancer
|
||||
}
|
||||
|
||||
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{
|
||||
@@ -171,69 +119,64 @@ 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
|
||||
}
|
||||
btag := rule.GetBalancingTag()
|
||||
if len(btag) > 0 {
|
||||
brule, found := r.balancers[btag]
|
||||
if btag := rule.GetBalancingTag(); len(btag) > 0 {
|
||||
brule, found := newBalancers[btag]
|
||||
if !found {
|
||||
if rr.Webhook != nil {
|
||||
rr.Webhook.Close()
|
||||
}
|
||||
closeNewWebhooks()
|
||||
return errors.New("balancer ", btag, " not found")
|
||||
}
|
||||
rr.Balancer = brule
|
||||
}
|
||||
r.rules = append(r.rules, rr)
|
||||
newRules = append(newRules, 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()
|
||||
|
||||
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()
|
||||
}
|
||||
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)
|
||||
}
|
||||
r.rules = newRules
|
||||
return nil
|
||||
}
|
||||
return errors.New("empty tag name!")
|
||||
r.rules.Store(&newRules)
|
||||
closeWebhooks(removed)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListRule implements routing.Router
|
||||
func (r *Router) ListRule() []routing.Route {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
ruleList := make([]routing.Route, 0)
|
||||
for _, rule := range r.rules {
|
||||
rules := *r.rules.Load()
|
||||
ruleList := make([]routing.Route, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
ruleList = append(ruleList, &Route{
|
||||
outboundTag: rule.Tag,
|
||||
ruleTag: rule.RuleTag,
|
||||
@@ -252,7 +195,9 @@ func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context,
|
||||
ctx = routing_dns.ContextWithDNSClient(ctx, r.dns)
|
||||
}
|
||||
|
||||
for _, rule := range r.rules {
|
||||
rules := *r.rules.Load()
|
||||
|
||||
for _, rule := range rules {
|
||||
if rule.Apply(ctx) {
|
||||
return rule, ctx, nil
|
||||
}
|
||||
@@ -265,7 +210,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 r.rules {
|
||||
for _, rule := range rules {
|
||||
if rule.Apply(ctx) {
|
||||
return rule, ctx, nil
|
||||
}
|
||||
@@ -279,9 +224,9 @@ func (r *Router) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// closeWebhooks closes all webhook notifiers in the current rule set.
|
||||
func (r *Router) closeWebhooks() {
|
||||
for _, rule := range r.rules {
|
||||
// closeWebhooks closes all webhook notifiers in the given rule set.
|
||||
func closeWebhooks(rules []*Rule) {
|
||||
for _, rule := range rules {
|
||||
if rule.Webhook != nil {
|
||||
rule.Webhook.Close()
|
||||
}
|
||||
@@ -292,7 +237,7 @@ func (r *Router) closeWebhooks() {
|
||||
func (r *Router) Close() error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.closeWebhooks()
|
||||
closeWebhooks(*r.rules.Load())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+17
-23
@@ -8,6 +8,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
@@ -40,6 +41,7 @@ type WebhookNotifier struct {
|
||||
deduplication uint32
|
||||
client *http.Client
|
||||
seen sync.Map
|
||||
lastSweep atomic.Int64
|
||||
done chan struct{}
|
||||
wg sync.WaitGroup
|
||||
closeOnce sync.Once
|
||||
@@ -77,11 +79,6 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if h.deduplication > 0 {
|
||||
h.wg.Add(1)
|
||||
go h.cleanupLoop()
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
@@ -201,6 +198,7 @@ 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
|
||||
@@ -210,27 +208,23 @@ func (h *WebhookNotifier) isDuplicate(email string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -48,6 +48,20 @@ func (m *Manager) RegisterCounter(name string) (stats.Counter, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// GetOrRegisterCounter implements stats.Manager.
|
||||
func (m *Manager) GetOrRegisterCounter(name string) (stats.Counter, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
|
||||
if c, found := m.counters[name]; found {
|
||||
return c, nil
|
||||
}
|
||||
errors.LogDebug(context.Background(), "create new counter ", name)
|
||||
c := new(Counter)
|
||||
m.counters[name] = c
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// UnregisterCounter implements stats.Manager.
|
||||
func (m *Manager) UnregisterCounter(name string) error {
|
||||
m.access.Lock()
|
||||
@@ -97,6 +111,20 @@ func (m *Manager) RegisterOnlineMap(name string) (stats.OnlineMap, error) {
|
||||
return om, nil
|
||||
}
|
||||
|
||||
// GetOrRegisterOnlineMap implements stats.Manager.
|
||||
func (m *Manager) GetOrRegisterOnlineMap(name string) (stats.OnlineMap, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
|
||||
if om, found := m.onlineMaps[name]; found {
|
||||
return om, nil
|
||||
}
|
||||
errors.LogDebug(context.Background(), "create new OnlineMap ", name)
|
||||
om := NewOnlineMap()
|
||||
m.onlineMaps[name] = om
|
||||
return om, nil
|
||||
}
|
||||
|
||||
// UnregisterOnlineMap implements stats.Manager.
|
||||
func (m *Manager) UnregisterOnlineMap(name string) error {
|
||||
m.access.Lock()
|
||||
@@ -149,6 +177,26 @@ func (m *Manager) RegisterChannel(name string) (stats.Channel, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// GetOrRegisterChannel implements stats.Manager.
|
||||
func (m *Manager) GetOrRegisterChannel(name string) (stats.Channel, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
|
||||
if c, found := m.channels[name]; found {
|
||||
return c, nil
|
||||
}
|
||||
errors.LogDebug(context.Background(), "create new channel ", name)
|
||||
c := NewChannel(&ChannelConfig{BufferSize: 64, Blocking: false})
|
||||
if m.running {
|
||||
// Start before publishing so no goroutine can observe an unstarted channel.
|
||||
if err := c.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
m.channels[name] = c
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// UnregisterChannel implements stats.Manager.
|
||||
func (m *Manager) UnregisterChannel(name string) error {
|
||||
m.access.Lock()
|
||||
|
||||
+1
-1
@@ -122,7 +122,7 @@ func NewReader(reader io.Reader) Reader {
|
||||
}
|
||||
|
||||
_, isFile := reader.(*os.File)
|
||||
if !isFile && useReadv {
|
||||
if !isFile && useReadV() {
|
||||
if sc, ok := reader.(syscall.Conn); ok {
|
||||
rawConn, err := sc.SyscallConn()
|
||||
if err != nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ package buf
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"github.com/xtls/xray-core/common/platform"
|
||||
@@ -143,13 +144,24 @@ func (r *ReadVReader) ReadMultiBuffer() (MultiBuffer, error) {
|
||||
return mb, nil
|
||||
}
|
||||
|
||||
var useReadv bool
|
||||
var useReadv atomic.Bool
|
||||
|
||||
func init() {
|
||||
func useReadV() bool {
|
||||
return useReadv.Load()
|
||||
}
|
||||
|
||||
func reloadEnvSettings() error {
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
value := platform.NewEnvFlag(platform.UseReadV).GetValue(func() string { return defaultFlagValue })
|
||||
enabled := false
|
||||
switch value {
|
||||
case defaultFlagValue, "auto", "enable":
|
||||
useReadv = true
|
||||
enabled = true
|
||||
}
|
||||
useReadv.Store(enabled)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
platform.RegisterEnvReload(reloadEnvSettings)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ import (
|
||||
"github.com/xtls/xray-core/features/stats"
|
||||
)
|
||||
|
||||
const useReadv = false
|
||||
func useReadV() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func NewReadVReader(reader io.Reader, rawConn syscall.RawConn, counter stats.Counter) Reader {
|
||||
panic("not implemented")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build darwin
|
||||
//go:build darwin && !ios
|
||||
|
||||
package net
|
||||
|
||||
@@ -198,7 +198,9 @@ func darwinSocketInfoMatchLevel(info []byte, network string, srcAddr netip.Addr,
|
||||
|
||||
vflag := info[darwinInSockInfoVFlagOff]
|
||||
if srcAddr.Is4() {
|
||||
if family != unix.AF_INET || vflag&darwinInSockInfoIPv4 == 0 {
|
||||
// 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 {
|
||||
return darwinSocketNoMatch
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build darwin
|
||||
//go:build darwin && !ios
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build darwin
|
||||
//go:build darwin && !ios
|
||||
|
||||
package net
|
||||
|
||||
@@ -52,6 +52,57 @@ 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 {
|
||||
@@ -264,6 +315,18 @@ 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 {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build ios
|
||||
|
||||
package net
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (int, string, string, error) {
|
||||
return 0, "", "", errors.New("process lookup is not supported on this platform")
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var envReloadRegistry = struct {
|
||||
sync.RWMutex
|
||||
handlers []func() error
|
||||
}{}
|
||||
|
||||
// RegisterEnvReload registers an environment reload handler and runs it once
|
||||
// immediately so package defaults keep the same behavior as init-time reads.
|
||||
func RegisterEnvReload(handler func() error) {
|
||||
if handler == nil {
|
||||
return
|
||||
}
|
||||
envReloadRegistry.Lock()
|
||||
envReloadRegistry.handlers = append(envReloadRegistry.handlers, handler)
|
||||
envReloadRegistry.Unlock()
|
||||
if err := handler(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// ReloadEnvSettings refreshes all registered environment-backed package state.
|
||||
func ReloadEnvSettings() error {
|
||||
envReloadRegistry.RLock()
|
||||
handlers := append([]func() error{}, envReloadRegistry.handlers...)
|
||||
envReloadRegistry.RUnlock()
|
||||
|
||||
var errs []error
|
||||
for _, handler := range handlers {
|
||||
if err := handler(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
@@ -3,11 +3,8 @@ package bittorrent
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
)
|
||||
|
||||
type SniffHeader struct{}
|
||||
@@ -39,50 +36,44 @@ func SniffUTP(b []byte) (*SniffHeader, error) {
|
||||
return nil, common.ErrNoClue
|
||||
}
|
||||
|
||||
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 {
|
||||
// type 4 (ST_SYN), version 1
|
||||
if b[0] != 0x41 {
|
||||
return nil, errNotBittorrent
|
||||
}
|
||||
|
||||
var extension uint8
|
||||
|
||||
if binary.Read(buffer, binary.BigEndian, &extension) != nil {
|
||||
return nil, common.ErrNoClue
|
||||
} else if extension != 0 && extension != 1 {
|
||||
// timestamp_difference is always 0 in new connections
|
||||
if binary.BigEndian.Uint32(b[8:12]) != 0 {
|
||||
return nil, errNotBittorrent
|
||||
}
|
||||
|
||||
// Walk the extension chain. Selective ack (1) and extension bits (2)
|
||||
extension, offset := b[1], 20
|
||||
for extension != 0 {
|
||||
if extension != 1 {
|
||||
if len(b) < offset+2 {
|
||||
return nil, errNotBittorrent
|
||||
}
|
||||
if binary.Read(buffer, binary.BigEndian, &extension) != nil {
|
||||
return nil, common.ErrNoClue
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
if len(b) < offset+2+length {
|
||||
return nil, errNotBittorrent
|
||||
}
|
||||
extension = b[offset]
|
||||
offset += 2 + length
|
||||
}
|
||||
|
||||
if common.Error2(buffer.ReadBytes(2)) != nil {
|
||||
return nil, common.ErrNoClue
|
||||
}
|
||||
|
||||
var timestamp uint32
|
||||
if err := binary.Read(buffer, binary.BigEndian, ×tamp); err != nil {
|
||||
return nil, common.ErrNoClue
|
||||
}
|
||||
if math.Abs(float64(time.Now().UnixMicro()-int64(timestamp))) > float64(24*time.Hour) {
|
||||
// extensions should consume all ST_SYN payload
|
||||
if len(b) != offset {
|
||||
return nil, errNotBittorrent
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -39,79 +42,67 @@ func (h *SniffHeader) Domain() string {
|
||||
}
|
||||
|
||||
var (
|
||||
methods = [...]string{"get", "post", "head", "put", "delete", "options", "connect"}
|
||||
|
||||
errNotHTTPMethod = errors.New("not an HTTP method")
|
||||
validMethods = map[string]bool{}
|
||||
errNotHTTP = errors.New("not an HTTP request")
|
||||
)
|
||||
|
||||
func beginWithHTTPMethod(b []byte) error {
|
||||
for _, m := range &methods {
|
||||
if len(b) >= len(m) && strings.EqualFold(string(b[:len(m)]), m) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(b) < len(m) {
|
||||
return common.ErrNoClue
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return errNotHTTPMethod
|
||||
func isValidHTTPMethod(b []byte) bool {
|
||||
if len(b) == 0 {
|
||||
return false
|
||||
}
|
||||
idx := bytes.IndexByte(b, ' ')
|
||||
if idx == -1 {
|
||||
return false
|
||||
}
|
||||
method := unsafe.String(unsafe.SliceData(b), idx)
|
||||
return validMethods[method]
|
||||
}
|
||||
|
||||
func SniffHTTP(b []byte, c context.Context) (*SniffHeader, error) {
|
||||
if !isValidHTTPMethod(b) {
|
||||
return nil, errNotHTTP
|
||||
}
|
||||
content := session.ContentFromContext(c)
|
||||
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 {
|
||||
ShouldSniffAttr = false
|
||||
r, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(b)))
|
||||
if err != nil {
|
||||
if err == io.ErrUnexpectedEOF {
|
||||
return nil, common.ErrNoClue
|
||||
}
|
||||
return nil, errNotHTTP
|
||||
}
|
||||
if err := beginWithHTTPMethod(b); err != nil {
|
||||
return nil, err
|
||||
if r.Host == "" {
|
||||
return nil, common.ErrNoClue
|
||||
}
|
||||
|
||||
sh := &SniffHeader{
|
||||
version: HTTP1,
|
||||
host: r.Host,
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
return sh, nil
|
||||
}
|
||||
|
||||
@@ -14,75 +14,76 @@ func TestHTTPHeaders(t *testing.T) {
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
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`,
|
||||
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",
|
||||
domain: "net.tutsplus.com",
|
||||
},
|
||||
{
|
||||
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`,
|
||||
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",
|
||||
domain: "localhost",
|
||||
},
|
||||
{
|
||||
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`,
|
||||
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",
|
||||
domain: "",
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
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`,
|
||||
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",
|
||||
domain: "",
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
input: `GET /tutorials/other/top-20-mysql-best-practices/ HTTP/1.1`,
|
||||
input: "GET /tutorials/other/top-20-mysql-best-practices/ HTTP/1.1\r\n",
|
||||
domain: "",
|
||||
err: true,
|
||||
},
|
||||
@@ -97,6 +98,7 @@ first_name=John&last_name=Doe&action=Submit`,
|
||||
} 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())
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
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
|
||||
|
||||
@@ -3,7 +3,6 @@ package quic
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/aes"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
@@ -28,22 +27,43 @@ func (s SniffHeader) Domain() string {
|
||||
return s.domain
|
||||
}
|
||||
|
||||
const (
|
||||
versionDraft29 uint32 = 0xff00001d
|
||||
version1 uint32 = 0x1
|
||||
var (
|
||||
errNotQUIC = errors.New("not quic")
|
||||
errNotQUICInitial = errors.New("not initial packet")
|
||||
)
|
||||
|
||||
type quicVersionSpec struct {
|
||||
ver uint32
|
||||
typeInitial byte
|
||||
initialSalt []byte
|
||||
labelPrefix string
|
||||
}
|
||||
|
||||
var (
|
||||
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,
|
||||
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,
|
||||
}
|
||||
errNotQuic = errors.New("not quic")
|
||||
errNotQuicInitial = errors.New("not initial packet")
|
||||
)
|
||||
|
||||
func SniffQUIC(b []byte) (*SniffHeader, error) {
|
||||
@@ -63,60 +83,61 @@ 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)
|
||||
if versionNumber != 0 && typeByte&0x40 == 0 {
|
||||
return nil, errNotQuic
|
||||
} else if versionNumber != versionDraft29 && versionNumber != version1 {
|
||||
return nil, errNotQuic
|
||||
var s *quicVersionSpec
|
||||
if v, ok := quicVersionSpecMap[versionNumber]; ok {
|
||||
s = v
|
||||
} else {
|
||||
return nil, errNotQUIC
|
||||
}
|
||||
|
||||
packetType := (typeByte & 0x30) >> 4
|
||||
isQuicInitial := packetType == 0x0
|
||||
|
||||
var destConnID []byte
|
||||
if l, err := buffer.ReadByte(); err != nil {
|
||||
return nil, errNotQuic
|
||||
return nil, errNotQUIC
|
||||
} else if destConnID, err = buffer.ReadBytes(int32(l)); err != nil {
|
||||
return nil, errNotQuic
|
||||
return nil, errNotQUIC
|
||||
}
|
||||
|
||||
if l, err := buffer.ReadByte(); err != nil {
|
||||
return nil, errNotQuic
|
||||
return nil, errNotQUIC
|
||||
} else if common.Error2(buffer.ReadBytes(int32(l))) != nil {
|
||||
return nil, errNotQuic
|
||||
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)
|
||||
packetType := (typeByte & 0x30) >> 4
|
||||
isQUICInitial := packetType == s.typeInitial
|
||||
|
||||
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())
|
||||
@@ -125,25 +146,23 @@ 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
|
||||
}
|
||||
|
||||
var salt []byte
|
||||
if versionNumber == version1 {
|
||||
salt = quicSalt
|
||||
} else {
|
||||
salt = quicSaltOld
|
||||
}
|
||||
salt := s.initialSalt
|
||||
label := s.labelPrefix
|
||||
initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt)
|
||||
secret := hkdfExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size())
|
||||
hpKey := hkdfExpandLabel(initialSuite.Hash, secret, []byte{}, "quic hp", initialSuite.KeyLen)
|
||||
secret := hkdfExpandLabel(initialSecret, "client in", crypto.SHA256.Size())
|
||||
hpKey := hkdfExpandLabel(secret, label+" hp", 16)
|
||||
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)])
|
||||
@@ -153,8 +172,8 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
|
||||
b[hdrLen+i] ^= mask[i+1]
|
||||
}
|
||||
|
||||
key := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16)
|
||||
iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12)
|
||||
key := hkdfExpandLabel(secret, label+" key", 16)
|
||||
iv := hkdfExpandLabel(secret, label+" iv", 12)
|
||||
cipher := AEADAESGCMTLS13(key, iv)
|
||||
|
||||
nonce := cache.Extend(int32(cipher.NonceSize()))
|
||||
@@ -179,44 +198,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
|
||||
}
|
||||
@@ -232,13 +251,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
|
||||
}
|
||||
@@ -248,7 +267,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,35 +285,33 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
|
||||
return nil, protocol.ErrProtoNeedMoreData
|
||||
}
|
||||
|
||||
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...)
|
||||
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
|
||||
|
||||
out := make([]byte, length)
|
||||
n, err := hkdf.Expand(hash.New, secret, b).Read(out)
|
||||
n, err := hkdf.Expand(crypto.SHA256.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
@@ -207,6 +207,7 @@ func getConfig() string {
|
||||
"tag": "XHTTP_IN",
|
||||
"streamSettings": {
|
||||
"network": "xhttp",
|
||||
"security": "tls",
|
||||
"xhttpSettings": {
|
||||
"host": "bing.com",
|
||||
"path": "/xhttp_client_upload",
|
||||
|
||||
+33
-19
@@ -8,7 +8,7 @@ import (
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
@@ -27,25 +27,39 @@ var AddrParser = protocol.NewAddressParser(
|
||||
)
|
||||
|
||||
var (
|
||||
Show bool
|
||||
BaseKey []byte
|
||||
Show atomic.Bool
|
||||
baseKey atomic.Value
|
||||
)
|
||||
|
||||
func init() {
|
||||
if strings.ToLower(platform.NewEnvFlag(platform.XUDPLog).GetValue(func() string { return "" })) == "true" {
|
||||
Show = true
|
||||
func reloadEnvSettings() error {
|
||||
Show.Store(strings.ToLower(platform.NewEnvFlag(platform.XUDPLog).GetValue(func() string { return "" })) == "true")
|
||||
raw := platform.NewEnvFlag(platform.XUDPBaseKey).GetValue(func() string { return "" })
|
||||
if raw == "" {
|
||||
ensureBaseKey()
|
||||
return nil
|
||||
}
|
||||
BaseKey = make([]byte, 32)
|
||||
rand.Read(BaseKey)
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond) // this is not nice, but need to give some time for Android to setup ENV
|
||||
if raw := platform.NewEnvFlag(platform.XUDPBaseKey).GetValue(func() string { return "" }); raw != "" {
|
||||
if BaseKey, _ = base64.RawURLEncoding.DecodeString(raw); len(BaseKey) == 32 {
|
||||
return
|
||||
}
|
||||
panic(platform.XUDPBaseKey + ": invalid value (BaseKey must be 32 bytes): " + raw + " len " + strconv.Itoa(len(BaseKey)))
|
||||
}
|
||||
}()
|
||||
key, _ := base64.RawURLEncoding.DecodeString(raw)
|
||||
if len(key) != 32 {
|
||||
return errors.New(platform.XUDPBaseKey + ": invalid value (BaseKey must be 32 bytes): " + raw + " len " + strconv.Itoa(len(key)))
|
||||
}
|
||||
baseKey.Store(append([]byte(nil), key...))
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureBaseKey() []byte {
|
||||
if key := baseKey.Load(); key != nil {
|
||||
return key.([]byte)
|
||||
}
|
||||
key := make([]byte, 32)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
baseKey.Store(key)
|
||||
return key
|
||||
}
|
||||
|
||||
func init() {
|
||||
platform.RegisterEnvReload(reloadEnvSettings)
|
||||
}
|
||||
|
||||
func GetGlobalID(ctx context.Context) (globalID [8]byte) {
|
||||
@@ -54,10 +68,10 @@ func GetGlobalID(ctx context.Context) (globalID [8]byte) {
|
||||
}
|
||||
if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.Network == net.Network_UDP &&
|
||||
(inbound.Name == "dokodemo-door" || inbound.Name == "socks" || inbound.Name == "shadowsocks" || inbound.Name == "tun") {
|
||||
h := blake3.New(8, BaseKey)
|
||||
h := blake3.New(8, ensureBaseKey())
|
||||
h.Write([]byte(inbound.Source.String()))
|
||||
copy(globalID[:], h.Sum(nil))
|
||||
if Show {
|
||||
if Show.Load() {
|
||||
errors.LogInfo(ctx, fmt.Sprintf("XUDP inbound.Source.String(): %v\tglobalID: %v\n", inbound.Source.String(), globalID))
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -19,8 +19,8 @@ import (
|
||||
|
||||
var (
|
||||
Version_x byte = 26
|
||||
Version_y byte = 6
|
||||
Version_z byte = 27
|
||||
Version_y byte = 7
|
||||
Version_z byte = 28
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -187,6 +187,9 @@ func NewWithContext(ctx context.Context, config *Config) (*Instance, error) {
|
||||
}
|
||||
|
||||
func initInstanceWithConfig(config *Config, server *Instance) (bool, error) {
|
||||
if err := platform.ReloadEnvSettings(); err != nil {
|
||||
return true, errors.New("failed to reload environment settings").Base(err)
|
||||
}
|
||||
server.ctx = context.WithValue(server.ctx, "cone",
|
||||
platform.NewEnvFlag(platform.UseCone).GetValue(func() string { return "" }) != "true")
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package policy
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/platform"
|
||||
@@ -82,32 +83,41 @@ func ManagerType() interface{} {
|
||||
return (*Manager)(nil)
|
||||
}
|
||||
|
||||
var defaultBufferSize int32
|
||||
var defaultBufferSize atomic.Int32
|
||||
|
||||
func init() {
|
||||
func reloadEnvSettings() error {
|
||||
defaultBufferSize.Store(readDefaultBufferSize())
|
||||
return nil
|
||||
}
|
||||
|
||||
func readDefaultBufferSize() int32 {
|
||||
const defaultValue = -17
|
||||
size := platform.NewEnvFlag(platform.BufferSize).GetValueAsInt(defaultValue)
|
||||
|
||||
switch size {
|
||||
case 0:
|
||||
defaultBufferSize = -1 // For pipe to use unlimited size
|
||||
return -1 // For pipe to use unlimited size
|
||||
case defaultValue: // Env flag not defined. Use default values per CPU-arch.
|
||||
switch runtime.GOARCH {
|
||||
case "arm", "mips", "mipsle":
|
||||
defaultBufferSize = 0
|
||||
return 0
|
||||
case "arm64", "mips64", "mips64le":
|
||||
defaultBufferSize = 4 * 1024 // 4k cache for low-end devices
|
||||
return 4 * 1024 // 4k cache for low-end devices
|
||||
default:
|
||||
defaultBufferSize = 512 * 1024
|
||||
return 512 * 1024
|
||||
}
|
||||
default:
|
||||
defaultBufferSize = int32(size) * 1024 * 1024
|
||||
return int32(size) * 1024 * 1024
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
platform.RegisterEnvReload(reloadEnvSettings)
|
||||
}
|
||||
|
||||
func defaultBufferPolicy() Buffer {
|
||||
return Buffer{
|
||||
PerConnection: defaultBufferSize,
|
||||
PerConnection: defaultBufferSize.Load(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+21
-30
@@ -81,6 +81,8 @@ type Manager interface {
|
||||
|
||||
// RegisterCounter registers a new counter to the manager. The identifier string must not be empty, and unique among other counters.
|
||||
RegisterCounter(string) (Counter, error)
|
||||
// GetOrRegisterCounter returns the counter by its identifier, atomically creating and registering it if absent.
|
||||
GetOrRegisterCounter(string) (Counter, error)
|
||||
// UnregisterCounter unregisters a counter from the manager by its identifier.
|
||||
UnregisterCounter(string) error
|
||||
// GetCounter returns a counter by its identifier.
|
||||
@@ -91,6 +93,8 @@ type Manager interface {
|
||||
|
||||
// RegisterOnlineMap registers a new OnlineMap to the manager. The identifier string must not be empty, and unique among other OnlineMaps.
|
||||
RegisterOnlineMap(string) (OnlineMap, error)
|
||||
// GetOrRegisterOnlineMap returns the OnlineMap by its identifier, atomically creating and registering it if absent.
|
||||
GetOrRegisterOnlineMap(string) (OnlineMap, error)
|
||||
// UnregisterOnlineMap unregisters an OnlineMap from the manager by its identifier.
|
||||
UnregisterOnlineMap(string) error
|
||||
// GetOnlineMap returns an OnlineMap by its identifier.
|
||||
@@ -101,6 +105,8 @@ type Manager interface {
|
||||
|
||||
// RegisterChannel registers a new channel to the manager. The identifier string must not be empty, and unique among other channels.
|
||||
RegisterChannel(string) (Channel, error)
|
||||
// GetOrRegisterChannel returns the channel by its identifier, atomically creating and registering it if absent.
|
||||
GetOrRegisterChannel(string) (Channel, error)
|
||||
// UnregisterChannel unregisters a channel from the manager by its identifier.
|
||||
UnregisterChannel(string) error
|
||||
// GetChannel returns a channel by its identifier.
|
||||
@@ -110,36 +116,6 @@ type Manager interface {
|
||||
GetAllOnlineUsers() []string
|
||||
}
|
||||
|
||||
// GetOrRegisterCounter tries to get the StatCounter first. If not exist, it then tries to create a new counter.
|
||||
func GetOrRegisterCounter(m Manager, name string) (Counter, error) {
|
||||
counter := m.GetCounter(name)
|
||||
if counter != nil {
|
||||
return counter, nil
|
||||
}
|
||||
|
||||
return m.RegisterCounter(name)
|
||||
}
|
||||
|
||||
// GetOrRegisterOnlineMap tries to get the OnlineMap first. If not exist, it then tries to create a new OnlineMap.
|
||||
func GetOrRegisterOnlineMap(m Manager, name string) (OnlineMap, error) {
|
||||
onlineMap := m.GetOnlineMap(name)
|
||||
if onlineMap != nil {
|
||||
return onlineMap, nil
|
||||
}
|
||||
|
||||
return m.RegisterOnlineMap(name)
|
||||
}
|
||||
|
||||
// GetOrRegisterChannel tries to get the StatChannel first. If not exist, it then tries to create a new channel.
|
||||
func GetOrRegisterChannel(m Manager, name string) (Channel, error) {
|
||||
channel := m.GetChannel(name)
|
||||
if channel != nil {
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
return m.RegisterChannel(name)
|
||||
}
|
||||
|
||||
// ManagerType returns the type of Manager interface. Can be used to implement common.HasType.
|
||||
//
|
||||
// xray:api:stable
|
||||
@@ -160,6 +136,11 @@ func (NoopManager) RegisterCounter(string) (Counter, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// GetOrRegisterCounter implements Manager.
|
||||
func (NoopManager) GetOrRegisterCounter(string) (Counter, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// UnregisterCounter implements Manager.
|
||||
func (NoopManager) UnregisterCounter(string) error {
|
||||
return nil
|
||||
@@ -178,6 +159,11 @@ func (NoopManager) RegisterOnlineMap(string) (OnlineMap, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// GetOrRegisterOnlineMap implements Manager.
|
||||
func (NoopManager) GetOrRegisterOnlineMap(string) (OnlineMap, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// UnregisterOnlineMap implements Manager.
|
||||
func (NoopManager) UnregisterOnlineMap(string) error {
|
||||
return nil
|
||||
@@ -196,6 +182,11 @@ func (NoopManager) RegisterChannel(string) (Channel, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// GetOrRegisterChannel implements Manager.
|
||||
func (NoopManager) GetOrRegisterChannel(string) (Channel, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// UnregisterChannel implements Manager.
|
||||
func (NoopManager) UnregisterChannel(string) error {
|
||||
return nil
|
||||
|
||||
@@ -3,36 +3,37 @@ module github.com/xtls/xray-core
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716
|
||||
github.com/cloudflare/circl v1.6.4
|
||||
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
|
||||
github.com/cloudflare/circl v1.6.5
|
||||
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/miekg/dns v1.1.72
|
||||
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138
|
||||
github.com/miekg/dns v1.1.73
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/pion/stun/v3 v3.1.6
|
||||
github.com/pires/go-proxyproto v0.14.0
|
||||
github.com/pion/stun/v3 v3.1.7
|
||||
github.com/pires/go-proxyproto v0.15.0
|
||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sagernet/sing v0.5.1
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/stretchr/testify v1.12.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.53.0
|
||||
golang.org/x/crypto v0.55.0
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/sync v0.21.0
|
||||
golang.org/x/sys v0.46.0
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1
|
||||
google.golang.org/grpc v1.82.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
google.golang.org/grpc v1.83.1
|
||||
google.golang.org/protobuf v1.36.12
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
|
||||
h12.io/socks v1.0.3
|
||||
lukechampine.com/blake3 v1.4.1
|
||||
@@ -40,23 +41,24 @@ 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/pion/dtls/v3 v3.1.4 // indirect
|
||||
github.com/libp2p/go-netroute v0.2.1 // indirect
|
||||
github.com/pion/dtls/v3 v3.1.5 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/transport/v4 v4.0.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/pion/transport/v4 v4.1.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
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.45.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
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.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/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/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.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U=
|
||||
github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY=
|
||||
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
|
||||
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
|
||||
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=
|
||||
@@ -23,40 +21,52 @@ 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/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
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/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.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY=
|
||||
github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc=
|
||||
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/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.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/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/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=
|
||||
@@ -69,8 +79,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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
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=
|
||||
@@ -82,38 +92,40 @@ 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.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.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.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.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
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/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.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -121,21 +133,20 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
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=
|
||||
@@ -147,20 +158,18 @@ 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-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=
|
||||
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=
|
||||
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=
|
||||
|
||||
@@ -65,6 +65,9 @@ func (v *Address) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
|
||||
func (v *Address) Build() *net.IPOrDomain {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
return net.NewIPOrDomain(v.Address)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRootEnvAppliesArbitraryValues(t *testing.T) {
|
||||
const (
|
||||
valueKey = "XRAY_TEST_CONFIG_ENV"
|
||||
emptyKey = "XRAY_TEST_CONFIG_EMPTY"
|
||||
)
|
||||
t.Setenv(valueKey, "before")
|
||||
t.Setenv(emptyKey, "before")
|
||||
|
||||
config := new(Config)
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"env": {
|
||||
"XRAY_TEST_CONFIG_ENV": "configured",
|
||||
"XRAY_TEST_CONFIG_EMPTY": ""
|
||||
}
|
||||
}`), config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := config.Build(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got := os.Getenv(valueKey); got != "configured" {
|
||||
t.Fatalf("env %q = %q, want %q", valueKey, got, "configured")
|
||||
}
|
||||
if got := os.Getenv(emptyKey); got != "" {
|
||||
t.Fatalf("env %q = %q, want empty", emptyKey, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvConfigOverride(t *testing.T) {
|
||||
base := EnvConfig{
|
||||
"ONE": "one",
|
||||
"TWO": "old",
|
||||
}
|
||||
override := EnvConfig{
|
||||
"TWO": "new",
|
||||
"THREE": "three",
|
||||
}
|
||||
base.Override(override)
|
||||
|
||||
want := map[string]string{
|
||||
"ONE": "one",
|
||||
"TWO": "new",
|
||||
"THREE": "three",
|
||||
}
|
||||
for key, value := range want {
|
||||
if got := base[key]; got != value {
|
||||
t.Fatalf("env %q = %q, want %q", key, got, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -148,6 +148,7 @@ 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)
|
||||
@@ -261,6 +262,10 @@ 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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
googleuuid "github.com/google/uuid"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/fragment"
|
||||
@@ -22,6 +25,7 @@ import (
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/sudoku"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/xdns"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/xicmp"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/xmc"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
@@ -67,6 +71,7 @@ var (
|
||||
"header-custom": func() interface{} { return new(HeaderCustomTCP) },
|
||||
"fragment": func() interface{} { return new(FragmentMask) },
|
||||
"sudoku": func() interface{} { return new(Sudoku) },
|
||||
"xmc": func() interface{} { return new(XMC) },
|
||||
}, "type", "settings")
|
||||
|
||||
udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
|
||||
@@ -715,6 +720,81 @@ func (c *Xdns) Build() (proto.Message, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
type XMC struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Profiles []XMCProfile `json:"profiles"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type XMCProfile struct {
|
||||
// Resolve the UUID by username, then request the session profile with
|
||||
// unsigned=false. Client and server must use the same signed profile.
|
||||
Username string `json:"username"`
|
||||
UUID string `json:"uuid"`
|
||||
TexturesValue string `json:"texturesValue"`
|
||||
TexturesSignature string `json:"texturesSignature"`
|
||||
}
|
||||
|
||||
var xmcUsernamePattern = regexp.MustCompile(`^[A-Za-z0-9_]{3,16}$`)
|
||||
|
||||
func (c *XMCProfile) Build() (*xmc.Profile, error) {
|
||||
if !xmcUsernamePattern.MatchString(c.Username) {
|
||||
return nil, fmt.Errorf("invalid minecraft profile username: %q", c.Username)
|
||||
}
|
||||
|
||||
profileUUID, err := googleuuid.Parse(c.UUID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid minecraft profile UUID: %w", err)
|
||||
}
|
||||
if c.TexturesValue == "" || c.TexturesSignature == "" {
|
||||
return nil, fmt.Errorf("incomplete minecraft profile textures")
|
||||
}
|
||||
|
||||
return &xmc.Profile{
|
||||
Username: c.Username,
|
||||
Uuid: append([]byte(nil), profileUUID[:]...),
|
||||
TexturesValue: c.TexturesValue,
|
||||
TexturesSignature: c.TexturesSignature,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *XMC) Build() (proto.Message, error) {
|
||||
if len(c.Profiles) == 0 {
|
||||
return nil, fmt.Errorf("minecraft profiles are required")
|
||||
}
|
||||
|
||||
if c.Password == "" {
|
||||
return nil, fmt.Errorf("empty password")
|
||||
}
|
||||
|
||||
rsaPrivateKey, err := xmc.DeriveRSAKey(c.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derive minecraft rsa key: %w", err)
|
||||
}
|
||||
|
||||
rsaPublicKey, err := x509.MarshalPKIXPublicKey(&rsaPrivateKey.PublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal minecraft rsa public key: %w", err)
|
||||
}
|
||||
|
||||
profiles := make([]*xmc.Profile, 0, len(c.Profiles))
|
||||
for i := range c.Profiles {
|
||||
profile, err := c.Profiles[i].Build()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build minecraft profile %d: %w", i, err)
|
||||
}
|
||||
profiles = append(profiles, profile)
|
||||
}
|
||||
|
||||
return &xmc.Config{
|
||||
Password: c.Password,
|
||||
Hostname: c.Hostname,
|
||||
RsaPrivateKey: x509.MarshalPKCS1PrivateKey(rsaPrivateKey),
|
||||
RsaPublicKey: rsaPublicKey,
|
||||
Profiles: profiles,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Xicmp struct {
|
||||
DGRAM bool `json:"dgram"`
|
||||
IPs []string `json:"ips"`
|
||||
@@ -736,9 +816,11 @@ func (c *Xicmp) Build() (proto.Message, error) {
|
||||
}
|
||||
|
||||
type Realm struct {
|
||||
Url string `json:"url"`
|
||||
StunServers []string `json:"stunServers"`
|
||||
TlsConfig *TLSConfig `json:"tlsConfig"`
|
||||
Url string `json:"url"`
|
||||
StunServers []string `json:"stunServers"`
|
||||
TlsConfig *TLSConfig `json:"tlsConfig"`
|
||||
IPMode string `json:"ipMode"`
|
||||
PortMapping *realm.PortMapping `json:"portMapping"`
|
||||
}
|
||||
|
||||
func (c *Realm) Build() (proto.Message, error) {
|
||||
@@ -818,6 +900,8 @@ func (c *Realm) Build() (proto.Message, error) {
|
||||
ID: id,
|
||||
StunServers: stunServers,
|
||||
TlsConfig: tlsConfig,
|
||||
IPMode: strings.ToLower(c.IPMode),
|
||||
PortMapping: c.PortMapping,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -848,20 +932,24 @@ 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"`
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type FinalMask struct {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/xmc"
|
||||
)
|
||||
|
||||
func TestXMCBuildProfile(t *testing.T) {
|
||||
built, err := (&XMC{
|
||||
Password: "test-password",
|
||||
Profiles: []XMCProfile{
|
||||
{
|
||||
Username: "TestUser",
|
||||
UUID: "00112233-4455-6677-8899-aabbccddeeff",
|
||||
TexturesValue: "textures-value",
|
||||
TexturesSignature: "textures-signature",
|
||||
},
|
||||
},
|
||||
}).Build()
|
||||
if err != nil {
|
||||
t.Fatalf("build XMC config: %v", err)
|
||||
}
|
||||
config := built.(*xmc.Config)
|
||||
if len(config.Profiles) != 1 || len(config.Profiles[0].Uuid) != 16 {
|
||||
t.Fatalf("unexpected profiles: %+v", config.Profiles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXMCBuildRequiresProfile(t *testing.T) {
|
||||
_, err := (&XMC{Password: "test-password"}).Build()
|
||||
if err == nil || !strings.Contains(err.Error(), "profiles are required") {
|
||||
t.Fatalf("expected required profiles error, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -285,10 +285,11 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
|
||||
}
|
||||
|
||||
config.QuicParams = &internet.QuicParams{
|
||||
Congestion: c.FinalMask.QuicParams.Congestion,
|
||||
BbrProfile: profile,
|
||||
BrutalUp: up,
|
||||
BrutalDown: down,
|
||||
Congestion: c.FinalMask.QuicParams.Congestion,
|
||||
BbrProfile: profile,
|
||||
BrutalUp: up,
|
||||
BrutalDown: down,
|
||||
BrutalDisableLossCompensation: c.FinalMask.QuicParams.BrutalDisableLossCompensation,
|
||||
UdpHop: &internet.UdpHop{
|
||||
Ports: c.FinalMask.QuicParams.UdpHop.PortList.Build().Ports(),
|
||||
IntervalMin: int64(c.FinalMask.QuicParams.UdpHop.Interval.From),
|
||||
@@ -301,7 +302,10 @@ 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,8 +450,8 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
|
||||
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
|
||||
}
|
||||
if c.Xmux == (XmuxConfig{}) {
|
||||
c.Xmux.MaxConnections.From = 6
|
||||
c.Xmux.MaxConnections.To = 6
|
||||
c.Xmux.MaxConnections.From = 3
|
||||
c.Xmux.MaxConnections.To = 3
|
||||
c.Xmux.HMaxRequestTimes.From = 600
|
||||
c.Xmux.HMaxRequestTimes.To = 900
|
||||
c.Xmux.HMaxReusableSecs.From = 1800
|
||||
@@ -751,6 +751,7 @@ type Masquerade struct {
|
||||
|
||||
Url string `json:"url"`
|
||||
RewriteHost bool `json:"rewriteHost"`
|
||||
XForwarded bool `json:"xForwarded"`
|
||||
Insecure bool `json:"insecure"`
|
||||
|
||||
Content string `json:"content"`
|
||||
@@ -791,6 +792,7 @@ 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
|
||||
|
||||
@@ -113,6 +113,10 @@ func (c *REALITYConfig) Build() (proto.Message, error) {
|
||||
config.MinClientVer[i] = byte(u)
|
||||
}
|
||||
}
|
||||
errors.LogWarning(context.Background(), `REALITY: Changing "minClientVer" will increase the likelihood of your server's IP being blocked by the GFW`)
|
||||
} else {
|
||||
config.MinClientVer = []byte{26, 3, 27} // change it at your own risk: https://github.com/XTLS/Xray-core/commit/af7eb68028732a8ee3c0e5d6ab2b8a657bb2e770
|
||||
errors.LogWarning(context.Background(), `REALITY: The default minimal client version is Xray-core v26.3.27, other clients may be refused to connect`)
|
||||
}
|
||||
if c.MaxClientVer != "" {
|
||||
config.MaxClientVer = make([]byte, 3)
|
||||
@@ -157,8 +161,10 @@ func (c *REALITYConfig) Build() (proto.Message, error) {
|
||||
}
|
||||
|
||||
for _, sn := range config.ServerNames {
|
||||
if strings.Contains(sn, "apple") || strings.Contains(sn, "icloud") {
|
||||
errors.LogWarning(context.Background(), `REALITY: Choosing apple, icloud, etc. as the target may get your IP blocked by the GFW`)
|
||||
sn = strings.ToLower(sn)
|
||||
if strings.HasSuffix(sn, ".ru") || strings.HasSuffix(sn, ".ir") || strings.HasSuffix(sn, ".cn") ||
|
||||
strings.Contains(sn, "apple") || strings.Contains(sn, "icloud") || strings.Contains(sn, "microsoft") {
|
||||
errors.LogWarning(context.Background(), `REALITY: Choosing "`, sn, `" as the target will increase the likelihood of your server's IP being blocked by the GFW`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+70
-1
@@ -1,12 +1,19 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/xtls/xray-core/proxy/tun"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type TunConfig struct {
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"desc"`
|
||||
MTU uint32 `json:"mtu"`
|
||||
Gateway []string `json:"gateway"`
|
||||
DNS []string `json:"dns"`
|
||||
@@ -18,6 +25,7 @@ type TunConfig struct {
|
||||
func (v *TunConfig) Build() (proto.Message, error) {
|
||||
config := &tun.Config{
|
||||
Name: v.Name,
|
||||
Desc: v.Desc,
|
||||
MTU: v.MTU,
|
||||
Gateway: v.Gateway,
|
||||
DNS: v.DNS,
|
||||
@@ -32,10 +40,71 @@ func (v *TunConfig) Build() (proto.Message, error) {
|
||||
}
|
||||
|
||||
if config.Name == "" {
|
||||
config.Name = "xray0"
|
||||
name, err := GetAvailableTunName()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Name = name
|
||||
}
|
||||
if config.Desc == "" {
|
||||
config.Desc = "Wintun"
|
||||
}
|
||||
if config.MTU == 0 {
|
||||
config.MTU = 1500
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
const (
|
||||
tunNamePrefix = "utun"
|
||||
minTunIndex = 10
|
||||
maxTunIndex = 1024
|
||||
)
|
||||
|
||||
func GetAvailableTunName() (string, error) {
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fail to get system interface information: %w", err)
|
||||
}
|
||||
|
||||
usedNames := make(map[string]struct{}, len(interfaces))
|
||||
for _, iface := range interfaces {
|
||||
usedNames[iface.Name] = struct{}{}
|
||||
}
|
||||
|
||||
startIndex, err := randomInt(minTunIndex, maxTunIndex)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fail to generate valid tun name: %w", err)
|
||||
}
|
||||
|
||||
rangeSize := maxTunIndex - minTunIndex + 1
|
||||
|
||||
for offset := 0; offset < rangeSize; offset++ {
|
||||
index := minTunIndex + (startIndex-minTunIndex+offset)%rangeSize
|
||||
name := tunNamePrefix + strconv.Itoa(index)
|
||||
|
||||
if _, exists := usedNames[name]; !exists {
|
||||
return name, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf(
|
||||
"no available TUN interface name in range %s%d-%s%d",
|
||||
tunNamePrefix,
|
||||
minTunIndex,
|
||||
tunNamePrefix,
|
||||
maxTunIndex,
|
||||
)
|
||||
}
|
||||
|
||||
func randomInt(min, max int) (int, error) {
|
||||
value, err := rand.Int(
|
||||
rand.Reader,
|
||||
big.NewInt(int64(max-min+1)),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return min + int(value.Int64()), nil
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ 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) {
|
||||
@@ -141,6 +142,7 @@ func (c *WireGuardConfig) Build() (proto.Message, error) {
|
||||
|
||||
config.IsClient = c.IsClient
|
||||
config.NoKernelTun = c.NoKernelTun
|
||||
config.DNS = c.DNS
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
+29
-7
@@ -3,6 +3,7 @@ package conf
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -139,7 +140,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 {
|
||||
} else if c.ListenOn == nil || len(c.ListenOn.String()) == 0 {
|
||||
// Listen on anyip, must set PortList
|
||||
if c.PortList == nil {
|
||||
return nil, errors.New("Listen on AnyIP but no Port(s) set in InboundDetour.")
|
||||
@@ -175,7 +176,7 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
|
||||
receiverSettings.StreamSettings = ss
|
||||
if strings.Contains(ss.SecurityType, "reality") && (receiverSettings.PortList == nil ||
|
||||
len(receiverSettings.PortList.Ports()) != 1 || receiverSettings.PortList.Ports()[0] != 443) {
|
||||
errors.LogWarning(context.Background(), `REALITY: Listening on non-443 ports may get your IP blocked by the GFW`)
|
||||
errors.LogWarning(context.Background(), `REALITY: Listening on non-443 ports will increase the likelihood of your server's IP being blocked by the GFW`)
|
||||
}
|
||||
}
|
||||
if c.SniffingConfig != nil {
|
||||
@@ -250,13 +251,13 @@ func validateOutboundTransportSecurity(rawConfig interface{}, senderSettings *pr
|
||||
if vlessCfg.Encryption != "" && vlessCfg.Encryption != "none" {
|
||||
return nil
|
||||
}
|
||||
if requiresTransportSecurity(vlessCfg.Address) {
|
||||
if requiresTransportSecurity(vlessCfg.Vnext[0].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.Address) {
|
||||
if requiresTransportSecurity(tjCfg.Servers[0].Address) {
|
||||
return errors.New("trojan without TLS is prohibited unless the server address is a private IP or domain")
|
||||
}
|
||||
}
|
||||
@@ -357,13 +358,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),
|
||||
@@ -379,11 +380,20 @@ func (c *StatsConfig) Build() (*stats.Config, error) {
|
||||
return &stats.Config{}, nil
|
||||
}
|
||||
|
||||
type EnvConfig map[string]string
|
||||
|
||||
func (c EnvConfig) Override(o EnvConfig) {
|
||||
for key, value := range o {
|
||||
c[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
// Deprecated: Global transport config is no longer used
|
||||
// left for returning error
|
||||
Transport map[string]json.RawMessage `json:"transport"`
|
||||
|
||||
Env EnvConfig `json:"env"`
|
||||
LogConfig *LogConfig `json:"log"`
|
||||
RouterConfig *RouterConfig `json:"routing"`
|
||||
DNSConfig *DNSConfig `json:"dns"`
|
||||
@@ -439,6 +449,12 @@ func (c *Config) Override(o *Config, fn string) {
|
||||
if o.Transport != nil {
|
||||
c.Transport = o.Transport
|
||||
}
|
||||
if o.Env != nil {
|
||||
if c.Env == nil {
|
||||
c.Env = EnvConfig{}
|
||||
}
|
||||
c.Env.Override(o.Env)
|
||||
}
|
||||
if o.Policy != nil {
|
||||
c.Policy = o.Policy
|
||||
}
|
||||
@@ -514,6 +530,12 @@ func (c *Config) Override(o *Config, fn string) {
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *Config) Build() (*core.Config, error) {
|
||||
for key, value := range c.Env {
|
||||
if err := os.Setenv(key, value); err != nil {
|
||||
return nil, errors.New("failed to apply environment configuration").Base(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := PostProcessConfigureFile(c); err != nil {
|
||||
return nil, errors.New("failed to post-process configuration file").Base(err)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ The -confdir=dir flag sets a dir with multiple json config
|
||||
The -format=json flag sets the format of config files.
|
||||
Default "auto".
|
||||
|
||||
The config root env object sets process environment variables after all config
|
||||
files are parsed. Variables needed to locate or parse config files must be set
|
||||
in the process environment before Xray starts.
|
||||
|
||||
The -test flag tells Xray to test config files only,
|
||||
without launching the server.
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
//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
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
//go:build !linux && !openbsd
|
||||
// +build !linux,!openbsd
|
||||
|
||||
package dokodemo
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/pires/go-proxyproto"
|
||||
@@ -31,12 +32,24 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
useSplice bool
|
||||
useSplice atomic.Bool
|
||||
allNetworks [8]bool
|
||||
defaultBlockPrivateRule *FinalRule
|
||||
defaultBlockAllRule *FinalRule
|
||||
)
|
||||
|
||||
func reloadEnvSettings() error {
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue })
|
||||
enabled := false
|
||||
switch value {
|
||||
case defaultFlagValue, "auto", "enable":
|
||||
enabled = true
|
||||
}
|
||||
useSplice.Store(enabled)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
||||
h := new(Handler)
|
||||
@@ -48,12 +61,7 @@ func init() {
|
||||
return h, nil
|
||||
}))
|
||||
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue })
|
||||
switch value {
|
||||
case defaultFlagValue, "auto", "enable":
|
||||
useSplice = true
|
||||
}
|
||||
platform.RegisterEnvReload(reloadEnvSettings)
|
||||
|
||||
for i := range allNetworks {
|
||||
allNetworks[i] = true
|
||||
@@ -422,7 +430,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
|
||||
responseDone := func() error {
|
||||
defer timer.SetTimeout(plcy.Timeouts.UplinkOnly)
|
||||
if destination.Network == net.Network_TCP && useSplice && proxy.IsRAWTransportWithoutSecurity(conn) { // it would be tls conn in special use case of MITM, we need to let link handle traffic
|
||||
if destination.Network == net.Network_TCP && useSplice.Load() && proxy.IsRAWTransportWithoutSecurity(conn) { // it would be tls conn in special use case of MITM, we need to let link handle traffic
|
||||
var writeConn net.Conn
|
||||
var inTimer *signal.ActivityTimer
|
||||
if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Conn != nil {
|
||||
|
||||
@@ -173,15 +173,18 @@ func fillRequestHeader(ctx context.Context, header []*Header) ([]*Header, error)
|
||||
outbounds := session.OutboundsFromContext(ctx)
|
||||
ob := outbounds[len(outbounds)-1]
|
||||
|
||||
if inbound == nil || ob == nil {
|
||||
return nil, errors.New("missing inbound or outbound metadata from context")
|
||||
var src net.Destination
|
||||
if inbound != nil {
|
||||
src = inbound.Source
|
||||
} else {
|
||||
src = net.TCPDestination(net.AnyIP, 0)
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Source net.Destination
|
||||
Target net.Destination
|
||||
}{
|
||||
Source: inbound.Source,
|
||||
Source: src,
|
||||
Target: ob.Target,
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -31,13 +31,17 @@ Here is simple Xray config snippet to enable the inbound:
|
||||
"port": 0,
|
||||
"protocol": "tun",
|
||||
"settings": {
|
||||
"name": "xray0",
|
||||
"MTU": 1492
|
||||
"name": "utun10",
|
||||
"desc": "Wintun",
|
||||
"mtu": 1500
|
||||
}
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
`desc` sets the Windows Wintun adapter tunnel type and defaults to `Wintun`.
|
||||
It is ignored on other platforms.
|
||||
|
||||
## SUPPORTED FEATURES
|
||||
|
||||
- IPv4 and IPv6
|
||||
|
||||
+14
-4
@@ -7,11 +7,12 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -30,6 +31,7 @@ type Config struct {
|
||||
UserLevel uint32 `protobuf:"varint,5,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
|
||||
AutoSystemRoutingTable []string `protobuf:"bytes,6,rep,name=auto_system_routing_table,json=autoSystemRoutingTable,proto3" json:"auto_system_routing_table,omitempty"`
|
||||
AutoOutboundsInterface string `protobuf:"bytes,7,opt,name=auto_outbounds_interface,json=autoOutboundsInterface,proto3" json:"auto_outbounds_interface,omitempty"`
|
||||
Desc string `protobuf:"bytes,8,opt,name=desc,proto3" json:"desc,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -113,11 +115,18 @@ func (x *Config) GetAutoOutboundsInterface() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetDesc() string {
|
||||
if x != nil {
|
||||
return x.Desc
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_proxy_tun_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_proxy_tun_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\xee\x01\n" +
|
||||
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\x82\x02\n" +
|
||||
"\x06Config\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
|
||||
"\x03MTU\x18\x02 \x01(\rR\x03MTU\x12\x18\n" +
|
||||
@@ -126,7 +135,8 @@ const file_proxy_tun_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"user_level\x18\x05 \x01(\rR\tuserLevel\x129\n" +
|
||||
"\x19auto_system_routing_table\x18\x06 \x03(\tR\x16autoSystemRoutingTable\x128\n" +
|
||||
"\x18auto_outbounds_interface\x18\a \x01(\tR\x16autoOutboundsInterfaceBL\n" +
|
||||
"\x18auto_outbounds_interface\x18\a \x01(\tR\x16autoOutboundsInterface\x12\x12\n" +
|
||||
"\x04desc\x18\b \x01(\tR\x04descBL\n" +
|
||||
"\x12com.xray.proxy.tunP\x01Z#github.com/xtls/xray-core/proxy/tun\xaa\x02\x0eXray.Proxy.Tunb\x06proto3"
|
||||
|
||||
var (
|
||||
|
||||
@@ -14,4 +14,5 @@ message Config {
|
||||
uint32 user_level = 5;
|
||||
repeated string auto_system_routing_table = 6;
|
||||
string auto_outbounds_interface = 7;
|
||||
string desc = 8;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin
|
||||
if len(t.tag) > 0 && pm.ForSystem().Stats.InboundUplink {
|
||||
statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + t.tag + ">>>traffic>>>uplink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
t.uplinkCounter = c
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin
|
||||
if len(t.tag) > 0 && pm.ForSystem().Stats.InboundDownlink {
|
||||
statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + t.tag + ">>>traffic>>>downlink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
t.downlinkCounter = c
|
||||
}
|
||||
|
||||
+136
-5
@@ -11,6 +11,8 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
@@ -38,21 +40,111 @@ 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)
|
||||
@@ -77,6 +169,7 @@ func NewTun(options *Config) (Tun, error) {
|
||||
options: options,
|
||||
tunFd: fd,
|
||||
ownsFd: false,
|
||||
waitKq: newWaitKqueue(fd),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -103,6 +196,7 @@ func NewTun(options *Config) (Tun, error) {
|
||||
options: options,
|
||||
tunFd: int(tunFile.Fd()),
|
||||
ownsFd: true,
|
||||
waitKq: newWaitKqueue(int(tunFile.Fd())),
|
||||
gateway: gateway,
|
||||
}, nil
|
||||
}
|
||||
@@ -134,6 +228,9 @@ 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())
|
||||
@@ -242,9 +339,43 @@ func (t *DarwinTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
|
||||
}), nil
|
||||
}
|
||||
|
||||
// Wait some cpu cycles
|
||||
// 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.
|
||||
func (t *DarwinTun) Wait() {
|
||||
procyield(1)
|
||||
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)
|
||||
}
|
||||
|
||||
func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) {
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestSelectDarwinGatewayDefault(t *testing.T) {
|
||||
@@ -47,3 +51,183 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+729
-25
@@ -3,28 +3,60 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
_ "unsafe"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
"sync"
|
||||
"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
|
||||
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
|
||||
)
|
||||
|
||||
//go:linkname procyield runtime.procyield
|
||||
func procyield(cycles uint32)
|
||||
|
||||
type FreeBSDTun struct {
|
||||
device tun.Device
|
||||
mtu uint32
|
||||
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
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -34,20 +66,172 @@ 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
|
||||
}
|
||||
|
||||
return &FreeBSDTun{device: tunDev, mtu: options.MTU}, nil
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
return t.device.Close()
|
||||
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))
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) Name() (string, error) {
|
||||
@@ -55,21 +239,13 @@ func (t *FreeBSDTun) Name() (string, error) {
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) Index() (int, error) {
|
||||
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
|
||||
return t.tunIndex, 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.mtu) + tunHeaderSize)
|
||||
b := buf.NewWithSize(int32(t.options.MTU) + tunHeaderSize)
|
||||
defer b.Release()
|
||||
|
||||
// prepare Unix specific packet header
|
||||
@@ -104,7 +280,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.mtu) + tunHeaderSize)
|
||||
b := buf.NewWithSize(int32(t.options.MTU) + tunHeaderSize)
|
||||
|
||||
// read the bytes to the interface file
|
||||
n, err := b.ReadFrom(t.device.File())
|
||||
@@ -141,23 +317,551 @@ func (t *FreeBSDTun) Wait() {
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) newEndpoint() (stack.LinkEndpoint, error) {
|
||||
return &LinkEndpoint{deviceMTU: t.mtu, device: t}, nil
|
||||
return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil
|
||||
}
|
||||
|
||||
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
|
||||
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 findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
|
||||
if fixedName == "" {
|
||||
return nil, errors.New("automatic outbound interface selection is not supported on this platform")
|
||||
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
|
||||
}
|
||||
iface, err := net.InterfaceByName(fixedName)
|
||||
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
|
||||
}
|
||||
|
||||
physical, err := physicalDefaultRoutes(tunIndex, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if iface.Index == tunIndex {
|
||||
return nil, errors.New("outbound interface cannot be the TUN interface")
|
||||
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")
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
//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")
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ var _ GVisorDevice = (*WindowsTun)(nil)
|
||||
// interface with the same name exist, it tried to be reused.
|
||||
func NewTun(options *Config) (Tun, error) {
|
||||
// instantiate wintun adapter
|
||||
adapter, err := open(options.Name)
|
||||
adapter, err := open(options.Name, options.Desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -73,12 +73,12 @@ func NewTun(options *Config) (Tun, error) {
|
||||
return tun, nil
|
||||
}
|
||||
|
||||
func open(name string) (*wintun.Adapter, error) {
|
||||
func open(name, desc string) (*wintun.Adapter, error) {
|
||||
// generate a deterministic GUID from the adapter name
|
||||
id := md5.Sum([]byte(name))
|
||||
guid := (*windows.GUID)(unsafe.Pointer(&id[0]))
|
||||
// try to create adapter anew
|
||||
adapter, err := wintun.CreateAdapter(name, "Xray", guid)
|
||||
adapter, err := wintun.CreateAdapter(name, desc, guid)
|
||||
if err == nil {
|
||||
return adapter, nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"hash/crc64"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
@@ -218,10 +219,17 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
return nil
|
||||
}
|
||||
|
||||
var enablePadding = false
|
||||
var enablePadding atomic.Bool
|
||||
|
||||
func shouldEnablePadding(s protocol.SecurityType) bool {
|
||||
return enablePadding || s == protocol.SecurityType_AES128_GCM || s == protocol.SecurityType_CHACHA20_POLY1305 || s == protocol.SecurityType_AUTO
|
||||
return enablePadding.Load() || s == protocol.SecurityType_AES128_GCM || s == protocol.SecurityType_CHACHA20_POLY1305 || s == protocol.SecurityType_AUTO
|
||||
}
|
||||
|
||||
func reloadEnvSettings() error {
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
paddingValue := platform.NewEnvFlag(platform.UseVmessPadding).GetValue(func() string { return defaultFlagValue })
|
||||
enablePadding.Store(paddingValue != defaultFlagValue)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -229,10 +237,5 @@ func init() {
|
||||
return New(ctx, config.(*Config))
|
||||
}))
|
||||
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
|
||||
paddingValue := platform.NewEnvFlag(platform.UseVmessPadding).GetValue(func() string { return defaultFlagValue })
|
||||
if paddingValue != defaultFlagValue {
|
||||
enablePadding = true
|
||||
}
|
||||
platform.RegisterEnvReload(reloadEnvSettings)
|
||||
}
|
||||
|
||||
+63
-27
@@ -5,9 +5,10 @@ import (
|
||||
"fmt"
|
||||
gonet "net"
|
||||
"net/netip"
|
||||
reflect "reflect"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
|
||||
@@ -30,6 +31,11 @@ import (
|
||||
"golang.zx2c4.com/wireguard/device"
|
||||
)
|
||||
|
||||
type entry struct {
|
||||
got []net.IP
|
||||
time time.Time
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
conf *DeviceConfig
|
||||
policyManager policy.Manager
|
||||
@@ -43,6 +49,11 @@ 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) {
|
||||
@@ -57,7 +68,7 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
if len(tag) > 0 && p.ForSystem().Stats.OutboundUplink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "outbound>>>" + tag + ">>>traffic>>>uplink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
uplinkCounter = c
|
||||
}
|
||||
@@ -65,7 +76,7 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
if len(tag) > 0 && p.ForSystem().Stats.OutboundDownlink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "outbound>>>" + tag + ">>>traffic>>>downlink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
downlinkCounter = c
|
||||
}
|
||||
@@ -98,6 +109,20 @@ 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")
|
||||
@@ -106,10 +131,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, []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))
|
||||
tun, tnet, err = createKernelTun(localAddresses, dnses, int(conf.Mtu))
|
||||
} else {
|
||||
errors.LogWarning(context.Background(), "Using gVisor TUN")
|
||||
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)
|
||||
tun, tnet, _, err = CreateNetTUN(localAddresses, dnses, int(conf.Mtu), true)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -126,6 +151,9 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
|
||||
tun: tun,
|
||||
tnet: tnet,
|
||||
|
||||
local: local,
|
||||
cache: make(map[string]entry),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -138,14 +166,9 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
}
|
||||
ob.Name = "wireguard"
|
||||
ob.CanSpliceCopy = 3
|
||||
dialer.SetOutboundGateway(ctx, ob)
|
||||
|
||||
if h.dev == nil {
|
||||
if err := h.init(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.dev.Up(); err != nil {
|
||||
if err := h.init(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -258,8 +281,11 @@ func (h *Handler) Close() (err error) {
|
||||
func (h *Handler) init(ctx context.Context) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if h.tun == nil {
|
||||
return errors.New("closed")
|
||||
}
|
||||
if h.dev != nil {
|
||||
return nil
|
||||
return h.dev.Up()
|
||||
}
|
||||
resolveFunc := h.resolveLocal
|
||||
listenFunc := func() (net.PacketConn, error) {
|
||||
@@ -345,31 +371,34 @@ func (h *Handler) init(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (h *Handler) resolveLocal(host string) (net.IP, error) {
|
||||
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
|
||||
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})
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) resolveRemote(host string) (net.IP, error) {
|
||||
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.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})
|
||||
}
|
||||
ips := make([]net.IP, 0, len(addrs))
|
||||
for _, addr := range addrs {
|
||||
ips = append(ips, net.ParseIP(addr))
|
||||
}
|
||||
return ips, nil
|
||||
return h.tnet.LookupHost(host)
|
||||
})
|
||||
}
|
||||
|
||||
func resolveDomain(host string, strategy DeviceConfig_DomainStrategy, lookupIP func(host string) ([]net.IP, error)) (net.IP, error) {
|
||||
func (h *Handler) resolveDomain(host string, strategy DeviceConfig_DomainStrategy, lookupIP func(host string) ([]net.IP, uint32, error)) (net.IP, error) {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
ips, err := lookupIP(host)
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -409,6 +438,13 @@ func resolveDomain(host string, strategy DeviceConfig_DomainStrategy, lookupIP f
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -164,6 +164,7 @@ 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
|
||||
}
|
||||
@@ -261,6 +262,13 @@ 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 = "" +
|
||||
@@ -275,7 +283,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\"\xdc\x03\n" +
|
||||
"allowedIps\"\xee\x03\n" +
|
||||
"\fDeviceConfig\x12\x1d\n" +
|
||||
"\n" +
|
||||
"secret_key\x18\x01 \x01(\tR\tsecretKey\x12\x1a\n" +
|
||||
@@ -286,7 +294,9 @@ 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\"\\\n" +
|
||||
"\rno_kernel_tun\x18\t \x01(\bR\vnoKernelTun\x12\x10\n" +
|
||||
"\x03DNS\x18\n" +
|
||||
" \x03(\tR\x03DNS\"\\\n" +
|
||||
"\x0eDomainStrategy\x12\f\n" +
|
||||
"\bFORCE_IP\x10\x00\x12\r\n" +
|
||||
"\tFORCE_IP4\x10\x01\x12\r\n" +
|
||||
|
||||
@@ -34,4 +34,5 @@ message DeviceConfig {
|
||||
DomainStrategy domain_strategy = 7;
|
||||
bool is_client = 8;
|
||||
bool no_kernel_tun = 9;
|
||||
repeated string DNS = 10;
|
||||
}
|
||||
@@ -248,7 +248,7 @@ var (
|
||||
errTimeout = errors.New("i/o timeout")
|
||||
)
|
||||
|
||||
func (net *Net) LookupHost(host string) (addrs []string, err error) {
|
||||
func (net *Net) LookupHost(host string) (addrs []net.IP, ttl uint32, 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) ([]string, error) {
|
||||
func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]net.IP, uint32, error) {
|
||||
if host == "" || (!tnet.hasV6 && !tnet.hasV4) {
|
||||
return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
|
||||
return nil, 0, &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) ([]string,
|
||||
}
|
||||
}
|
||||
if ip, err := netip.ParseAddr(host[:zlen]); err == nil {
|
||||
return []string{ip.String()}, nil
|
||||
return []net.IP{ip.AsSlice()}, 0, nil
|
||||
}
|
||||
|
||||
if !isDomainName(host) {
|
||||
return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
|
||||
return nil, 0, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
|
||||
}
|
||||
type result struct {
|
||||
p dnsmessage.Parser
|
||||
@@ -611,6 +611,7 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
||||
lane <- result{p, server, err}
|
||||
}()
|
||||
}
|
||||
ttl := uint32(300)
|
||||
for l := 0; l < lanes; l++ {
|
||||
result := <-lane
|
||||
if result.error != nil {
|
||||
@@ -644,6 +645,7 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
||||
}
|
||||
break loop
|
||||
}
|
||||
ttl = min(ttl, h.TTL)
|
||||
addrsV4 = append(addrsV4, netip.AddrFrom4(a.A))
|
||||
|
||||
case dnsmessage.TypeAAAA:
|
||||
@@ -656,6 +658,7 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
||||
}
|
||||
break loop
|
||||
}
|
||||
ttl = min(ttl, h.TTL)
|
||||
addrsV6 = append(addrsV6, netip.AddrFrom16(aaaa.AAAA))
|
||||
|
||||
default:
|
||||
@@ -680,11 +683,11 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
||||
}
|
||||
|
||||
if len(addrs) == 0 && lastErr != nil {
|
||||
return nil, lastErr
|
||||
return nil, 0, lastErr
|
||||
}
|
||||
saddrs := make([]string, 0, len(addrs))
|
||||
ips := make([]net.IP, 0, len(addrs))
|
||||
for _, ip := range addrs {
|
||||
saddrs = append(saddrs, ip.String())
|
||||
ips = append(ips, ip.AsSlice())
|
||||
}
|
||||
return saddrs, nil
|
||||
return ips, ttl, nil
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) {
|
||||
if len(tag) > 0 && p.ForSystem().Stats.InboundUplink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + tag + ">>>traffic>>>uplink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
uplinkCounter = c
|
||||
}
|
||||
@@ -74,7 +74,7 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) {
|
||||
if len(tag) > 0 && p.ForSystem().Stats.InboundDownlink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + tag + ">>>traffic>>>downlink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
downlinkCounter = c
|
||||
}
|
||||
|
||||
@@ -27,9 +27,10 @@ type task struct {
|
||||
}
|
||||
|
||||
var (
|
||||
conns chan *websocket.Conn
|
||||
server *http.Server
|
||||
mu sync.Mutex
|
||||
conns chan *websocket.Conn
|
||||
server *http.Server
|
||||
currentAddr string
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
var upgrader = &websocket.Upgrader{
|
||||
@@ -47,8 +48,13 @@ func Reload() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if addr == currentAddr && (addr == "" || server != nil) {
|
||||
return
|
||||
}
|
||||
|
||||
if server != nil {
|
||||
server.Close()
|
||||
server = nil
|
||||
}
|
||||
if HasBrowserDialer() {
|
||||
for len(conns) > 0 {
|
||||
@@ -60,6 +66,7 @@ func Reload() {
|
||||
}
|
||||
conns = nil
|
||||
}
|
||||
currentAddr = addr
|
||||
if addr != "" {
|
||||
token := uuid.New()
|
||||
csrfToken := token.String()
|
||||
@@ -220,5 +227,8 @@ func CheckOK(conn *websocket.Conn) error {
|
||||
}
|
||||
|
||||
func init() {
|
||||
Reload()
|
||||
platform.RegisterEnvReload(func() error {
|
||||
Reload()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -443,22 +443,26 @@ 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"`
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
func (x *QuicParams) Reset() {
|
||||
@@ -519,6 +523,13 @@ 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
|
||||
@@ -575,6 +586,20 @@ 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
|
||||
@@ -582,6 +607,13 @@ 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"`
|
||||
@@ -1020,7 +1052,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\"\xf2\x04\n" +
|
||||
"\finterval_max\x18\x03 \x01(\x03R\vintervalMax\"\xc7\x06\n" +
|
||||
"\n" +
|
||||
"QuicParams\x12\x1e\n" +
|
||||
"\n" +
|
||||
@@ -1030,17 +1062,23 @@ 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\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" +
|
||||
"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" +
|
||||
"\vProxyConfig\x12\x10\n" +
|
||||
"\x03tag\x18\x01 \x01(\tR\x03tag\x120\n" +
|
||||
"\x13transportLayerProxy\x18\x02 \x01(\bR\x13transportLayerProxy\"\x93\x01\n" +
|
||||
|
||||
@@ -75,15 +75,19 @@ message QuicParams {
|
||||
string bbr_profile = 2;
|
||||
uint64 brutal_up = 3;
|
||||
uint64 brutal_down = 4;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
message ProxyConfig {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pion/stun/v3"
|
||||
@@ -15,35 +16,67 @@ 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)
|
||||
servers := resolveSTUNServers(c.PacketConn.LocalAddr().(*net.UDPAddr).IP, c.stunServers, c.family)
|
||||
errors.LogDebug(context.Background(), "[realm] update stun servers ", servers, " with ", time.Since(start))
|
||||
if len(servers) == 0 {
|
||||
return nil, errors.New("empty locals")
|
||||
@@ -70,7 +103,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)
|
||||
filteredPeers, seen := candidatePunchAddrs(locals, peers, c.family)
|
||||
errors.LogDebug(context.Background(), "[realm] filtered peers ", filteredPeers)
|
||||
expandedPeers := expandSymmetricNATCandidates(filteredPeers, seen)
|
||||
errors.LogDebug(context.Background(), "[realm] expanded peers ", expandedPeers)
|
||||
@@ -86,6 +119,11 @@ 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
|
||||
}
|
||||
@@ -116,10 +154,12 @@ 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
|
||||
}
|
||||
|
||||
@@ -169,3 +209,48 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,115 @@ 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"`
|
||||
@@ -31,13 +140,15 @@ 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[0]
|
||||
mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -49,7 +160,7 @@ func (x *Config) String() string {
|
||||
func (*Config) ProtoMessage() {}
|
||||
|
||||
func (x *Config) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[0]
|
||||
mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -62,7 +173,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{0}
|
||||
return file_transport_internet_finalmask_realm_config_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *Config) GetScheme() string {
|
||||
@@ -114,11 +225,29 @@ 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\"\xd5\x01\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" +
|
||||
"\x06Config\x12\x16\n" +
|
||||
"\x06scheme\x18\x01 \x01(\tR\x06scheme\x12\x12\n" +
|
||||
"\x04host\x18\x02 \x01(\tR\x04host\x12\x12\n" +
|
||||
@@ -127,7 +256,13 @@ 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\ttlsConfigB\x97\x01\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" +
|
||||
"+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 (
|
||||
@@ -142,18 +277,22 @@ 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_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
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_goTypes = []any{
|
||||
(*Config)(nil), // 0: xray.transport.internet.finalmask.realm.Config
|
||||
(*tls.Config)(nil), // 1: xray.transport.internet.tls.Config
|
||||
(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
|
||||
}
|
||||
var file_transport_internet_finalmask_realm_config_proto_depIdxs = []int32{
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
func init() { file_transport_internet_finalmask_realm_config_proto_init() }
|
||||
@@ -166,13 +305,14 @@ 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: 0,
|
||||
NumMessages: 1,
|
||||
NumEnums: 1,
|
||||
NumMessages: 2,
|
||||
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,6 +8,18 @@ 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;
|
||||
@@ -16,4 +28,6 @@ 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;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
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)
|
||||
}
|
||||
@@ -33,14 +33,16 @@ type STUNPacketEvent struct {
|
||||
}
|
||||
|
||||
type realmConnServer struct {
|
||||
cleaned chan struct{}
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
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
|
||||
@@ -57,8 +59,29 @@ 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,
|
||||
@@ -66,6 +89,8 @@ 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,
|
||||
@@ -74,6 +99,12 @@ 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
|
||||
@@ -137,6 +168,8 @@ 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:
|
||||
@@ -148,6 +181,9 @@ 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())
|
||||
})
|
||||
@@ -159,7 +195,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)
|
||||
servers := resolveSTUNServers(c.PacketConn.LocalAddr().(*net.UDPAddr).IP, c.stunServers, c.family)
|
||||
errors.LogDebug(context.Background(), "[realm] update stun servers ", servers, " with ", time.Since(start))
|
||||
if len(servers) > 0 {
|
||||
start = time.Now()
|
||||
@@ -232,7 +268,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) {
|
||||
close(c.cleaned)
|
||||
c.wg.Done()
|
||||
return
|
||||
}
|
||||
backoff *= 2
|
||||
@@ -259,7 +295,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")
|
||||
close(c.cleaned)
|
||||
c.wg.Done()
|
||||
return
|
||||
default:
|
||||
goto retry
|
||||
@@ -360,7 +396,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)
|
||||
filteredPeers, seen := candidatePunchAddrs(locals, peers, c.family)
|
||||
errors.LogDebug(context.Background(), "[realm] ", ev.Nonce, " filtered peers ", filteredPeers)
|
||||
expandedPeers := expandSymmetricNATCandidates(filteredPeers, seen)
|
||||
errors.LogDebug(context.Background(), "[realm] ", ev.Nonce, " expanded peers ", expandedPeers)
|
||||
@@ -398,6 +434,6 @@ func (c *realmConnServer) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||
|
||||
func (c *realmConnServer) Close() error {
|
||||
c.cancel()
|
||||
<-c.cleaned
|
||||
c.wg.Wait()
|
||||
return c.PacketConn.Close()
|
||||
}
|
||||
|
||||
@@ -23,14 +23,23 @@ const (
|
||||
symmetricNATMaxPortsPerHost = 32
|
||||
)
|
||||
|
||||
func resolveSTUNServers(local net.IP, servers []string) []*net.UDPAddr {
|
||||
func resolveSTUNServers(local net.IP, servers []string, family Family) []*net.UDPAddr {
|
||||
var network string
|
||||
if local.IsUnspecified() {
|
||||
network = "ip"
|
||||
} else {
|
||||
if local.To4() != nil {
|
||||
network = "ip4"
|
||||
if family == Family_Dual {
|
||||
if local.IsUnspecified() {
|
||||
network = "ip"
|
||||
} else {
|
||||
if local.To4() != nil {
|
||||
network = "ip4"
|
||||
} else {
|
||||
network = "ip6"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if family == Family_V4 {
|
||||
network = "ip4"
|
||||
}
|
||||
if family == Family_V6 {
|
||||
network = "ip6"
|
||||
}
|
||||
}
|
||||
@@ -104,7 +113,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) ([]netip.AddrPort, map[netip.AddrPort]struct{}) {
|
||||
func candidatePunchAddrs(locals, peers []netip.AddrPort, family Family) ([]netip.AddrPort, map[netip.AddrPort]struct{}) {
|
||||
var allow4, allow6 bool
|
||||
for _, local := range locals {
|
||||
if local.Addr().Is4() {
|
||||
@@ -116,6 +125,12 @@ func candidatePunchAddrs(locals, peers []netip.AddrPort) ([]netip.AddrPort, map[
|
||||
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 {
|
||||
@@ -218,3 +233,17 @@ 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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package xmc
|
||||
|
||||
// Copied from https://github.com/Tnze/go-mc/blob/539b4a3a7f030332eb58b8a946116ae7907630d2/net/CFB8/cfb8.go
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/subtle"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type cfb8 struct {
|
||||
c cipher.Block
|
||||
blockSize int
|
||||
ivPos int
|
||||
iv []byte
|
||||
de bool
|
||||
}
|
||||
|
||||
func newCFB8Decrypt(c cipher.Block, iv []byte) *cfb8 {
|
||||
return newCFB8(c, iv, true)
|
||||
}
|
||||
|
||||
func newCFB8Encrypt(c cipher.Block, iv []byte) *cfb8 {
|
||||
return newCFB8(c, iv, false)
|
||||
}
|
||||
|
||||
func newCFB8(c cipher.Block, iv []byte, de bool) *cfb8 {
|
||||
cp := make([]byte, len(iv)*3)
|
||||
copy(cp, iv)
|
||||
return &cfb8{
|
||||
c: c,
|
||||
blockSize: c.BlockSize(),
|
||||
iv: cp,
|
||||
de: de,
|
||||
}
|
||||
}
|
||||
|
||||
func (cf *cfb8) XORKeyStream(dst, src []byte) {
|
||||
if len(src) == 0 {
|
||||
return
|
||||
}
|
||||
if len(dst) < len(src) {
|
||||
panic("cfb8: output smaller than input")
|
||||
}
|
||||
|
||||
// If dst and src does not overlap in first block size,
|
||||
// and the length of src is greater than 2*blockSize,
|
||||
// we can use an optimized implementation.
|
||||
if len(src) > cf.blockSize<<1 &&
|
||||
(uintptr(unsafe.Pointer(&dst[0]))+uintptr(cf.blockSize) <= uintptr(unsafe.Pointer(&src[0])) ||
|
||||
uintptr(unsafe.Pointer(&src[0]))+uintptr(len(src)) <= uintptr(unsafe.Pointer(&dst[0]))) {
|
||||
// encrypt/decrypt first blockSize bytes
|
||||
// After this, the IV will come to the same as
|
||||
// the last blockSize of ciphertext, so
|
||||
// we can reuse them without copy.
|
||||
cf.xorKeyStream(dst, src[:cf.blockSize])
|
||||
var ciphertext []byte
|
||||
if cf.de {
|
||||
ciphertext = src
|
||||
} else {
|
||||
ciphertext = dst
|
||||
}
|
||||
dst = dst[cf.blockSize:]
|
||||
src = src[cf.blockSize:]
|
||||
iv := cf.iv
|
||||
_ = iv[0] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
var (
|
||||
i int
|
||||
val byte
|
||||
)
|
||||
dst = dst[:len(src)]
|
||||
if cf.de && // and requires to be non-overlapping at all
|
||||
uintptr(unsafe.Pointer(&dst[0])) <= uintptr(unsafe.Pointer(&src[len(src)-1])) &&
|
||||
uintptr(unsafe.Pointer(&src[0])) <= uintptr(unsafe.Pointer(&dst[len(dst)-1])) {
|
||||
for i = 0; i < len(src)-cf.blockSize; i += 1 {
|
||||
cf.c.Encrypt(dst[i:], ciphertext[i:])
|
||||
}
|
||||
subtle.XORBytes(dst, src[:i], dst)
|
||||
for ; i < len(src); i += 1 {
|
||||
cf.c.Encrypt(iv, ciphertext[i:])
|
||||
dst[i] = src[i] ^ iv[0]
|
||||
}
|
||||
} else {
|
||||
_ = ciphertext[len(src)]
|
||||
for i, val = range src {
|
||||
cf.c.Encrypt(iv, ciphertext[i:])
|
||||
dst[i] = val ^ iv[0]
|
||||
}
|
||||
// for-range does not increase i in the last loop,
|
||||
// compared to the classic for clause
|
||||
i += 1
|
||||
}
|
||||
// copy the current IV for next operation
|
||||
copy(iv, ciphertext[i:i+cf.blockSize])
|
||||
cf.ivPos = 0
|
||||
return
|
||||
}
|
||||
|
||||
cf.xorKeyStream(dst, src)
|
||||
}
|
||||
|
||||
func (cf *cfb8) xorKeyStream(dst, src []byte) {
|
||||
dst = dst[:len(src)] // remove bounds check in loop
|
||||
for i, val := range src {
|
||||
posPlusBlockSize := cf.ivPos + cf.blockSize
|
||||
// fast mod; 2*blockSize must be a non-negative integer power of 2
|
||||
tempPos := posPlusBlockSize & (cf.blockSize<<1 - 1)
|
||||
// reuse space to store encrypted block
|
||||
cf.c.Encrypt(cf.iv[tempPos:], cf.iv[cf.ivPos:])
|
||||
// Only the first byte of the encrypted block is used
|
||||
// for encryption/decryption, other bytes are ignored.
|
||||
val ^= cf.iv[tempPos]
|
||||
|
||||
if cf.ivPos == cf.blockSize<<1 {
|
||||
// bound reached; move to next round for next operation
|
||||
// copy next block to the start of the ring buffer
|
||||
copy(cf.iv, cf.iv[cf.ivPos+1:])
|
||||
// insert the encrypted byte to the end of IV
|
||||
if cf.de {
|
||||
cf.iv[cf.blockSize-1] = src[i]
|
||||
} else {
|
||||
cf.iv[cf.blockSize-1] = val
|
||||
}
|
||||
cf.ivPos = 0
|
||||
} else {
|
||||
// insert the encrypted byte to the end of IV
|
||||
if cf.de {
|
||||
cf.iv[posPlusBlockSize] = src[i]
|
||||
} else {
|
||||
cf.iv[posPlusBlockSize] = val
|
||||
}
|
||||
// move to next block
|
||||
cf.ivPos += 1
|
||||
}
|
||||
|
||||
dst[i] = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type clientConn struct {
|
||||
reader io.Reader
|
||||
writer io.Writer
|
||||
c net.Conn
|
||||
|
||||
state clientState
|
||||
|
||||
handshakeLock sync.Mutex
|
||||
lifecycleMu sync.Mutex
|
||||
closed bool
|
||||
profiles []loginProfile
|
||||
password string
|
||||
rsaPublicKey []byte
|
||||
hostname string
|
||||
paddingSchedule []paddingTurn
|
||||
packet *packetStream
|
||||
deadlines *connectionDeadlines
|
||||
}
|
||||
|
||||
type clientState int
|
||||
|
||||
var (
|
||||
clientStateHandshake clientState = 1
|
||||
clientStateProxy clientState = 2
|
||||
)
|
||||
|
||||
func newClientConn(c net.Conn, profiles []loginProfile, password string, rsaPublicKey []byte, hostname string) (*clientConn, error) {
|
||||
if len(rsaPublicKey) == 0 {
|
||||
return nil, fmt.Errorf("empty rsa public key")
|
||||
}
|
||||
if len(profiles) == 0 {
|
||||
return nil, fmt.Errorf("empty profiles")
|
||||
}
|
||||
paddingSchedule, err := newClientPaddingSchedule2612()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("select padding profile: %w", err)
|
||||
}
|
||||
return &clientConn{
|
||||
reader: bufio.NewReader(c),
|
||||
writer: c,
|
||||
c: c,
|
||||
state: clientStateHandshake,
|
||||
handshakeLock: sync.Mutex{},
|
||||
profiles: profiles,
|
||||
password: password,
|
||||
rsaPublicKey: rsaPublicKey,
|
||||
hostname: hostname,
|
||||
paddingSchedule: paddingSchedule,
|
||||
deadlines: newConnectionDeadlines(c),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *clientConn) handshake() error {
|
||||
c.handshakeLock.Lock()
|
||||
defer c.handshakeLock.Unlock()
|
||||
|
||||
if c.state != clientStateHandshake {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := c.deadlines.beginHandshake(); err != nil {
|
||||
return fmt.Errorf("set deadline: %w", err)
|
||||
}
|
||||
defer func() { _ = c.deadlines.endHandshake() }()
|
||||
|
||||
var (
|
||||
protocolVersion Varint = Varint(775)
|
||||
serverAddress String = String(c.hostname)
|
||||
serverPort UnsignedShort = UnsignedShort(25565)
|
||||
nextState Varint = Varint(2)
|
||||
)
|
||||
|
||||
host, portString, err := net.SplitHostPort(c.c.RemoteAddr().String())
|
||||
if err == nil {
|
||||
port, err := strconv.Atoi(portString)
|
||||
if err == nil {
|
||||
serverPort = UnsignedShort(port)
|
||||
}
|
||||
|
||||
if serverAddress == "" {
|
||||
serverAddress = String(host)
|
||||
}
|
||||
}
|
||||
|
||||
err = writePacket(c.writer, 0x00, &protocolVersion, &serverAddress, &serverPort, &nextState)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write handshake packet: %w", err)
|
||||
}
|
||||
|
||||
// Login Start
|
||||
randomProfile, err := rand.Int(rand.Reader, big.NewInt(int64(len(c.profiles))))
|
||||
if err != nil {
|
||||
return fmt.Errorf("select profile: %w", err)
|
||||
}
|
||||
selectedProfile := c.profiles[randomProfile.Int64()]
|
||||
username := String(selectedProfile.Username)
|
||||
|
||||
err = writePacket(c.writer, 0x00, &username, &selectedProfile.UUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write login start: %w", err)
|
||||
}
|
||||
|
||||
// Encryption Request
|
||||
pkt, err := readPacket(c.reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read encryption request: %w", err)
|
||||
}
|
||||
|
||||
if pkt.packetID != 0x01 {
|
||||
return fmt.Errorf("bad encrypt request packet id")
|
||||
}
|
||||
|
||||
var (
|
||||
serverId String
|
||||
publicKey Bytes
|
||||
verifyToken Bytes
|
||||
)
|
||||
|
||||
err = pkt.readFields(&serverId, &publicKey, &verifyToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read encryption request fields: %w", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(publicKey, c.rsaPublicKey) {
|
||||
return fmt.Errorf("server public key mismatch")
|
||||
}
|
||||
|
||||
k, err := x509.ParsePKIXPublicKey(publicKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse server public key: %w", err)
|
||||
}
|
||||
|
||||
rsaPublicKey, ok := k.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return fmt.Errorf("parse server public key: not rsa")
|
||||
}
|
||||
|
||||
sharedSecret := make([]byte, 16)
|
||||
if _, err = rand.Read(sharedSecret); err != nil {
|
||||
return fmt.Errorf("generate shared secret: %w", err)
|
||||
}
|
||||
|
||||
encryptedSharedSecret, err := rsa.EncryptPKCS1v15(rand.Reader, rsaPublicKey, sharedSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt shared secret: %w", err)
|
||||
}
|
||||
|
||||
verifyToken = append(verifyToken, []byte(c.password)...) // append pre-shared password
|
||||
|
||||
encryptedVerifyToken, err := rsa.EncryptPKCS1v15(rand.Reader, rsaPublicKey, verifyToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt verify token: %w", err)
|
||||
}
|
||||
|
||||
// Send Encryption Response
|
||||
err = writePacket(
|
||||
c.writer,
|
||||
0x01,
|
||||
(*Bytes)(&encryptedSharedSecret),
|
||||
(*Bytes)(&encryptedVerifyToken),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write encryption response: %w", err)
|
||||
}
|
||||
|
||||
// Enable encryption
|
||||
c.reader, err = newCryptoReader(c.reader, sharedSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("new crypto reader: %w", err)
|
||||
}
|
||||
|
||||
c.writer, err = newCryptoWriter(c.writer, sharedSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("new crypto writer: %w", err)
|
||||
}
|
||||
|
||||
pkt, err = readPacket(c.reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read login finished: %w", err)
|
||||
}
|
||||
if pkt.packetID == 0x00 {
|
||||
var reason String
|
||||
if readErr := pkt.readFields(&reason); readErr != nil {
|
||||
return fmt.Errorf("authentication rejected")
|
||||
}
|
||||
return fmt.Errorf("authentication rejected: %s", reason)
|
||||
}
|
||||
if pkt.packetID != 0x02 {
|
||||
return fmt.Errorf("bad login finished packet id: %d", pkt.packetID)
|
||||
}
|
||||
|
||||
receivedProfile, err := readLoginSuccess(pkt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read login finished fields: %w", err)
|
||||
}
|
||||
if receivedProfile != selectedProfile {
|
||||
return fmt.Errorf("login profile mismatch")
|
||||
}
|
||||
loginAcknowledgedLength, err := writePacketWithLength(c.writer, 0x03)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write login acknowledged: %w", err)
|
||||
}
|
||||
if err = runPaddingSchedule(c.reader, c.writer, true, loginAcknowledgedLength, c.paddingSchedule); err != nil {
|
||||
return fmt.Errorf("run startup padding: %w", err)
|
||||
}
|
||||
|
||||
packet := newPacketStream(c.reader, c.writer, true)
|
||||
c.lifecycleMu.Lock()
|
||||
if c.closed {
|
||||
c.lifecycleMu.Unlock()
|
||||
packet.Stop()
|
||||
return net.ErrClosed
|
||||
}
|
||||
c.packet = packet
|
||||
c.reader = packet
|
||||
c.writer = packet
|
||||
c.state = clientStateProxy
|
||||
c.lifecycleMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clientConn) Read(b []byte) (int, error) {
|
||||
err := c.handshake()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("handshake: %w", err)
|
||||
}
|
||||
|
||||
return c.reader.Read(b)
|
||||
}
|
||||
|
||||
func (c *clientConn) Write(b []byte) (int, error) {
|
||||
err := c.handshake()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("handshake: %w", err)
|
||||
}
|
||||
|
||||
return c.writer.Write(b)
|
||||
}
|
||||
|
||||
func (c *clientConn) Close() error {
|
||||
c.lifecycleMu.Lock()
|
||||
c.closed = true
|
||||
packet := c.packet
|
||||
c.lifecycleMu.Unlock()
|
||||
if packet != nil {
|
||||
packet.Stop()
|
||||
}
|
||||
return c.c.Close()
|
||||
}
|
||||
|
||||
func (c *clientConn) LocalAddr() net.Addr {
|
||||
return c.c.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *clientConn) RemoteAddr() net.Addr {
|
||||
return c.c.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *clientConn) SetDeadline(t time.Time) error {
|
||||
return c.deadlines.setDeadline(t)
|
||||
}
|
||||
|
||||
func (c *clientConn) SetReadDeadline(t time.Time) error {
|
||||
return c.deadlines.setReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *clientConn) SetWriteDeadline(t time.Time) error {
|
||||
return c.deadlines.setWriteDeadline(t)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
func (c *Config) TCP() {
|
||||
}
|
||||
|
||||
func (c *Config) WrapConnClient(conn net.Conn) (net.Conn, error) {
|
||||
profiles, err := profilesFromConfig(c.Profiles)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minecraft finalmask: %w", err)
|
||||
}
|
||||
cc, err := newClientConn(conn, profiles, c.Password, c.RsaPublicKey, c.Hostname)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minecraft finalmask: %w", err)
|
||||
}
|
||||
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
func (c *Config) WrapConnServer(conn net.Conn) (net.Conn, error) {
|
||||
profiles, err := profilesFromConfig(c.Profiles)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minecraft finalmask: %w", err)
|
||||
}
|
||||
cc, err := wrapConnServer(conn, profiles, c.Password, c.RsaPrivateKey, c.RsaPublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minecraft finalmask: %w", err)
|
||||
}
|
||||
|
||||
return cc, nil
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.5
|
||||
// source: transport/internet/finalmask/xmc/config.proto
|
||||
|
||||
package xmc
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type Profile struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Resolve the UUID from https://api.mojang.com/users/profiles/minecraft/{username}.
|
||||
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
|
||||
Uuid []byte `protobuf:"bytes,2,opt,name=uuid,proto3" json:"uuid,omitempty"`
|
||||
// Copy the signed textures property returned by
|
||||
// https://sessionserver.mojang.com/session/minecraft/profile/{uuid}?unsigned=false.
|
||||
TexturesValue string `protobuf:"bytes,3,opt,name=textures_value,json=texturesValue,proto3" json:"textures_value,omitempty"`
|
||||
TexturesSignature string `protobuf:"bytes,4,opt,name=textures_signature,json=texturesSignature,proto3" json:"textures_signature,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Profile) Reset() {
|
||||
*x = Profile{}
|
||||
mi := &file_transport_internet_finalmask_xmc_config_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Profile) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Profile) ProtoMessage() {}
|
||||
|
||||
func (x *Profile) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_finalmask_xmc_config_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Profile.ProtoReflect.Descriptor instead.
|
||||
func (*Profile) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_finalmask_xmc_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Profile) GetUsername() string {
|
||||
if x != nil {
|
||||
return x.Username
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Profile) GetUuid() []byte {
|
||||
if x != nil {
|
||||
return x.Uuid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Profile) GetTexturesValue() string {
|
||||
if x != nil {
|
||||
return x.TexturesValue
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Profile) GetTexturesSignature() string {
|
||||
if x != nil {
|
||||
return x.TexturesSignature
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"`
|
||||
RsaPrivateKey []byte `protobuf:"bytes,8,opt,name=rsa_private_key,json=rsaPrivateKey,proto3" json:"rsa_private_key,omitempty"`
|
||||
RsaPublicKey []byte `protobuf:"bytes,9,opt,name=rsa_public_key,json=rsaPublicKey,proto3" json:"rsa_public_key,omitempty"`
|
||||
Hostname string `protobuf:"bytes,10,opt,name=hostname,proto3" json:"hostname,omitempty"`
|
||||
Profiles []*Profile `protobuf:"bytes,11,rep,name=profiles,proto3" json:"profiles,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Config) Reset() {
|
||||
*x = Config{}
|
||||
mi := &file_transport_internet_finalmask_xmc_config_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Config) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Config) ProtoMessage() {}
|
||||
|
||||
func (x *Config) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_finalmask_xmc_config_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
|
||||
func (*Config) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_finalmask_xmc_config_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *Config) GetPassword() string {
|
||||
if x != nil {
|
||||
return x.Password
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetRsaPrivateKey() []byte {
|
||||
if x != nil {
|
||||
return x.RsaPrivateKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetRsaPublicKey() []byte {
|
||||
if x != nil {
|
||||
return x.RsaPublicKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetHostname() string {
|
||||
if x != nil {
|
||||
return x.Hostname
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetProfiles() []*Profile {
|
||||
if x != nil {
|
||||
return x.Profiles
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_transport_internet_finalmask_xmc_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_transport_internet_finalmask_xmc_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"-transport/internet/finalmask/xmc/config.proto\x12%xray.transport.internet.finalmask.xmc\"\x8f\x01\n" +
|
||||
"\aProfile\x12\x1a\n" +
|
||||
"\busername\x18\x01 \x01(\tR\busername\x12\x12\n" +
|
||||
"\x04uuid\x18\x02 \x01(\fR\x04uuid\x12%\n" +
|
||||
"\x0etextures_value\x18\x03 \x01(\tR\rtexturesValue\x12-\n" +
|
||||
"\x12textures_signature\x18\x04 \x01(\tR\x11texturesSignature\"\xe0\x01\n" +
|
||||
"\x06Config\x12\x1a\n" +
|
||||
"\bpassword\x18\x01 \x01(\tR\bpassword\x12&\n" +
|
||||
"\x0frsa_private_key\x18\b \x01(\fR\rrsaPrivateKey\x12$\n" +
|
||||
"\x0ersa_public_key\x18\t \x01(\fR\frsaPublicKey\x12\x1a\n" +
|
||||
"\bhostname\x18\n" +
|
||||
" \x01(\tR\bhostname\x12J\n" +
|
||||
"\bprofiles\x18\v \x03(\v2..xray.transport.internet.finalmask.xmc.ProfileR\bprofilesJ\x04\b\x02\x10\x03B\x91\x01\n" +
|
||||
")com.xray.transport.internet.finalmask.xmcP\x01Z:github.com/xtls/xray-core/transport/internet/finalmask/xmc\xaa\x02%Xray.Transport.Internet.Finalmask.XMCb\x06proto3"
|
||||
|
||||
var (
|
||||
file_transport_internet_finalmask_xmc_config_proto_rawDescOnce sync.Once
|
||||
file_transport_internet_finalmask_xmc_config_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_transport_internet_finalmask_xmc_config_proto_rawDescGZIP() []byte {
|
||||
file_transport_internet_finalmask_xmc_config_proto_rawDescOnce.Do(func() {
|
||||
file_transport_internet_finalmask_xmc_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_xmc_config_proto_rawDesc), len(file_transport_internet_finalmask_xmc_config_proto_rawDesc)))
|
||||
})
|
||||
return file_transport_internet_finalmask_xmc_config_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_transport_internet_finalmask_xmc_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_transport_internet_finalmask_xmc_config_proto_goTypes = []any{
|
||||
(*Profile)(nil), // 0: xray.transport.internet.finalmask.xmc.Profile
|
||||
(*Config)(nil), // 1: xray.transport.internet.finalmask.xmc.Config
|
||||
}
|
||||
var file_transport_internet_finalmask_xmc_config_proto_depIdxs = []int32{
|
||||
0, // 0: xray.transport.internet.finalmask.xmc.Config.profiles:type_name -> xray.transport.internet.finalmask.xmc.Profile
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_transport_internet_finalmask_xmc_config_proto_init() }
|
||||
func file_transport_internet_finalmask_xmc_config_proto_init() {
|
||||
if File_transport_internet_finalmask_xmc_config_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_xmc_config_proto_rawDesc), len(file_transport_internet_finalmask_xmc_config_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_transport_internet_finalmask_xmc_config_proto_goTypes,
|
||||
DependencyIndexes: file_transport_internet_finalmask_xmc_config_proto_depIdxs,
|
||||
MessageInfos: file_transport_internet_finalmask_xmc_config_proto_msgTypes,
|
||||
}.Build()
|
||||
File_transport_internet_finalmask_xmc_config_proto = out.File
|
||||
file_transport_internet_finalmask_xmc_config_proto_goTypes = nil
|
||||
file_transport_internet_finalmask_xmc_config_proto_depIdxs = nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package xray.transport.internet.finalmask.xmc;
|
||||
option csharp_namespace = "Xray.Transport.Internet.Finalmask.XMC";
|
||||
option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/xmc";
|
||||
option java_package = "com.xray.transport.internet.finalmask.xmc";
|
||||
option java_multiple_files = true;
|
||||
|
||||
message Profile {
|
||||
// Resolve the UUID from https://api.mojang.com/users/profiles/minecraft/{username}.
|
||||
string username = 1;
|
||||
bytes uuid = 2;
|
||||
// Copy the signed textures property returned by
|
||||
// https://sessionserver.mojang.com/session/minecraft/profile/{uuid}?unsigned=false.
|
||||
string textures_value = 3;
|
||||
string textures_signature = 4;
|
||||
}
|
||||
|
||||
message Config {
|
||||
string password = 1;
|
||||
reserved 2;
|
||||
bytes rsa_private_key = 8;
|
||||
bytes rsa_public_key = 9;
|
||||
string hostname = 10;
|
||||
repeated Profile profiles = 11;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const handshakeTimeout = 2 * time.Minute
|
||||
|
||||
type connectionDeadlines struct {
|
||||
mu sync.Mutex
|
||||
c net.Conn
|
||||
|
||||
read time.Time
|
||||
write time.Time
|
||||
handshake time.Time
|
||||
}
|
||||
|
||||
func newConnectionDeadlines(c net.Conn) *connectionDeadlines {
|
||||
return &connectionDeadlines{c: c}
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) beginHandshake() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.handshake = time.Now().Add(handshakeTimeout)
|
||||
if err := d.applyLocked(); err != nil {
|
||||
d.handshake = time.Time{}
|
||||
_ = d.applyLocked()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) endHandshake() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.handshake = time.Time{}
|
||||
return d.applyLocked()
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) setDeadline(t time.Time) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.read = t
|
||||
d.write = t
|
||||
return d.applyLocked()
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) setReadDeadline(t time.Time) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.read = t
|
||||
return d.c.SetReadDeadline(earlierDeadline(d.read, d.handshake))
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) setWriteDeadline(t time.Time) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.write = t
|
||||
return d.c.SetWriteDeadline(earlierDeadline(d.write, d.handshake))
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) applyLocked() error {
|
||||
if err := d.c.SetReadDeadline(earlierDeadline(d.read, d.handshake)); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.c.SetWriteDeadline(earlierDeadline(d.write, d.handshake))
|
||||
}
|
||||
|
||||
func earlierDeadline(user, internal time.Time) time.Time {
|
||||
if internal.IsZero() {
|
||||
return user
|
||||
}
|
||||
if user.IsZero() || internal.Before(user) {
|
||||
return internal
|
||||
}
|
||||
return user
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConnectionDeadlinesRestoreCallerValues(t *testing.T) {
|
||||
client, server := net.Pipe()
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
|
||||
recording := &deadlineRecordingConn{Conn: client}
|
||||
deadlines := newConnectionDeadlines(recording)
|
||||
callerDeadline := time.Now().Add(10 * time.Minute)
|
||||
if err := deadlines.setDeadline(callerDeadline); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := deadlines.beginHandshake(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
read, write := recording.currentDeadlines()
|
||||
if !read.Before(callerDeadline) || !write.Before(callerDeadline) {
|
||||
t.Fatalf("handshake deadlines = %s/%s, caller = %s", read, write, callerDeadline)
|
||||
}
|
||||
|
||||
shortReadDeadline := time.Now().Add(time.Second)
|
||||
if err := deadlines.setReadDeadline(shortReadDeadline); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
read, _ = recording.currentDeadlines()
|
||||
if !read.Equal(shortReadDeadline) {
|
||||
t.Fatalf("read deadline = %s, want %s", read, shortReadDeadline)
|
||||
}
|
||||
|
||||
if err := deadlines.endHandshake(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
read, write = recording.currentDeadlines()
|
||||
if !read.Equal(shortReadDeadline) || !write.Equal(callerDeadline) {
|
||||
t.Fatalf("restored deadlines = %s/%s, want %s/%s", read, write, shortReadDeadline, callerDeadline)
|
||||
}
|
||||
}
|
||||
|
||||
type deadlineRecordingConn struct {
|
||||
net.Conn
|
||||
mu sync.Mutex
|
||||
read time.Time
|
||||
write time.Time
|
||||
}
|
||||
|
||||
func (c *deadlineRecordingConn) SetDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
c.read = t
|
||||
c.write = t
|
||||
c.mu.Unlock()
|
||||
return c.Conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *deadlineRecordingConn) SetReadDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
c.read = t
|
||||
c.mu.Unlock()
|
||||
return c.Conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *deadlineRecordingConn) SetWriteDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
c.write = t
|
||||
c.mu.Unlock()
|
||||
return c.Conn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (c *deadlineRecordingConn) currentDeadlines() (time.Time, time.Time) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.read, c.write
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
type sha256Stream struct {
|
||||
seed []byte
|
||||
counter uint64
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func newSHA256Stream(seed []byte) *sha256Stream {
|
||||
return &sha256Stream{
|
||||
seed: seed,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sha256Stream) Read(p []byte) (n int, err error) {
|
||||
for len(p) > len(s.buf) {
|
||||
h := sha256.New()
|
||||
h.Write(s.seed)
|
||||
h.Write([]byte(fmt.Sprintf("-%d", s.counter)))
|
||||
s.counter++
|
||||
s.buf = append(s.buf, h.Sum(nil)...)
|
||||
}
|
||||
n = copy(p, s.buf)
|
||||
s.buf = s.buf[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func derivePrime(stream *sha256Stream) *big.Int {
|
||||
pBytes := make([]byte, 64) // 512 bits
|
||||
_, _ = stream.Read(pBytes)
|
||||
pBytes[0] |= 0xc0 // ensure it is big enough so p*q is 1024 bits
|
||||
pBytes[63] |= 0x01 // ensure odd
|
||||
|
||||
p := new(big.Int).SetBytes(pBytes)
|
||||
for {
|
||||
if p.ProbablyPrime(20) {
|
||||
pMinus1 := new(big.Int).Sub(p, big.NewInt(1))
|
||||
e := big.NewInt(65537)
|
||||
gcd := new(big.Int).GCD(nil, nil, pMinus1, e)
|
||||
if gcd.Cmp(big.NewInt(1)) == 0 {
|
||||
return p
|
||||
}
|
||||
}
|
||||
p.Add(p, big.NewInt(2))
|
||||
}
|
||||
}
|
||||
|
||||
// DeriveRSAKey derives a 1024-bit RSA private key from a password.
|
||||
func DeriveRSAKey(password string) (*rsa.PrivateKey, error) {
|
||||
seed := []byte(password)
|
||||
|
||||
pStream := newSHA256Stream(append(seed, []byte("-p-prime")...))
|
||||
qStream := newSHA256Stream(append(seed, []byte("-q-prime")...))
|
||||
|
||||
p := derivePrime(pStream)
|
||||
q := derivePrime(qStream)
|
||||
|
||||
// ensure p != q (if they are, let's search q further)
|
||||
for p.Cmp(q) == 0 {
|
||||
q.Add(q, big.NewInt(2))
|
||||
for {
|
||||
if q.ProbablyPrime(20) {
|
||||
qMinus1 := new(big.Int).Sub(q, big.NewInt(1))
|
||||
e := big.NewInt(65537)
|
||||
gcd := new(big.Int).GCD(nil, nil, qMinus1, e)
|
||||
if gcd.Cmp(big.NewInt(1)) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
q.Add(q, big.NewInt(2))
|
||||
}
|
||||
}
|
||||
|
||||
n := new(big.Int).Mul(p, q)
|
||||
pMinus1 := new(big.Int).Sub(p, big.NewInt(1))
|
||||
qMinus1 := new(big.Int).Sub(q, big.NewInt(1))
|
||||
totient := new(big.Int).Mul(pMinus1, qMinus1)
|
||||
|
||||
e := big.NewInt(65537)
|
||||
d := new(big.Int).ModInverse(e, totient)
|
||||
if d == nil {
|
||||
return nil, fmt.Errorf("failed to compute mod inverse")
|
||||
}
|
||||
|
||||
priv := &rsa.PrivateKey{
|
||||
PublicKey: rsa.PublicKey{
|
||||
N: n,
|
||||
E: 65537,
|
||||
},
|
||||
D: d,
|
||||
Primes: []*big.Int{p, q},
|
||||
}
|
||||
priv.Precompute()
|
||||
|
||||
return priv, nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeriveRSAKey(t *testing.T) {
|
||||
password := "my-very-secret-password-12345"
|
||||
|
||||
key1, err := DeriveRSAKey(password)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to derive key: %v", err)
|
||||
}
|
||||
|
||||
err = key1.Validate()
|
||||
if err != nil {
|
||||
t.Fatalf("key is not valid RSA key: %v", err)
|
||||
}
|
||||
|
||||
key2, err := DeriveRSAKey(password)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to derive key second time: %v", err)
|
||||
}
|
||||
|
||||
// Verify determinism
|
||||
if key1.D.Cmp(key2.D) != 0 || key1.N.Cmp(key2.N) != 0 {
|
||||
t.Errorf("derived keys are not identical for the same password")
|
||||
}
|
||||
|
||||
// Verify different passwords yield different keys
|
||||
keyDifferent, err := DeriveRSAKey(password + "-different")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to derive different key: %v", err)
|
||||
}
|
||||
|
||||
if key1.D.Cmp(keyDifferent.D) == 0 || key1.N.Cmp(keyDifferent.N) == 0 {
|
||||
t.Errorf("derived keys are identical for different passwords")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveRSAKeyGoldenPrivateKey(t *testing.T) {
|
||||
const password = "deterministic-rsa-key-golden"
|
||||
const wantPKCS1DERHash = "3a8c4ad56a6fb42dab73c4d5fc3af754460a2db1441edc0970cbc7f4e0798d2f"
|
||||
|
||||
key, err := DeriveRSAKey(password)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to derive key: %v", err)
|
||||
}
|
||||
|
||||
gotHash := sha256.Sum256(x509.MarshalPKCS1PrivateKey(key))
|
||||
got := hex.EncodeToString(gotHash[:])
|
||||
if got != wantPKCS1DERHash {
|
||||
t.Fatalf("derived private key changed\nwant sha256: %s\n got sha256: %s", wantPKCS1DERHash, got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func deriveTestRSAKey(t *testing.T, password string) ([]byte, []byte) {
|
||||
t.Helper()
|
||||
|
||||
key, err := DeriveRSAKey(password)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to derive rsa key: %v", err)
|
||||
}
|
||||
|
||||
publicKey, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal public key: %v", err)
|
||||
}
|
||||
|
||||
return x509.MarshalPKCS1PrivateKey(key), publicKey
|
||||
}
|
||||
|
||||
func testLoginProfile(username string) loginProfile {
|
||||
profile := loginProfile{
|
||||
Username: username,
|
||||
TexturesValue: strings.Repeat("texture-value-", 40),
|
||||
TexturesSignature: strings.Repeat("texture-signature-", 24),
|
||||
}
|
||||
digest := sha256.Sum256([]byte(username))
|
||||
copy(profile.UUID[:], digest[:16])
|
||||
profile.UUID[6] = (profile.UUID[6] & 0x0f) | 0x40
|
||||
profile.UUID[8] = (profile.UUID[8] & 0x3f) | 0x80
|
||||
return profile
|
||||
}
|
||||
|
||||
func TestHandshakeSuccess(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to listen: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
password := "super-secure-shared-key-12345"
|
||||
profiles := []loginProfile{testLoginProfile("test_user")}
|
||||
privateKey, publicKey := deriveTestRSAKey(t, password)
|
||||
|
||||
go func() {
|
||||
rawConn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer rawConn.Close()
|
||||
|
||||
server, err := wrapConnServer(rawConn, profiles, password, privateKey, publicKey)
|
||||
if err != nil {
|
||||
t.Errorf("failed to wrap server: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
n, err := server.Read(buf)
|
||||
if err != nil {
|
||||
t.Errorf("server read error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !bytes.Equal(buf[:n], []byte("hello server")) {
|
||||
t.Errorf("unexpected payload from client: %s", string(buf[:n]))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = server.Write([]byte("hello client"))
|
||||
if err != nil {
|
||||
t.Errorf("server write error: %v", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
clientRaw, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to dial: %v", err)
|
||||
}
|
||||
defer clientRaw.Close()
|
||||
|
||||
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create client: %v", err)
|
||||
}
|
||||
|
||||
_, err = client.Write([]byte("hello server"))
|
||||
if err != nil {
|
||||
t.Fatalf("client write error: %v", err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
n, err := client.Read(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("client read error: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(buf[:n], []byte("hello client")) {
|
||||
t.Errorf("unexpected payload from server: %s", string(buf[:n]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandshakePasswordMismatch(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to listen: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
clientPassword := "client-secret-123"
|
||||
serverPassword := "server-secret-456"
|
||||
profiles := []loginProfile{testLoginProfile("test_user")}
|
||||
serverPrivateKey, serverPublicKey := deriveTestRSAKey(t, serverPassword)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
rawConn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer rawConn.Close()
|
||||
|
||||
server, err := wrapConnServer(rawConn, profiles, serverPassword, serverPrivateKey, serverPublicKey)
|
||||
if err != nil {
|
||||
// Wrapping is synchronous and shouldn't fail initially simply because key derivation works with any string
|
||||
t.Logf("wrapped server: %v", err)
|
||||
}
|
||||
|
||||
// When client sends data, handshake happens and should fail
|
||||
buf := make([]byte, 1024)
|
||||
_, err = server.Read(buf)
|
||||
if err == nil {
|
||||
t.Errorf("expected handshake to fail due to password mismatch, but it succeeded")
|
||||
} else {
|
||||
t.Logf("server read failed as expected: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
clientRaw, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to dial: %v", err)
|
||||
}
|
||||
defer clientRaw.Close()
|
||||
|
||||
client, err := newClientConn(clientRaw, profiles, clientPassword, serverPublicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create client: %v", err)
|
||||
}
|
||||
|
||||
err = client.handshake()
|
||||
if err == nil {
|
||||
t.Fatal("expected client handshake to fail due to password mismatch")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Check if we lost connection or received error
|
||||
t.Log("Handshake mismatch tested")
|
||||
}
|
||||
|
||||
func TestHandshakeNetPipeWithKeepAlive(t *testing.T) {
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
const password = "net-pipe-shared-key"
|
||||
profiles := []loginProfile{testLoginProfile("pipe_user")}
|
||||
privateKey, publicKey := deriveTestRSAKey(t, password)
|
||||
serverDone := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
server, err := wrapConnServer(serverRaw, profiles, password, privateKey, publicKey)
|
||||
if err != nil {
|
||||
serverDone <- err
|
||||
return
|
||||
}
|
||||
|
||||
request := make([]byte, len("hello server"))
|
||||
if _, err = io.ReadFull(server, request); err != nil {
|
||||
serverDone <- fmt.Errorf("read request: %w", err)
|
||||
return
|
||||
}
|
||||
if string(request) != "hello server" {
|
||||
serverDone <- fmt.Errorf("unexpected request: %q", request)
|
||||
return
|
||||
}
|
||||
|
||||
followupDone := make(chan error, 1)
|
||||
go func() {
|
||||
followup := make([]byte, len("after keepalive"))
|
||||
_, readErr := io.ReadFull(server, followup)
|
||||
if readErr == nil && string(followup) != "after keepalive" {
|
||||
readErr = fmt.Errorf("unexpected followup: %q", followup)
|
||||
}
|
||||
followupDone <- readErr
|
||||
}()
|
||||
|
||||
if err = server.packet.writeKeepAlive(Long(42)); err != nil {
|
||||
serverDone <- fmt.Errorf("write keep-alive: %w", err)
|
||||
return
|
||||
}
|
||||
if _, err = server.Write([]byte("hello client")); err != nil {
|
||||
serverDone <- fmt.Errorf("write response: %w", err)
|
||||
return
|
||||
}
|
||||
serverDone <- <-followupDone
|
||||
}()
|
||||
|
||||
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = client.Write([]byte("hello server")); err != nil {
|
||||
t.Fatalf("write request: %v", err)
|
||||
}
|
||||
response := make([]byte, len("hello client"))
|
||||
if _, err = io.ReadFull(client, response); err != nil {
|
||||
t.Fatalf("read response: %v", err)
|
||||
}
|
||||
if string(response) != "hello client" {
|
||||
t.Fatalf("unexpected response: %q", response)
|
||||
}
|
||||
if _, err = client.Write([]byte("after keepalive")); err != nil {
|
||||
t.Fatalf("write followup: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case err = <-serverDone:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("net.Pipe handshake timed out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusQueryUnaffected(t *testing.T) {
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
const password = "status-shared-key"
|
||||
profiles := []loginProfile{testLoginProfile("status_user")}
|
||||
privateKey, publicKey := deriveTestRSAKey(t, password)
|
||||
serverDone := make(chan error, 1)
|
||||
go func() {
|
||||
server, err := wrapConnServer(serverRaw, profiles, password, privateKey, publicKey)
|
||||
if err == nil {
|
||||
err = server.handshake()
|
||||
}
|
||||
serverDone <- err
|
||||
}()
|
||||
|
||||
protocolVersion := Varint(775)
|
||||
serverAddress := String("localhost")
|
||||
serverPort := UnsignedShort(25565)
|
||||
nextState := Varint(1)
|
||||
if err := writePacket(clientRaw, 0x00, &protocolVersion, &serverAddress, &serverPort, &nextState); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writePacket(clientRaw, 0x00); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response, err := readPacket(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.packetID != 0x00 {
|
||||
t.Fatalf("status packet id = %d", response.packetID)
|
||||
}
|
||||
var responseJSON String
|
||||
if err = response.readFields(&responseJSON); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(responseJSON) != statusResponse {
|
||||
t.Fatalf("status response = %q", responseJSON)
|
||||
}
|
||||
|
||||
payload := Long(0x0102030405060708)
|
||||
if err = writePacket(clientRaw, 0x01, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pong, err := readPacket(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var receivedPayload Long
|
||||
if pong.packetID != 0x01 {
|
||||
t.Fatalf("pong packet id = %d", pong.packetID)
|
||||
}
|
||||
if err = pong.readFields(&receivedPayload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if receivedPayload != payload {
|
||||
t.Fatalf("pong payload = %x", receivedPayload)
|
||||
}
|
||||
|
||||
select {
|
||||
case err = <-serverDone:
|
||||
if err == nil || !strings.Contains(err.Error(), "ping") {
|
||||
t.Fatalf("server error = %v", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("status handshake timed out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientHandshakeHonorsCallerDeadline(t *testing.T) {
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
const password = "deadline-shared-key"
|
||||
profiles := []loginProfile{testLoginProfile("deadline_user")}
|
||||
_, publicKey := deriveTestRSAKey(t, password)
|
||||
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = client.SetDeadline(time.Now().Add(30 * time.Millisecond)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
started := time.Now()
|
||||
_, err = client.Write([]byte("blocked"))
|
||||
var netErr net.Error
|
||||
if !errors.As(err, &netErr) || !netErr.Timeout() {
|
||||
t.Fatalf("error = %v, want network timeout", err)
|
||||
}
|
||||
if elapsed := time.Since(started); elapsed > 500*time.Millisecond {
|
||||
t.Fatalf("caller deadline took %s", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientCloseInterruptsHandshake(t *testing.T) {
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer serverRaw.Close()
|
||||
|
||||
const password = "close-shared-key"
|
||||
profiles := []loginProfile{testLoginProfile("close_user")}
|
||||
_, publicKey := deriveTestRSAKey(t, password)
|
||||
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, writeErr := client.Write([]byte("blocked"))
|
||||
done <- writeErr
|
||||
}()
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
if err = client.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
select {
|
||||
case err = <-done:
|
||||
if err == nil {
|
||||
t.Fatal("handshake unexpectedly succeeded after close")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("close did not interrupt handshake")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLoginAcknowledgedPacketRejectsData(t *testing.T) {
|
||||
if err := validateLoginAcknowledgedPacket(&mcPacket{packetID: 0x03}); err != nil {
|
||||
t.Fatalf("valid login acknowledged packet: %v", err)
|
||||
}
|
||||
if err := validateLoginAcknowledgedPacket(&mcPacket{packetID: 0x03, data: []byte{0x00}}); err == nil {
|
||||
t.Fatal("login acknowledged packet with trailing data was accepted")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
configurationClientboundCustomPayload = 0x01
|
||||
configurationServerboundCustomPayload = 0x02
|
||||
configurationKeepAlive = 0x04
|
||||
|
||||
packetChannel = "xmc:data"
|
||||
maxPacketData = 24 * 1024
|
||||
keepAlivePeriod = 15 * time.Second
|
||||
)
|
||||
|
||||
// packetStream carries the raw proxy byte stream in Minecraft configuration
|
||||
// custom payload packets. The configuration state provides bidirectional
|
||||
// payload packets and keep-alives without requiring version-specific world data.
|
||||
type packetStream struct {
|
||||
reader io.Reader
|
||||
writer io.Writer
|
||||
isClient bool
|
||||
|
||||
readMu sync.Mutex
|
||||
writeMu sync.Mutex
|
||||
pending []byte
|
||||
|
||||
keepAliveID atomic.Int64
|
||||
done chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
func newPacketStream(reader io.Reader, writer io.Writer, isClient bool) *packetStream {
|
||||
s := &packetStream{
|
||||
reader: reader,
|
||||
writer: writer,
|
||||
isClient: isClient,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
if !isClient {
|
||||
go s.keepAliveLoop()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *packetStream) Read(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
s.readMu.Lock()
|
||||
defer s.readMu.Unlock()
|
||||
|
||||
if len(s.pending) > 0 {
|
||||
n := copy(p, s.pending)
|
||||
s.pending = s.pending[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
for {
|
||||
packet, err := readPacket(s.reader)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("read minecraft packet stream: %w", err)
|
||||
}
|
||||
|
||||
if packet.packetID == s.remoteCustomPayloadID() {
|
||||
payload, ok, err := parseCustomPayload(packet)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !ok || len(payload) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
n := copy(p, payload)
|
||||
if n < len(payload) {
|
||||
s.pending = append(s.pending[:0], payload[n:]...)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
if packet.packetID == configurationKeepAlive {
|
||||
var id Long
|
||||
if err := packet.readFields(&id); err != nil {
|
||||
return 0, fmt.Errorf("read minecraft keep-alive: %w", err)
|
||||
}
|
||||
if s.isClient {
|
||||
if err := s.writeKeepAlive(id); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *packetStream) Write(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
s.writeMu.Lock()
|
||||
defer s.writeMu.Unlock()
|
||||
|
||||
written := 0
|
||||
for written < len(p) {
|
||||
end := written + maxPacketData
|
||||
if end > len(p) {
|
||||
end = len(p)
|
||||
}
|
||||
channel := String(packetChannel)
|
||||
payload := RestBytes(p[written:end])
|
||||
if err := writePacket(s.writer, s.localCustomPayloadID(), &channel, &payload); err != nil {
|
||||
return written, fmt.Errorf("write minecraft custom payload: %w", err)
|
||||
}
|
||||
written = end
|
||||
}
|
||||
|
||||
return written, nil
|
||||
}
|
||||
|
||||
func (s *packetStream) Stop() {
|
||||
s.stopOnce.Do(func() { close(s.done) })
|
||||
}
|
||||
|
||||
func (s *packetStream) localCustomPayloadID() int {
|
||||
if s.isClient {
|
||||
return configurationServerboundCustomPayload
|
||||
}
|
||||
return configurationClientboundCustomPayload
|
||||
}
|
||||
|
||||
func (s *packetStream) remoteCustomPayloadID() int {
|
||||
if s.isClient {
|
||||
return configurationClientboundCustomPayload
|
||||
}
|
||||
return configurationServerboundCustomPayload
|
||||
}
|
||||
|
||||
func parseCustomPayload(packet *mcPacket) ([]byte, bool, error) {
|
||||
r := bytes.NewReader(packet.data)
|
||||
var channel String
|
||||
if err := channel.readFrom(r); err != nil {
|
||||
return nil, false, fmt.Errorf("read minecraft custom payload channel: %w", err)
|
||||
}
|
||||
if string(channel) != packetChannel {
|
||||
return nil, false, nil
|
||||
}
|
||||
payload := make([]byte, r.Len())
|
||||
if _, err := io.ReadFull(r, payload); err != nil {
|
||||
return nil, false, fmt.Errorf("read minecraft custom payload data: %w", err)
|
||||
}
|
||||
return payload, true, nil
|
||||
}
|
||||
|
||||
func (s *packetStream) writeKeepAlive(id Long) error {
|
||||
s.writeMu.Lock()
|
||||
defer s.writeMu.Unlock()
|
||||
if err := writePacket(s.writer, configurationKeepAlive, &id); err != nil {
|
||||
return fmt.Errorf("write minecraft keep-alive: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *packetStream) keepAliveLoop() {
|
||||
ticker := time.NewTicker(keepAlivePeriod)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
id := Long(s.keepAliveID.Add(1))
|
||||
if err := s.writeKeepAlive(id); err != nil {
|
||||
return
|
||||
}
|
||||
case <-s.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPacketStreamUsesPlainFraming(t *testing.T) {
|
||||
payload := []byte("hello")
|
||||
var wire bytes.Buffer
|
||||
stream := newPacketStream(bytes.NewReader(nil), &wire, true)
|
||||
|
||||
written, err := stream.Write(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("write payload: %v", err)
|
||||
}
|
||||
if written != len(payload) {
|
||||
t.Fatalf("written = %d, want %d", written, len(payload))
|
||||
}
|
||||
wantOutbound := []byte{0x0f, 0x02, 0x08, 'x', 'm', 'c', ':', 'd', 'a', 't', 'a', 'h', 'e', 'l', 'l', 'o'}
|
||||
if !bytes.Equal(wire.Bytes(), wantOutbound) {
|
||||
t.Fatalf("wire frame = %x, want %x", wire.Bytes(), wantOutbound)
|
||||
}
|
||||
|
||||
wantInbound := append([]byte(nil), wantOutbound...)
|
||||
wantInbound[1] = configurationClientboundCustomPayload
|
||||
reader := newPacketStream(bytes.NewReader(wantInbound), io.Discard, true)
|
||||
got := make([]byte, len(payload))
|
||||
if _, err = io.ReadFull(reader, got); err != nil {
|
||||
t.Fatalf("read payload: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, payload) {
|
||||
t.Fatalf("payload = %q, want %q", got, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketStreamRoundTrip(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
const password = "packet-stream-shared-key"
|
||||
privateKey, publicKey := deriveTestRSAKey(t, password)
|
||||
profiles := []loginProfile{testLoginProfile("packet_user")}
|
||||
clientPayload := bytes.Repeat([]byte("client-payload-"), 5000)
|
||||
serverPayload := bytes.Repeat([]byte("server-payload-"), 5000)
|
||||
serverDone := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
rawConn, acceptErr := ln.Accept()
|
||||
if acceptErr != nil {
|
||||
serverDone <- acceptErr
|
||||
return
|
||||
}
|
||||
defer rawConn.Close()
|
||||
|
||||
server, wrapErr := wrapConnServer(rawConn, profiles, password, privateKey, publicKey)
|
||||
if wrapErr != nil {
|
||||
serverDone <- wrapErr
|
||||
return
|
||||
}
|
||||
got := make([]byte, len(clientPayload))
|
||||
if _, readErr := io.ReadFull(server, got); readErr != nil {
|
||||
serverDone <- readErr
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(got, clientPayload) {
|
||||
serverDone <- io.ErrUnexpectedEOF
|
||||
return
|
||||
}
|
||||
_, writeErr := server.Write(serverPayload)
|
||||
serverDone <- writeErr
|
||||
}()
|
||||
|
||||
rawClient, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rawClient.Close()
|
||||
|
||||
client, err := newClientConn(rawClient, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = client.Write(clientPayload); err != nil {
|
||||
t.Fatalf("write payload: %v", err)
|
||||
}
|
||||
got := make([]byte, len(serverPayload))
|
||||
if _, err = io.ReadFull(client, got); err != nil {
|
||||
t.Fatalf("read payload: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, serverPayload) {
|
||||
t.Fatal("server payload mismatch")
|
||||
}
|
||||
if err = <-serverDone; err != nil {
|
||||
t.Fatalf("server: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"time"
|
||||
)
|
||||
|
||||
type paddingDirection uint8
|
||||
|
||||
const (
|
||||
paddingClientToServer paddingDirection = iota + 1
|
||||
paddingServerToClient
|
||||
|
||||
paddingBufferLength = 16 * 1024
|
||||
maxPaddingChunkLength = 48 * 1024
|
||||
maxPaddingTurnLength = 8 * 1024 * 1024
|
||||
)
|
||||
|
||||
type paddingVariant struct {
|
||||
chunks []int
|
||||
delays []paddingDelayRange
|
||||
}
|
||||
|
||||
type paddingDelayRange struct {
|
||||
min time.Duration
|
||||
max time.Duration
|
||||
}
|
||||
|
||||
type paddingTurn struct {
|
||||
direction paddingDirection
|
||||
minLength int
|
||||
maxLength int
|
||||
variants []paddingVariant
|
||||
startDelay paddingDelayRange
|
||||
chunkDelay paddingDelayRange
|
||||
writeChunkMinLength int
|
||||
writeChunkLength int
|
||||
sendMinLength int
|
||||
sendMaxLength int
|
||||
sendVariants []int
|
||||
}
|
||||
|
||||
func runPaddingSchedule(reader io.Reader, writer io.Writer, isClient bool, firstTurnPrefixLength int, schedule []paddingTurn) error {
|
||||
if err := validatePaddingSchedule(schedule, firstTurnPrefixLength); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var writeBuffer []byte
|
||||
for i, turn := range schedule {
|
||||
prefixLength := 0
|
||||
if i == 0 {
|
||||
prefixLength = firstTurnPrefixLength
|
||||
}
|
||||
|
||||
localSends := isClient == (turn.direction == paddingClientToServer)
|
||||
if localSends {
|
||||
if err := writePaddingTurnWithBuffer(writer, turn, prefixLength, time.Sleep, &writeBuffer); err != nil {
|
||||
return fmt.Errorf("write padding turn %d: %w", i, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := readPaddingTurn(reader, turn, prefixLength); err != nil {
|
||||
return fmt.Errorf("read padding turn %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePaddingSchedule(schedule []paddingTurn, firstTurnPrefixLength int) error {
|
||||
if len(schedule) == 0 {
|
||||
return fmt.Errorf("empty padding schedule")
|
||||
}
|
||||
if firstTurnPrefixLength < 0 {
|
||||
return fmt.Errorf("negative first turn prefix length: %d", firstTurnPrefixLength)
|
||||
}
|
||||
if firstTurnPrefixLength > 0 && schedule[0].direction != paddingClientToServer {
|
||||
return fmt.Errorf("first prefixed padding turn is not client-to-server")
|
||||
}
|
||||
|
||||
for i, turn := range schedule {
|
||||
if turn.direction != paddingClientToServer && turn.direction != paddingServerToClient {
|
||||
return fmt.Errorf("padding turn %d has invalid direction: %d", i, turn.direction)
|
||||
}
|
||||
if err := validatePaddingDelayRange(turn.startDelay); err != nil {
|
||||
return fmt.Errorf("padding turn %d has an invalid start delay: %w", i, err)
|
||||
}
|
||||
if err := validatePaddingDelayRange(turn.chunkDelay); err != nil {
|
||||
return fmt.Errorf("padding turn %d has an invalid chunk delay: %w", i, err)
|
||||
}
|
||||
if turn.writeChunkMinLength < 0 || turn.writeChunkLength < turn.writeChunkMinLength || turn.writeChunkLength > maxPaddingChunkLength {
|
||||
return fmt.Errorf("padding turn %d has an invalid write chunk range: %d-%d", i, turn.writeChunkMinLength, turn.writeChunkLength)
|
||||
}
|
||||
if len(turn.variants) > 0 && turn.writeChunkLength != 0 {
|
||||
return fmt.Errorf("padding turn %d combines variants with generated write chunks", i)
|
||||
}
|
||||
|
||||
minLength, maxLength, err := paddingTurnBounds(turn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("padding turn %d: %w", i, err)
|
||||
}
|
||||
hasSendRange := turn.sendMinLength != 0 || turn.sendMaxLength != 0
|
||||
if hasSendRange {
|
||||
if len(turn.variants) > 0 {
|
||||
return fmt.Errorf("padding turn %d combines variants with a send range", i)
|
||||
}
|
||||
if turn.sendMinLength < minLength || turn.sendMaxLength < turn.sendMinLength || turn.sendMaxLength > maxLength {
|
||||
return fmt.Errorf("padding turn %d has an invalid send range: %d-%d", i, turn.sendMinLength, turn.sendMaxLength)
|
||||
}
|
||||
}
|
||||
if i == 0 && minLength-firstTurnPrefixLength < 1 {
|
||||
return fmt.Errorf("padding turn 0 is too short for %d prefix bytes", firstTurnPrefixLength)
|
||||
}
|
||||
if i == 0 && len(turn.variants) > 0 {
|
||||
for j, variant := range turn.variants {
|
||||
if _, _, err = trimPaddingPrefix(variant, firstTurnPrefixLength); err != nil {
|
||||
return fmt.Errorf("padding turn 0 variant %d: %w", j, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if i > 0 && turn.direction == schedule[i-1].direction {
|
||||
return fmt.Errorf("padding turns %d and %d have the same direction", i-1, i)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writePaddingTurn(w io.Writer, turn paddingTurn, prefixLength int) error {
|
||||
return writePaddingTurnWithSleep(w, turn, prefixLength, time.Sleep)
|
||||
}
|
||||
|
||||
func writePaddingTurnWithSleep(w io.Writer, turn paddingTurn, prefixLength int, sleep func(time.Duration)) error {
|
||||
return writePaddingTurnWithBuffer(w, turn, prefixLength, sleep, nil)
|
||||
}
|
||||
|
||||
func writePaddingTurnWithBuffer(w io.Writer, turn paddingTurn, prefixLength int, sleep func(time.Duration), reusableBuffer *[]byte) error {
|
||||
startDelay, err := randomPaddingDelay(turn.startDelay)
|
||||
if err != nil {
|
||||
return fmt.Errorf("select padding start delay: %w", err)
|
||||
}
|
||||
if startDelay > 0 {
|
||||
sleep(startDelay)
|
||||
}
|
||||
|
||||
targetLength, chunks, delays, err := selectPaddingVariant(turn, prefixLength)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recordLength := targetLength - prefixLength
|
||||
if recordLength < 1 {
|
||||
return fmt.Errorf("target length %d leaves an invalid record length %d", targetLength, recordLength)
|
||||
}
|
||||
|
||||
encodedLength := Varint(recordLength)
|
||||
var header bytes.Buffer
|
||||
if err = encodedLength.writeTo(&header); err != nil {
|
||||
return fmt.Errorf("write padding header: %w", err)
|
||||
}
|
||||
if len(chunks) == 0 {
|
||||
writeChunkLength := turn.writeChunkLength
|
||||
if writeChunkLength == 0 {
|
||||
writeChunkLength = paddingBufferLength
|
||||
} else if turn.writeChunkMinLength > 0 {
|
||||
writeChunkLength, err = randomPaddingTarget(turn.writeChunkMinLength, writeChunkLength)
|
||||
if err != nil {
|
||||
return fmt.Errorf("select padding write chunk length: %w", err)
|
||||
}
|
||||
}
|
||||
chunks = defaultPaddingChunks(recordLength, writeChunkLength)
|
||||
delays = make([]paddingDelayRange, len(chunks))
|
||||
for i := 1; i < len(delays); i++ {
|
||||
delays[i] = turn.chunkDelay
|
||||
}
|
||||
}
|
||||
if chunks[0] < header.Len() {
|
||||
return fmt.Errorf("first padding chunk %d is shorter than header %d", chunks[0], header.Len())
|
||||
}
|
||||
|
||||
maxChunkLength := 0
|
||||
for _, chunkLength := range chunks {
|
||||
if chunkLength < 1 || chunkLength > maxPaddingChunkLength {
|
||||
return fmt.Errorf("invalid padding chunk length: %d", chunkLength)
|
||||
}
|
||||
maxChunkLength = max(maxChunkLength, chunkLength)
|
||||
}
|
||||
var buffer []byte
|
||||
if reusableBuffer == nil {
|
||||
buffer = make([]byte, maxChunkLength)
|
||||
} else {
|
||||
if cap(*reusableBuffer) < maxChunkLength {
|
||||
*reusableBuffer = make([]byte, maxChunkLength)
|
||||
}
|
||||
buffer = (*reusableBuffer)[:maxChunkLength]
|
||||
clear(buffer)
|
||||
}
|
||||
copy(buffer, header.Bytes())
|
||||
written := 0
|
||||
for i, chunkLength := range chunks {
|
||||
if i < len(delays) {
|
||||
delay, delayErr := randomPaddingDelay(delays[i])
|
||||
if delayErr != nil {
|
||||
return fmt.Errorf("select padding chunk %d delay: %w", i, delayErr)
|
||||
}
|
||||
if delay > 0 {
|
||||
sleep(delay)
|
||||
}
|
||||
}
|
||||
if err = writeFull(w, buffer[:chunkLength]); err != nil {
|
||||
return fmt.Errorf("write padding chunk %d: %w", i, err)
|
||||
}
|
||||
written += chunkLength
|
||||
if i == 0 {
|
||||
clear(buffer[:header.Len()])
|
||||
}
|
||||
}
|
||||
if written != recordLength {
|
||||
return fmt.Errorf("padding chunks total %d, want %d", written, recordLength)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readPaddingTurn(r io.Reader, turn paddingTurn, prefixLength int) error {
|
||||
encodedLength, headerLength, err := readVarintWithLength(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read padding header: %w", err)
|
||||
}
|
||||
recordLength := int(encodedLength)
|
||||
if recordLength < headerLength || recordLength > maxPaddingTurnLength {
|
||||
return fmt.Errorf("invalid padding record length: %d", recordLength)
|
||||
}
|
||||
totalLength := prefixLength + recordLength
|
||||
if !paddingTurnAcceptsLength(turn, totalLength) {
|
||||
if len(turn.variants) > 0 {
|
||||
return fmt.Errorf("padding turn length %d is not an allowed variant", totalLength)
|
||||
}
|
||||
return fmt.Errorf("padding turn length %d is outside %d-%d", totalLength, turn.minLength, turn.maxLength)
|
||||
}
|
||||
|
||||
var buffer [paddingBufferLength]byte
|
||||
remaining := recordLength - headerLength
|
||||
for remaining > 0 {
|
||||
chunkLength := min(remaining, len(buffer))
|
||||
if _, err := io.ReadFull(r, buffer[:chunkLength]); err != nil {
|
||||
return fmt.Errorf("read padding body: %w", err)
|
||||
}
|
||||
remaining -= chunkLength
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func selectPaddingVariant(turn paddingTurn, prefixLength int) (int, []int, []paddingDelayRange, error) {
|
||||
if len(turn.variants) == 0 {
|
||||
minimum, maximum := turn.minLength, turn.maxLength
|
||||
if turn.sendMinLength != 0 || turn.sendMaxLength != 0 {
|
||||
minimum, maximum = turn.sendMinLength, turn.sendMaxLength
|
||||
}
|
||||
targetLength, err := randomPaddingTarget(minimum, maximum)
|
||||
return targetLength, nil, nil, err
|
||||
}
|
||||
|
||||
indices := turn.sendVariants
|
||||
if len(indices) == 0 {
|
||||
indices = make([]int, len(turn.variants))
|
||||
for i := range indices {
|
||||
indices[i] = i
|
||||
}
|
||||
}
|
||||
selected, err := randomPaddingIndex(len(indices))
|
||||
if err != nil {
|
||||
return 0, nil, nil, err
|
||||
}
|
||||
variantIndex := indices[selected]
|
||||
if variantIndex < 0 || variantIndex >= len(turn.variants) {
|
||||
return 0, nil, nil, fmt.Errorf("invalid send variant index: %d", variantIndex)
|
||||
}
|
||||
variant := turn.variants[variantIndex]
|
||||
targetLength := paddingVariantLength(variant)
|
||||
chunks, delays, err := trimPaddingPrefix(variant, prefixLength)
|
||||
if err != nil {
|
||||
return 0, nil, nil, err
|
||||
}
|
||||
return targetLength, chunks, delays, nil
|
||||
}
|
||||
|
||||
func trimPaddingPrefix(variant paddingVariant, prefixLength int) ([]int, []paddingDelayRange, error) {
|
||||
remainingPrefix := prefixLength
|
||||
firstChunk := 0
|
||||
for firstChunk < len(variant.chunks) && remainingPrefix > 0 {
|
||||
chunkLength := variant.chunks[firstChunk]
|
||||
if remainingPrefix < chunkLength {
|
||||
return nil, nil, fmt.Errorf("prefix length %d splits chunk %d", prefixLength, firstChunk)
|
||||
}
|
||||
remainingPrefix -= chunkLength
|
||||
firstChunk++
|
||||
}
|
||||
if remainingPrefix != 0 || firstChunk == len(variant.chunks) {
|
||||
return nil, nil, fmt.Errorf("prefix length %d leaves no padding record", prefixLength)
|
||||
}
|
||||
|
||||
chunks := append([]int(nil), variant.chunks[firstChunk:]...)
|
||||
delays := make([]paddingDelayRange, len(chunks))
|
||||
if len(variant.delays) > 0 {
|
||||
copy(delays, variant.delays[firstChunk:])
|
||||
}
|
||||
return chunks, delays, nil
|
||||
}
|
||||
|
||||
func defaultPaddingChunks(recordLength, writeChunkLength int) []int {
|
||||
chunks := make([]int, 0, (recordLength+writeChunkLength-1)/writeChunkLength)
|
||||
for remaining := recordLength; remaining > 0; {
|
||||
chunkLength := min(remaining, writeChunkLength)
|
||||
chunks = append(chunks, chunkLength)
|
||||
remaining -= chunkLength
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
func paddingTurnBounds(turn paddingTurn) (int, int, error) {
|
||||
if len(turn.variants) == 0 {
|
||||
if turn.minLength < 1 || turn.maxLength < turn.minLength || turn.maxLength > maxPaddingTurnLength {
|
||||
return 0, 0, fmt.Errorf("invalid range: %d-%d", turn.minLength, turn.maxLength)
|
||||
}
|
||||
return turn.minLength, turn.maxLength, nil
|
||||
}
|
||||
if turn.minLength != 0 || turn.maxLength != 0 {
|
||||
return 0, 0, fmt.Errorf("variants cannot be combined with a length range")
|
||||
}
|
||||
|
||||
minLength := maxPaddingTurnLength + 1
|
||||
maxLength := 0
|
||||
for i, variant := range turn.variants {
|
||||
if len(variant.chunks) == 0 {
|
||||
return 0, 0, fmt.Errorf("variant %d has no chunks", i)
|
||||
}
|
||||
if len(variant.delays) != 0 && len(variant.delays) != len(variant.chunks) {
|
||||
return 0, 0, fmt.Errorf("variant %d has %d chunks and %d delays", i, len(variant.chunks), len(variant.delays))
|
||||
}
|
||||
for j, chunkLength := range variant.chunks {
|
||||
if chunkLength < 1 || chunkLength > maxPaddingChunkLength {
|
||||
return 0, 0, fmt.Errorf("variant %d chunk %d has invalid length: %d", i, j, chunkLength)
|
||||
}
|
||||
if len(variant.delays) > 0 {
|
||||
if err := validatePaddingDelayRange(variant.delays[j]); err != nil {
|
||||
return 0, 0, fmt.Errorf("variant %d chunk %d has an invalid delay: %w", i, j, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
length := paddingVariantLength(variant)
|
||||
if length > maxPaddingTurnLength {
|
||||
return 0, 0, fmt.Errorf("variant %d is too long: %d", i, length)
|
||||
}
|
||||
minLength = min(minLength, length)
|
||||
maxLength = max(maxLength, length)
|
||||
}
|
||||
for _, index := range turn.sendVariants {
|
||||
if index < 0 || index >= len(turn.variants) {
|
||||
return 0, 0, fmt.Errorf("invalid send variant index: %d", index)
|
||||
}
|
||||
}
|
||||
return minLength, maxLength, nil
|
||||
}
|
||||
|
||||
func paddingTurnAcceptsLength(turn paddingTurn, length int) bool {
|
||||
if len(turn.variants) == 0 {
|
||||
return length >= turn.minLength && length <= turn.maxLength
|
||||
}
|
||||
for _, variant := range turn.variants {
|
||||
if paddingVariantLength(variant) == length {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func paddingVariantLength(variant paddingVariant) int {
|
||||
total := 0
|
||||
for _, chunkLength := range variant.chunks {
|
||||
total += chunkLength
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func validatePaddingDelayRange(delay paddingDelayRange) error {
|
||||
if delay.min < 0 || delay.max < delay.min {
|
||||
return fmt.Errorf("invalid range: %s-%s", delay.min, delay.max)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func randomPaddingDelay(delay paddingDelayRange) (time.Duration, error) {
|
||||
if err := validatePaddingDelayRange(delay); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if delay.min == delay.max {
|
||||
return delay.min, nil
|
||||
}
|
||||
span := int64(delay.max-delay.min) + 1
|
||||
offset, err := rand.Int(rand.Reader, big.NewInt(span))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("select padding delay: %w", err)
|
||||
}
|
||||
return delay.min + time.Duration(offset.Int64()), nil
|
||||
}
|
||||
|
||||
func randomPaddingIndex(length int) (int, error) {
|
||||
if length < 1 {
|
||||
return 0, fmt.Errorf("select from empty padding choices")
|
||||
}
|
||||
if length == 1 {
|
||||
return 0, nil
|
||||
}
|
||||
index, err := rand.Int(rand.Reader, big.NewInt(int64(length)))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("select padding choice: %w", err)
|
||||
}
|
||||
return int(index.Int64()), nil
|
||||
}
|
||||
|
||||
func randomPaddingTarget(minLength, maxLength int) (int, error) {
|
||||
if minLength == maxLength {
|
||||
return minLength, nil
|
||||
}
|
||||
span := int64(maxLength-minLength) + 1
|
||||
offset, err := rand.Int(rand.Reader, big.NewInt(span))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("select padding length: %w", err)
|
||||
}
|
||||
return minLength + int(offset.Int64()), nil
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Length and write-boundary templates come from controlled Minecraft 26.1.2
|
||||
// logins. Timing deliberately uses broad random bands that preserve only the
|
||||
// rough ordering of short and long phases; it does not replay captured delays.
|
||||
var startupPaddingSchedule2612 = []paddingTurn{
|
||||
{
|
||||
direction: paddingClientToServer,
|
||||
variants: []paddingVariant{
|
||||
paddingVariantFromChunks(2, 26, 16),
|
||||
},
|
||||
},
|
||||
{
|
||||
direction: paddingServerToClient,
|
||||
variants: []paddingVariant{
|
||||
paddingVariantFromChunks(26, 21, 25),
|
||||
},
|
||||
startDelay: millisecondRange(0, 20),
|
||||
},
|
||||
{
|
||||
direction: paddingClientToServer,
|
||||
variants: []paddingVariant{
|
||||
paddingVariantFromChunks(25),
|
||||
},
|
||||
startDelay: millisecondRange(2, 22),
|
||||
},
|
||||
{
|
||||
direction: paddingServerToClient,
|
||||
variants: []paddingVariant{
|
||||
registryPaddingVariant(),
|
||||
},
|
||||
startDelay: millisecondRange(20, 50),
|
||||
},
|
||||
{
|
||||
direction: paddingClientToServer,
|
||||
variants: []paddingVariant{
|
||||
paddingVariantFromChunks(2),
|
||||
},
|
||||
startDelay: millisecondRange(10, 35),
|
||||
},
|
||||
{
|
||||
direction: paddingServerToClient,
|
||||
variants: []paddingVariant{
|
||||
playStartPaddingVariant(4941, 252, 259, 267, 268, 251, 303, 259, 264, 54, 346),
|
||||
playStartPaddingVariant(4941, 262, 284, 272, 260, 260, 313, 264, 151, 224, 207, 215, 224, 390),
|
||||
playStartPaddingVariant(4941, 257, 272, 275, 260, 260, 313, 283, 274, 226, 207, 230, 215, 204, 221, 352),
|
||||
playStartPaddingVariant(4941, 259, 272, 288, 260, 260, 311, 270, 70, 236, 223, 201, 210, 352),
|
||||
playStartPaddingVariant(4941, 255, 269, 277, 263, 260, 136, 207, 210, 232, 325),
|
||||
playStartPaddingVariant(4941, 259, 270, 274, 263, 258, 327, 170, 210, 375),
|
||||
playStartPaddingVariant(4941, 257, 275, 291, 260, 260, 325, 269, 70, 230, 226, 207, 221, 352),
|
||||
playStartPaddingVariant(4941, 252, 273, 262, 252, 254, 306, 93),
|
||||
playStartPaddingVariant(4941, 273, 270, 269, 258, 256, 322, 221, 207, 215, 438),
|
||||
playStartPaddingVariant(4941, 259, 275, 274, 250, 258, 308, 267, 154, 233, 209, 207, 213, 393),
|
||||
playStartPaddingVariant(4941, 254, 267, 272, 260, 253, 311, 167, 204, 232, 207, 481, 8),
|
||||
playStartPaddingVariant(4941, 259, 269, 272, 261, 313, 207, 213, 500, 19),
|
||||
playStartPaddingVariant(4941, 262, 269, 274, 263, 274, 311, 270, 242, 210, 229, 221, 210, 431),
|
||||
playStartPaddingVariant(4941, 259, 265, 277, 263, 277, 316, 269, 156, 204, 210, 226, 207, 413),
|
||||
playStartPaddingVariant(4941, 215, 251, 249, 317, 260, 270, 249, 52),
|
||||
playStartPaddingVariant(4941, 224, 263, 277, 316, 267, 272, 260, 138, 230, 226, 207, 204, 352),
|
||||
playStartPaddingVariant(4941, 221, 258, 263, 319, 269, 288, 263, 136, 204, 210, 220, 207, 378),
|
||||
playStartPaddingVariant(4941, 221, 258, 260, 316, 273, 291, 226, 204, 229, 213, 489, 8),
|
||||
playStartPaddingVariant(4941, 238, 260, 261, 306, 272, 277, 260, 224, 241, 212, 207, 204, 393),
|
||||
playStartPaddingVariant(4941, 224, 260, 260, 309, 272, 277, 277, 138, 207, 207, 212, 241, 352),
|
||||
},
|
||||
startDelay: millisecondRange(35, 50),
|
||||
},
|
||||
}
|
||||
|
||||
// These turns cover the finite Play-state tail through the client's
|
||||
// player_loaded packet. Bounds are the observed per-turn minima and maxima
|
||||
// across 20 controlled 26.1.2 logins; payload bytes remain opaque padding.
|
||||
var playJoinPaddingSchedule2612 = []paddingTurn{
|
||||
clientPlayPaddingTurn(6, 883),
|
||||
serverPlayPaddingTurn(346, 58638),
|
||||
clientPlayPaddingTurn(6, 887),
|
||||
serverPlayPaddingTurn(388, 61077),
|
||||
clientPlayPaddingTurn(2, 50),
|
||||
serverPlayPaddingTurn(575, 65584),
|
||||
clientPlayPaddingTurn(6, 45),
|
||||
serverPlayPaddingTurn(86, 63563),
|
||||
clientPlayPaddingTurn(2, 44),
|
||||
serverPlayPaddingTurn(42, 51983),
|
||||
clientPlayPaddingTurn(2, 851),
|
||||
serverPlayPaddingTurn(309, 25083),
|
||||
clientPlayPaddingTurn(2, 19),
|
||||
serverPlayPaddingTurn(74, 63885),
|
||||
clientPlayPaddingTurn(8, 24),
|
||||
serverPlayPaddingTurn(30, 66128),
|
||||
clientPlayPaddingTurn(2, 19),
|
||||
serverPlayPaddingTurn(26, 35818),
|
||||
clientPlayPaddingTurn(6, 19),
|
||||
serverPlayPaddingTurn(35, 59407),
|
||||
clientPlayPaddingTurn(6, 19),
|
||||
serverPlayPaddingTurn(37, 65328),
|
||||
clientPlayPaddingTurn(2, 19),
|
||||
serverPlayPaddingTurn(26, 60622),
|
||||
clientPlayPaddingTurn(6, 19),
|
||||
serverPlayPaddingTurn(11, 60808),
|
||||
clientPlayPaddingTurn(8, 43),
|
||||
serverPlayPaddingTurn(55, 62027),
|
||||
clientPlayPaddingTurn(2, 19),
|
||||
serverPlayPaddingTurn(427, 65622),
|
||||
clientPlayPaddingTurn(5, 19),
|
||||
serverPlayPaddingTurn(35, 59401),
|
||||
clientPlayPaddingTurn(6, 19),
|
||||
}
|
||||
|
||||
type paddingLengthRange2612 struct {
|
||||
minimum int
|
||||
maximum int
|
||||
}
|
||||
|
||||
type serverPlayLengthBranches2612 struct {
|
||||
small paddingLengthRange2612
|
||||
large paddingLengthRange2612
|
||||
}
|
||||
|
||||
var serverPlayBranches2612 = []serverPlayLengthBranches2612{
|
||||
{small: paddingLengthRange2612{346, 18812}, large: paddingLengthRange2612{51702, 58638}},
|
||||
{small: paddingLengthRange2612{388, 20689}, large: paddingLengthRange2612{51445, 61077}},
|
||||
{small: paddingLengthRange2612{575, 20915}, large: paddingLengthRange2612{41428, 65584}},
|
||||
{small: paddingLengthRange2612{86, 2772}, large: paddingLengthRange2612{41428, 63563}},
|
||||
{small: paddingLengthRange2612{42, 26813}, large: paddingLengthRange2612{51983, 51983}},
|
||||
{small: paddingLengthRange2612{309, 19484}, large: paddingLengthRange2612{24837, 25083}},
|
||||
{small: paddingLengthRange2612{74, 40686}, large: paddingLengthRange2612{63885, 63885}},
|
||||
{small: paddingLengthRange2612{30, 44114}, large: paddingLengthRange2612{66128, 66128}},
|
||||
{small: paddingLengthRange2612{26, 1464}, large: paddingLengthRange2612{9941, 35818}},
|
||||
{small: paddingLengthRange2612{35, 42885}, large: paddingLengthRange2612{52194, 59407}},
|
||||
{small: paddingLengthRange2612{37, 47553}, large: paddingLengthRange2612{61765, 65328}},
|
||||
{small: paddingLengthRange2612{26, 1121}, large: paddingLengthRange2612{16162, 60622}},
|
||||
{small: paddingLengthRange2612{11, 45629}, large: paddingLengthRange2612{60808, 60808}},
|
||||
{small: paddingLengthRange2612{55, 10035}, large: paddingLengthRange2612{30237, 62027}},
|
||||
{small: paddingLengthRange2612{427, 52536}, large: paddingLengthRange2612{64014, 65622}},
|
||||
{small: paddingLengthRange2612{35, 22708}, large: paddingLengthRange2612{38987, 59401}},
|
||||
}
|
||||
|
||||
// Each mask preserves only the small/large branch order from one baseline
|
||||
// login. Actual lengths and timing are selected randomly inside each branch.
|
||||
var serverPlayBranchMasks2612 = []uint32{
|
||||
0x011c, 0x090a, 0x0821, 0xe921, 0x2102,
|
||||
0x0844, 0xa101, 0x1106, 0x2e00, 0xab01,
|
||||
0xe900, 0xac01, 0xab01, 0x8b80, 0x0808,
|
||||
0x2001, 0x0901, 0x000a, 0x2c01, 0x0801,
|
||||
}
|
||||
|
||||
type clientPlayBurst2612 struct {
|
||||
playIndex int
|
||||
regular paddingLengthRange2612
|
||||
burst paddingLengthRange2612
|
||||
}
|
||||
|
||||
var clientPlayBursts2612 = []clientPlayBurst2612{
|
||||
{playIndex: 0, regular: paddingLengthRange2612{6, 44}, burst: paddingLengthRange2612{877, 883}},
|
||||
{playIndex: 2, regular: paddingLengthRange2612{6, 45}, burst: paddingLengthRange2612{884, 887}},
|
||||
{playIndex: 10, regular: paddingLengthRange2612{2, 19}, burst: paddingLengthRange2612{851, 851}},
|
||||
}
|
||||
|
||||
// The 20 samples placed the one client initialization burst in these slots.
|
||||
var clientPlayBurstChoices2612 = []int{
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 1,
|
||||
2,
|
||||
}
|
||||
|
||||
var paddingSchedule2612 = buildPaddingSchedule2612()
|
||||
|
||||
func buildPaddingSchedule2612() []paddingTurn {
|
||||
schedule := make([]paddingTurn, 0, len(startupPaddingSchedule2612)+len(playJoinPaddingSchedule2612))
|
||||
schedule = append(schedule, startupPaddingSchedule2612...)
|
||||
schedule = append(schedule, playJoinPaddingSchedule2612...)
|
||||
return schedule
|
||||
}
|
||||
|
||||
func clientPlayPaddingTurn(minimum, maximum int) paddingTurn {
|
||||
return paddingTurn{
|
||||
direction: paddingClientToServer,
|
||||
minLength: minimum,
|
||||
maxLength: maximum,
|
||||
startDelay: millisecondRange(1, 30),
|
||||
writeChunkLength: 1024,
|
||||
}
|
||||
}
|
||||
|
||||
func serverPlayPaddingTurn(minimum, maximum int) paddingTurn {
|
||||
return paddingTurn{
|
||||
direction: paddingServerToClient,
|
||||
minLength: minimum,
|
||||
maxLength: maximum,
|
||||
startDelay: millisecondRange(1, 45),
|
||||
chunkDelay: millisecondRange(1, 4),
|
||||
writeChunkMinLength: 32 * 1024,
|
||||
writeChunkLength: maxPaddingChunkLength,
|
||||
}
|
||||
}
|
||||
|
||||
func newClientPaddingSchedule2612() ([]paddingTurn, error) {
|
||||
choice, err := randomPaddingIndex(len(clientPlayBurstChoices2612))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selectedBurst := clientPlayBurstChoices2612[choice]
|
||||
schedule := append([]paddingTurn(nil), paddingSchedule2612...)
|
||||
for i, burst := range clientPlayBursts2612 {
|
||||
lengthRange := burst.regular
|
||||
if i == selectedBurst {
|
||||
lengthRange = burst.burst
|
||||
}
|
||||
turn := &schedule[len(startupPaddingSchedule2612)+burst.playIndex]
|
||||
turn.sendMinLength = lengthRange.minimum
|
||||
turn.sendMaxLength = lengthRange.maximum
|
||||
}
|
||||
return schedule, nil
|
||||
}
|
||||
|
||||
type paddingPause struct {
|
||||
chunk int
|
||||
delay paddingDelayRange
|
||||
}
|
||||
|
||||
func paddingVariantFromChunks(chunks ...int) paddingVariant {
|
||||
return paddingVariant{chunks: chunks}
|
||||
}
|
||||
|
||||
func pacedPaddingVariant(chunks []int, pauses ...paddingPause) paddingVariant {
|
||||
delays := make([]paddingDelayRange, len(chunks))
|
||||
for _, pause := range pauses {
|
||||
if pause.chunk < 0 || pause.chunk >= len(delays) {
|
||||
panic("xmc: padding pause index is outside its chunk template")
|
||||
}
|
||||
delays[pause.chunk] = pause.delay
|
||||
}
|
||||
return paddingVariant{chunks: chunks, delays: delays}
|
||||
}
|
||||
|
||||
func registryPaddingVariant() paddingVariant {
|
||||
return pacedPaddingVariant(
|
||||
[]int{1590, 226, 329, 229, 186, 151, 78, 81, 79, 235, 67, 67, 78, 71, 82, 74, 982, 117, 1118, 1038, 970, 400, 239, 49, 50, 95, 65, 104, 32320, 2},
|
||||
paddingPause{28, millisecondRange(1, 4)},
|
||||
paddingPause{29, millisecondRange(44, 61)},
|
||||
)
|
||||
}
|
||||
|
||||
func playStartPaddingVariant(chunks ...int) paddingVariant {
|
||||
if len(chunks) < 2 {
|
||||
panic("xmc: play start padding variant needs at least two chunks")
|
||||
}
|
||||
return pacedPaddingVariant(
|
||||
chunks,
|
||||
paddingPause{len(chunks) / 2, millisecondRange(1, 5)},
|
||||
paddingPause{len(chunks) - 1, millisecondRange(9, 20)},
|
||||
)
|
||||
}
|
||||
|
||||
func millisecondRange(minimum, maximum int) paddingDelayRange {
|
||||
return paddingDelayRange{
|
||||
min: time.Duration(minimum) * time.Millisecond,
|
||||
max: time.Duration(maximum) * time.Millisecond,
|
||||
}
|
||||
}
|
||||
|
||||
func newServerPaddingSchedule2612() ([]paddingTurn, error) {
|
||||
schedule := append([]paddingTurn(nil), paddingSchedule2612...)
|
||||
profileIndex, err := randomPaddingIndex(len(serverPlayBranchMasks2612))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile := serverPlayBranchMasks2612[profileIndex]
|
||||
for i, branches := range serverPlayBranches2612 {
|
||||
lengthRange := branches.small
|
||||
if profile&(1<<i) != 0 {
|
||||
lengthRange = branches.large
|
||||
}
|
||||
turn := &schedule[len(startupPaddingSchedule2612)+1+i*2]
|
||||
turn.sendMinLength = lengthRange.minimum
|
||||
turn.sendMaxLength = lengthRange.maximum
|
||||
}
|
||||
return schedule, nil
|
||||
}
|
||||
@@ -0,0 +1,618 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPaddingTurnReachesFinalTargetLength(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 128, maxLength: 128}
|
||||
const prefixLength = 3
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, prefixLength); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := prefixLength + encoded.Len(); got != turn.minLength {
|
||||
t.Fatalf("total turn length = %d, want %d", got, turn.minLength)
|
||||
}
|
||||
encodedReader := bytes.NewReader(encoded.Bytes())
|
||||
var recordLength Varint
|
||||
if err := recordLength.readFrom(encodedReader); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := int(recordLength); got != encoded.Len() {
|
||||
t.Fatalf("record length = %d, encoded = %d", got, encoded.Len())
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, prefixLength); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingTurnSupportsThreeByteTarget(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 3, maxLength: 3}
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := encoded.Len(); got != 3 {
|
||||
t.Fatalf("padding length = %d, want 3", got)
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingTurnVarintBoundaries(t *testing.T) {
|
||||
for _, targetLength := range []int{127, 128, 16383, 16384} {
|
||||
t.Run(strconv.Itoa(targetLength), func(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: targetLength, maxLength: targetLength}
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if encoded.Len() != targetLength {
|
||||
t.Fatalf("padding length = %d, want %d", encoded.Len(), targetLength)
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingTurnRandomRange(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingServerToClient, minLength: 127, maxLength: 129}
|
||||
seen := make(map[int]bool)
|
||||
for range 100 {
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if encoded.Len() < turn.minLength || encoded.Len() > turn.maxLength {
|
||||
t.Fatalf("padding length = %d", encoded.Len())
|
||||
}
|
||||
seen[encoded.Len()] = true
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("padding range did not vary: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingTurnUsesRestrictedSendRange(t *testing.T) {
|
||||
turn := paddingTurn{
|
||||
direction: paddingServerToClient,
|
||||
minLength: 3,
|
||||
maxLength: 100,
|
||||
sendMinLength: 90,
|
||||
sendMaxLength: 100,
|
||||
}
|
||||
seen := make(map[int]bool)
|
||||
for range 100 {
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if encoded.Len() < turn.sendMinLength || encoded.Len() > turn.sendMaxLength {
|
||||
t.Fatalf("padding length = %d", encoded.Len())
|
||||
}
|
||||
seen[encoded.Len()] = true
|
||||
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("restricted send range did not vary: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingScheduleSynchronizesDirections(t *testing.T) {
|
||||
client, server := net.Pipe()
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
|
||||
schedule := []paddingTurn{
|
||||
{direction: paddingClientToServer, minLength: 33, maxLength: 33},
|
||||
{direction: paddingServerToClient, minLength: 4097, maxLength: 4097},
|
||||
{direction: paddingClientToServer, minLength: 16385, maxLength: 16385},
|
||||
}
|
||||
serverDone := make(chan error, 1)
|
||||
go func() {
|
||||
serverDone <- runPaddingSchedule(server, server, false, 3, schedule)
|
||||
}()
|
||||
if err := runPaddingSchedule(client, client, true, 3, schedule); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case err := <-serverDone:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("server padding schedule did not complete")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnHandlesFragmentedInput(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 1024, maxLength: 1024}
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := readPaddingTurn(&oneByteReader{reader: bytes.NewReader(encoded.Bytes())}, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnRejectsInvalidLength(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 64, maxLength: 96}
|
||||
data := encodePaddingLength(t, 63)
|
||||
if err := readPaddingTurn(bytes.NewReader(data), turn, 0); err == nil || !strings.Contains(err.Error(), "outside") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnRejectsNonCanonicalHeader(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 3, maxLength: 3}
|
||||
err := readPaddingTurn(bytes.NewReader([]byte{0x83, 0x00, 0x00}), turn, 0)
|
||||
if err == nil || !strings.Contains(err.Error(), "non-canonical") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnRejectsTruncatedBody(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 64, maxLength: 64}
|
||||
data := encodePaddingLength(t, 64)
|
||||
if err := readPaddingTurn(bytes.NewReader(data), turn, 0); err == nil || !strings.Contains(err.Error(), "body") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnHonorsConnectionTimeout(t *testing.T) {
|
||||
client, server := net.Pipe()
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
|
||||
if err := server.SetReadDeadline(time.Now().Add(20 * time.Millisecond)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 64, maxLength: 64}
|
||||
err := readPaddingTurn(server, turn, 0)
|
||||
var netErr net.Error
|
||||
if !errors.As(err, &netErr) || !netErr.Timeout() {
|
||||
t.Fatalf("error = %v, want network timeout", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePaddingSchedule(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schedule []paddingTurn
|
||||
prefix int
|
||||
}{
|
||||
{name: "empty"},
|
||||
{name: "bad direction", schedule: []paddingTurn{{direction: 99, minLength: 4, maxLength: 4}}},
|
||||
{name: "too small", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 0, maxLength: 4}}},
|
||||
{name: "reversed range", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 7}}},
|
||||
{name: "wrong first direction", prefix: 3, schedule: []paddingTurn{{direction: paddingServerToClient, minLength: 8, maxLength: 8}}},
|
||||
{name: "prefix leaves no header", prefix: 8, schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8}}},
|
||||
{name: "same direction", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8}, {direction: paddingClientToServer, minLength: 8, maxLength: 8}}},
|
||||
{name: "range with variants", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, variants: []paddingVariant{paddingVariantFromChunks(8)}}}},
|
||||
{name: "empty variant", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{{}}}}},
|
||||
{name: "bad chunk", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(maxPaddingChunkLength + 1)}}}},
|
||||
{
|
||||
name: "delay mismatch",
|
||||
schedule: []paddingTurn{{
|
||||
direction: paddingClientToServer,
|
||||
variants: []paddingVariant{{
|
||||
chunks: []int{4, 4},
|
||||
delays: []paddingDelayRange{{min: time.Millisecond, max: time.Millisecond}},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
{name: "reversed start delay", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, startDelay: paddingDelayRange{min: 2 * time.Millisecond, max: time.Millisecond}}}},
|
||||
{name: "reversed generated chunk delay", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, chunkDelay: paddingDelayRange{min: 2 * time.Millisecond, max: time.Millisecond}}}},
|
||||
{name: "oversized generated chunk", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, writeChunkLength: maxPaddingChunkLength + 1}}},
|
||||
{name: "reversed generated chunk range", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, writeChunkMinLength: 9, writeChunkLength: 8}}},
|
||||
{name: "variant with generated chunks", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8)}, writeChunkLength: 8}}},
|
||||
{name: "send range outside accepted range", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 16, sendMinLength: 7, sendMaxLength: 12}}},
|
||||
{name: "variant with send range", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8)}, sendMinLength: 8, sendMaxLength: 8}}},
|
||||
{name: "negative chunk delay", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{{chunks: []int{8}, delays: []paddingDelayRange{{min: -time.Millisecond}}}}}}},
|
||||
{name: "bad send variant", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8)}, sendVariants: []int{1}}}},
|
||||
{name: "prefix splits chunk", prefix: 3, schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8, 4)}}}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if err := validatePaddingSchedule(test.schedule, test.prefix); err == nil {
|
||||
t.Fatal("expected invalid padding schedule")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingSchedule2612MatchesCapturedTemplates(t *testing.T) {
|
||||
wantDirections := []paddingDirection{
|
||||
paddingClientToServer,
|
||||
paddingServerToClient,
|
||||
paddingClientToServer,
|
||||
paddingServerToClient,
|
||||
paddingClientToServer,
|
||||
paddingServerToClient,
|
||||
}
|
||||
wantLengths := [][]int{
|
||||
{44},
|
||||
{72},
|
||||
{25},
|
||||
{41172},
|
||||
{2},
|
||||
{7464, 8267, 8790, 8153, 7375, 7347, 8184, 6633, 7670, 8241, 7857, 7254, 8407, 8283, 6804, 8177, 8177, 7929, 8296, 8177},
|
||||
}
|
||||
if len(paddingSchedule2612) != len(wantDirections)+33 {
|
||||
t.Fatalf("padding schedule has %d turns, want %d", len(paddingSchedule2612), len(wantDirections)+33)
|
||||
}
|
||||
for i, turn := range paddingSchedule2612[:len(wantDirections)] {
|
||||
if turn.direction != wantDirections[i] {
|
||||
t.Fatalf("padding turn %d direction = %d, want %d", i, turn.direction, wantDirections[i])
|
||||
}
|
||||
if len(turn.variants) != len(wantLengths[i]) {
|
||||
t.Fatalf("padding turn %d has %d variants, want %d", i, len(turn.variants), len(wantLengths[i]))
|
||||
}
|
||||
for j, variant := range turn.variants {
|
||||
if got := paddingVariantLength(variant); got != wantLengths[i][j] {
|
||||
t.Fatalf("padding turn %d variant %d length = %d, want %d", i, j, got, wantLengths[i][j])
|
||||
}
|
||||
}
|
||||
}
|
||||
wantPlayBounds := [][2]int{
|
||||
{6, 883},
|
||||
{346, 58638},
|
||||
{6, 887},
|
||||
{388, 61077},
|
||||
{2, 50},
|
||||
{575, 65584},
|
||||
{6, 45},
|
||||
{86, 63563},
|
||||
{2, 44},
|
||||
{42, 51983},
|
||||
{2, 851},
|
||||
{309, 25083},
|
||||
{2, 19},
|
||||
{74, 63885},
|
||||
{8, 24},
|
||||
{30, 66128},
|
||||
{2, 19},
|
||||
{26, 35818},
|
||||
{6, 19},
|
||||
{35, 59407},
|
||||
{6, 19},
|
||||
{37, 65328},
|
||||
{2, 19},
|
||||
{26, 60622},
|
||||
{6, 19},
|
||||
{11, 60808},
|
||||
{8, 43},
|
||||
{55, 62027},
|
||||
{2, 19},
|
||||
{427, 65622},
|
||||
{5, 19},
|
||||
{35, 59401},
|
||||
{6, 19},
|
||||
}
|
||||
for i, want := range wantPlayBounds {
|
||||
turn := paddingSchedule2612[len(wantDirections)+i]
|
||||
wantDirection := paddingClientToServer
|
||||
if i%2 == 1 {
|
||||
wantDirection = paddingServerToClient
|
||||
}
|
||||
if turn.direction != wantDirection {
|
||||
t.Fatalf("play turn %d direction = %d, want %d", i, turn.direction, wantDirection)
|
||||
}
|
||||
if turn.minLength != want[0] || turn.maxLength != want[1] {
|
||||
t.Fatalf("play turn %d bounds = %d-%d, want %d-%d", i, turn.minLength, turn.maxLength, want[0], want[1])
|
||||
}
|
||||
if len(turn.variants) != 0 {
|
||||
t.Fatalf("play turn %d unexpectedly has captured variants", i)
|
||||
}
|
||||
}
|
||||
if got := len(paddingSchedule2612[3].variants[0].chunks); got != 30 {
|
||||
t.Fatalf("registry turn chunks = %d, want 30", got)
|
||||
}
|
||||
minimumPlayStart := maxPaddingTurnLength
|
||||
maximumPlayStart := 0
|
||||
for _, variant := range paddingSchedule2612[5].variants {
|
||||
length := paddingVariantLength(variant)
|
||||
minimumPlayStart = min(minimumPlayStart, length)
|
||||
maximumPlayStart = max(maximumPlayStart, length)
|
||||
if variant.chunks[0] != 4941 {
|
||||
t.Fatalf("play start first chunk = %d, want 4941", variant.chunks[0])
|
||||
}
|
||||
}
|
||||
if minimumPlayStart != 6633 || maximumPlayStart != 8790 {
|
||||
t.Fatalf("play start bounds = %d-%d, want 6633-8790", minimumPlayStart, maximumPlayStart)
|
||||
}
|
||||
if err := validatePaddingSchedule(paddingSchedule2612, 2); err != nil {
|
||||
t.Fatalf("captured schedule is invalid: %v", err)
|
||||
}
|
||||
|
||||
serverSchedule, err := newServerPaddingSchedule2612()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = validatePaddingSchedule(serverSchedule, 2); err != nil {
|
||||
t.Fatalf("server schedule is invalid: %v", err)
|
||||
}
|
||||
for i, branches := range serverPlayBranches2612 {
|
||||
turn := serverSchedule[len(startupPaddingSchedule2612)+1+i*2]
|
||||
got := paddingLengthRange2612{turn.sendMinLength, turn.sendMaxLength}
|
||||
if got != branches.small && got != branches.large {
|
||||
t.Fatalf("server play turn %d send range = %v, want %v or %v", i, got, branches.small, branches.large)
|
||||
}
|
||||
}
|
||||
|
||||
for range 20 {
|
||||
clientSchedule, clientErr := newClientPaddingSchedule2612()
|
||||
if clientErr != nil {
|
||||
t.Fatal(clientErr)
|
||||
}
|
||||
if clientErr = validatePaddingSchedule(clientSchedule, 2); clientErr != nil {
|
||||
t.Fatalf("client schedule is invalid: %v", clientErr)
|
||||
}
|
||||
burstCount := 0
|
||||
for _, burst := range clientPlayBursts2612 {
|
||||
turn := clientSchedule[len(startupPaddingSchedule2612)+burst.playIndex]
|
||||
got := paddingLengthRange2612{turn.sendMinLength, turn.sendMaxLength}
|
||||
switch got {
|
||||
case burst.regular:
|
||||
case burst.burst:
|
||||
burstCount++
|
||||
default:
|
||||
t.Fatalf("client play turn %d send range = %v", burst.playIndex, got)
|
||||
}
|
||||
}
|
||||
if burstCount != 1 {
|
||||
t.Fatalf("client schedule has %d initialization bursts, want 1", burstCount)
|
||||
}
|
||||
}
|
||||
|
||||
for variantIndex := range paddingSchedule2612[3].variants {
|
||||
turn := paddingSchedule2612[3]
|
||||
turn.sendVariants = []int{variantIndex}
|
||||
var encoded bytes.Buffer
|
||||
if err = writePaddingTurnWithSleep(&encoded, turn, 0, func(time.Duration) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = readPaddingTurn(bytes.NewReader(encoded.Bytes()), paddingSchedule2612[3], 0); err != nil {
|
||||
t.Fatalf("registry variant %d was rejected: %v", variantIndex, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingVariantPreservesWriteBoundaries(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
turn paddingTurn
|
||||
prefix int
|
||||
want []int
|
||||
}{
|
||||
{name: "login acknowledged turn", turn: paddingSchedule2612[0], prefix: 2, want: []int{26, 16}},
|
||||
{name: "server response turn", turn: paddingSchedule2612[1], want: []int{26, 21, 25}},
|
||||
{name: "single packet turn", turn: paddingSchedule2612[2], want: []int{25}},
|
||||
{name: "fixed registry profile", turn: paddingSchedule2612[3], want: paddingSchedule2612[3].variants[0].chunks},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var writer recordingWriter
|
||||
if err := writePaddingTurnWithSleep(&writer, test.turn, test.prefix, func(time.Duration) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(writer.writes) != len(test.want) {
|
||||
t.Fatalf("writes = %v, want %v", writer.writes, test.want)
|
||||
}
|
||||
for i := range test.want {
|
||||
if writer.writes[i] != test.want[i] {
|
||||
t.Fatalf("writes = %v, want %v", writer.writes, test.want)
|
||||
}
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(writer.Bytes()), test.turn, test.prefix); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingVariantAppliesPacing(t *testing.T) {
|
||||
turn := paddingTurn{
|
||||
direction: paddingClientToServer,
|
||||
startDelay: paddingDelayRange{min: 3 * time.Millisecond, max: 3 * time.Millisecond},
|
||||
variants: []paddingVariant{{
|
||||
chunks: []int{3, 5, 7},
|
||||
delays: []paddingDelayRange{
|
||||
{},
|
||||
{min: 2 * time.Millisecond, max: 2 * time.Millisecond},
|
||||
{min: 4 * time.Millisecond, max: 4 * time.Millisecond},
|
||||
},
|
||||
}},
|
||||
}
|
||||
var slept []time.Duration
|
||||
var writer recordingWriter
|
||||
if err := writePaddingTurnWithSleep(&writer, turn, 3, func(delay time.Duration) {
|
||||
slept = append(slept, delay)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []time.Duration{3 * time.Millisecond, 2 * time.Millisecond, 4 * time.Millisecond}
|
||||
if len(slept) != len(want) {
|
||||
t.Fatalf("delays = %v, want %v", slept, want)
|
||||
}
|
||||
for i := range want {
|
||||
if slept[i] != want[i] {
|
||||
t.Fatalf("delays = %v, want %v", slept, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratedPaddingChunksApplyPacing(t *testing.T) {
|
||||
turn := paddingTurn{
|
||||
direction: paddingServerToClient,
|
||||
minLength: 100,
|
||||
maxLength: 100,
|
||||
writeChunkLength: 32,
|
||||
chunkDelay: paddingDelayRange{min: 2 * time.Millisecond, max: 2 * time.Millisecond},
|
||||
}
|
||||
var slept []time.Duration
|
||||
var writer recordingWriter
|
||||
if err := writePaddingTurnWithSleep(&writer, turn, 0, func(delay time.Duration) {
|
||||
slept = append(slept, delay)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantWrites := []int{32, 32, 32, 4}
|
||||
if !slicesEqual(writer.writes, wantWrites) {
|
||||
t.Fatalf("writes = %v, want %v", writer.writes, wantWrites)
|
||||
}
|
||||
wantSleeps := []time.Duration{2 * time.Millisecond, 2 * time.Millisecond, 2 * time.Millisecond}
|
||||
if !slicesEqual(slept, wantSleeps) {
|
||||
t.Fatalf("delays = %v, want %v", slept, wantSleeps)
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(writer.Bytes()), turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratedPaddingChunkLengthIsRandomized(t *testing.T) {
|
||||
turn := paddingTurn{
|
||||
direction: paddingServerToClient,
|
||||
minLength: 100,
|
||||
maxLength: 100,
|
||||
writeChunkMinLength: 16,
|
||||
writeChunkLength: 32,
|
||||
}
|
||||
seen := make(map[int]bool)
|
||||
for range 100 {
|
||||
var writer recordingWriter
|
||||
if err := writePaddingTurnWithSleep(&writer, turn, 0, func(time.Duration) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
firstWrite := writer.writes[0]
|
||||
if firstWrite < turn.writeChunkMinLength || firstWrite > turn.writeChunkLength {
|
||||
t.Fatalf("first write = %d", firstWrite)
|
||||
}
|
||||
seen[firstWrite] = true
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("generated write chunk length did not vary: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingDelayRangeIsRandomized(t *testing.T) {
|
||||
delayRange := millisecondRange(25, 40)
|
||||
seen := make(map[time.Duration]bool)
|
||||
for range 100 {
|
||||
delay, err := randomPaddingDelay(delayRange)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if delay < delayRange.min || delay > delayRange.max {
|
||||
t.Fatalf("delay = %s, want %s-%s", delay, delayRange.min, delayRange.max)
|
||||
}
|
||||
seen[delay] = true
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("padding delay did not vary: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingSchedule2612UsesCoarseTimingBands(t *testing.T) {
|
||||
assertDelayRange(t, "turn 3 to 4", paddingSchedule2612[3].startDelay, 20*time.Millisecond, 50*time.Millisecond)
|
||||
assertDelayRange(t, "turn 5 to 6", paddingSchedule2612[5].startDelay, 35*time.Millisecond, 50*time.Millisecond)
|
||||
assertDelayRange(t, "first play client turn", paddingSchedule2612[6].startDelay, time.Millisecond, 30*time.Millisecond)
|
||||
assertDelayRange(t, "first play server turn", paddingSchedule2612[7].startDelay, time.Millisecond, 45*time.Millisecond)
|
||||
assertDelayRange(t, "play server chunk pacing", paddingSchedule2612[7].chunkDelay, time.Millisecond, 4*time.Millisecond)
|
||||
if paddingSchedule2612[6].writeChunkLength != 1024 {
|
||||
t.Fatalf("play client write chunk = %d, want 1024", paddingSchedule2612[6].writeChunkLength)
|
||||
}
|
||||
if paddingSchedule2612[7].writeChunkLength != maxPaddingChunkLength {
|
||||
t.Fatalf("play server write chunk = %d, want %d", paddingSchedule2612[7].writeChunkLength, maxPaddingChunkLength)
|
||||
}
|
||||
if paddingSchedule2612[7].writeChunkMinLength != 32*1024 {
|
||||
t.Fatalf("play server minimum write chunk = %d, want %d", paddingSchedule2612[7].writeChunkMinLength, 32*1024)
|
||||
}
|
||||
|
||||
for i, variant := range paddingSchedule2612[3].variants {
|
||||
minimum, maximum := paddingVariantDelayBounds(variant)
|
||||
if minimum != 45*time.Millisecond || maximum != 65*time.Millisecond {
|
||||
t.Fatalf("turn 4 variant %d duration = %s-%s, want 45ms-65ms", i, minimum, maximum)
|
||||
}
|
||||
}
|
||||
for i, variant := range paddingSchedule2612[5].variants {
|
||||
minimum, maximum := paddingVariantDelayBounds(variant)
|
||||
if minimum != 10*time.Millisecond || maximum != 25*time.Millisecond {
|
||||
t.Fatalf("turn 6 variant %d duration = %s-%s, want 10ms-25ms", i, minimum, maximum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertDelayRange(t *testing.T, name string, got paddingDelayRange, minimum, maximum time.Duration) {
|
||||
t.Helper()
|
||||
if got.min != minimum || got.max != maximum {
|
||||
t.Fatalf("%s delay = %s-%s, want %s-%s", name, got.min, got.max, minimum, maximum)
|
||||
}
|
||||
}
|
||||
|
||||
func paddingVariantDelayBounds(variant paddingVariant) (time.Duration, time.Duration) {
|
||||
var minimum time.Duration
|
||||
var maximum time.Duration
|
||||
for _, delay := range variant.delays {
|
||||
minimum += delay.min
|
||||
maximum += delay.max
|
||||
}
|
||||
return minimum, maximum
|
||||
}
|
||||
|
||||
func slicesEqual[T comparable](left, right []T) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for i := range left {
|
||||
if left[i] != right[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func encodePaddingLength(t *testing.T, length int) []byte {
|
||||
t.Helper()
|
||||
var encoded bytes.Buffer
|
||||
value := Varint(length)
|
||||
if err := value.writeTo(&encoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return encoded.Bytes()
|
||||
}
|
||||
|
||||
type oneByteReader struct {
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
func (r *oneByteReader) Read(p []byte) (int, error) {
|
||||
if len(p) > 1 {
|
||||
p = p[:1]
|
||||
}
|
||||
return r.reader.Read(p)
|
||||
}
|
||||
|
||||
type recordingWriter struct {
|
||||
bytes.Buffer
|
||||
writes []int
|
||||
}
|
||||
|
||||
func (w *recordingWriter) Write(p []byte) (int, error) {
|
||||
w.writes = append(w.writes, len(p))
|
||||
return w.Buffer.Write(p)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user