mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-14 11:34:03 +00:00
Compare commits
23
Commits
fix-custom
...
aa3d6589da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa3d6589da | ||
|
|
25c11e2d2b | ||
|
|
dffc7ada5e | ||
|
|
77f98eba09 | ||
|
|
f124daf5a3 | ||
|
|
598bde7412 | ||
|
|
9b373e39ca | ||
|
|
c7e569b037 | ||
|
|
f02a357861 | ||
|
|
5fe6d6217a | ||
|
|
0604ffa957 | ||
|
|
d3f1a24285 | ||
|
|
2323273e37 | ||
|
|
09107b71dc | ||
|
|
7021606ad3 | ||
|
|
7d214f8b09 | ||
|
|
8b419d833d | ||
|
|
a12801c13b | ||
|
|
a000371b2a | ||
|
|
bc6e966af8 | ||
|
|
fc5620de98 | ||
|
|
b02bdcf4cc | ||
|
|
2b329b3675 |
@@ -82,7 +82,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v4.6.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
|||||||
@@ -78,6 +78,12 @@ func (o *Observer) background() {
|
|||||||
sleepTime = time.Duration(o.config.ProbeInterval)
|
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 {
|
if !o.config.EnableConcurrency {
|
||||||
sort.Strings(outbounds)
|
sort.Strings(outbounds)
|
||||||
for _, v := range outbounds {
|
for _, v := range outbounds {
|
||||||
|
|||||||
@@ -347,7 +347,6 @@ func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound)
|
|||||||
// case addr.Family().IsDomain():
|
// case addr.Family().IsDomain():
|
||||||
default:
|
default:
|
||||||
ob.Gateway = addr
|
ob.Gateway = addr
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ func (b *Balancer) SelectOutbounds() ([]string, error) {
|
|||||||
|
|
||||||
// GetPrincipleTarget implements routing.BalancerPrincipleTarget
|
// GetPrincipleTarget implements routing.BalancerPrincipleTarget
|
||||||
func (r *Router) GetPrincipleTarget(tag string) ([]string, error) {
|
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 {
|
if s, ok := b.strategy.(BalancingPrincipleTarget); ok {
|
||||||
candidates, err := b.SelectOutbounds()
|
candidates, err := b.SelectOutbounds()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -151,7 +151,7 @@ func (r *Router) GetPrincipleTarget(tag string) ([]string, error) {
|
|||||||
|
|
||||||
// SetOverrideTarget implements routing.BalancerOverrider
|
// SetOverrideTarget implements routing.BalancerOverrider
|
||||||
func (r *Router) SetOverrideTarget(tag, target string) error {
|
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)
|
b.override.Put(target)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -160,7 +160,7 @@ func (r *Router) SetOverrideTarget(tag, target string) error {
|
|||||||
|
|
||||||
// GetOverrideTarget implements routing.BalancerOverrider
|
// GetOverrideTarget implements routing.BalancerOverrider
|
||||||
func (r *Router) GetOverrideTarget(tag string) (string, error) {
|
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 b.override.Get(), nil
|
||||||
}
|
}
|
||||||
return "", errors.New("cannot find tag")
|
return "", errors.New("cannot find tag")
|
||||||
|
|||||||
@@ -2,25 +2,8 @@ package router
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
sync "sync"
|
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 {
|
type overrideSettings struct {
|
||||||
target string
|
target string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -393,3 +394,22 @@ func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool {
|
|||||||
}
|
}
|
||||||
return false
|
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 (
|
import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/xtls/xray-core/app/router"
|
. "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) {
|
func BenchmarkMphDomainMatcher(b *testing.B) {
|
||||||
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
||||||
rules, err := geodata.ParseDomainRules([]string{"geosite:cn"}, geodata.Domain_Substr)
|
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) {
|
func (rr *RoutingRule) BuildCondition() (Condition, error) {
|
||||||
conds := NewConditionChan()
|
conds := NewConditionChan()
|
||||||
|
|
||||||
|
if len(rr.LocalOs) > 0 {
|
||||||
|
conds.Add(NewLocalOSMatcher(rr.LocalOs))
|
||||||
|
}
|
||||||
|
|
||||||
if len(rr.InboundTag) > 0 {
|
if len(rr.InboundTag) > 0 {
|
||||||
conds.Add(NewInboundTagMatcher(rr.InboundTag))
|
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"`
|
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"`
|
Process []string `protobuf:"bytes,21,rep,name=process,proto3" json:"process,omitempty"`
|
||||||
Webhook *WebhookConfig `protobuf:"bytes,22,opt,name=webhook,proto3" json:"webhook,omitempty"`
|
Webhook *WebhookConfig `protobuf:"bytes,22,opt,name=webhook,proto3" json:"webhook,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
// List of operating systems for matching the one Xray itself is running on.
|
||||||
sizeCache protoimpl.SizeCache
|
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() {
|
func (x *RoutingRule) Reset() {
|
||||||
@@ -278,6 +280,13 @@ func (x *RoutingRule) GetWebhook() *WebhookConfig {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *RoutingRule) GetLocalOs() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.LocalOs
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type isRoutingRule_TargetTag interface {
|
type isRoutingRule_TargetTag interface {
|
||||||
isRoutingRule_TargetTag()
|
isRoutingRule_TargetTag()
|
||||||
}
|
}
|
||||||
@@ -637,7 +646,7 @@ var File_app_router_config_proto protoreflect.FileDescriptor
|
|||||||
|
|
||||||
const file_app_router_config_proto_rawDesc = "" +
|
const file_app_router_config_proto_rawDesc = "" +
|
||||||
"\n" +
|
"\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" +
|
"\vRoutingRule\x12\x12\n" +
|
||||||
"\x03tag\x18\x01 \x01(\tH\x00R\x03tag\x12%\n" +
|
"\x03tag\x18\x01 \x01(\tH\x00R\x03tag\x12%\n" +
|
||||||
"\rbalancing_tag\x18\f \x01(\tH\x00R\fbalancingTag\x12\x19\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" +
|
"\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" +
|
"\x10vless_route_list\x18\x14 \x01(\v2\x19.xray.common.net.PortListR\x0evlessRouteList\x12\x18\n" +
|
||||||
"\aprocess\x18\x15 \x03(\tR\aprocess\x128\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" +
|
"\x0fAttributesEntry\x12\x10\n" +
|
||||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\f\n" +
|
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\f\n" +
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ message RoutingRule {
|
|||||||
|
|
||||||
repeated string process = 21;
|
repeated string process = 21;
|
||||||
WebhookConfig webhook = 22;
|
WebhookConfig webhook = 22;
|
||||||
|
|
||||||
|
// List of operating systems for matching the one Xray itself is running on.
|
||||||
|
repeated string local_os = 23;
|
||||||
}
|
}
|
||||||
|
|
||||||
message WebhookConfig {
|
message WebhookConfig {
|
||||||
|
|||||||
+59
-114
@@ -2,7 +2,9 @@ package router
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"maps"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common"
|
"github.com/xtls/xray-core/common"
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
@@ -17,8 +19,8 @@ import (
|
|||||||
// Router is an implementation of routing.Router.
|
// Router is an implementation of routing.Router.
|
||||||
type Router struct {
|
type Router struct {
|
||||||
domainStrategy Config_DomainStrategy
|
domainStrategy Config_DomainStrategy
|
||||||
rules []*Rule
|
rules atomic.Pointer[[]*Rule]
|
||||||
balancers map[string]*Balancer
|
balancers atomic.Pointer[map[string]*Balancer]
|
||||||
dns dns.Client
|
dns dns.Client
|
||||||
|
|
||||||
ctx context.Context
|
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.ohm = ohm
|
||||||
r.dispatcher = dispatcher
|
r.dispatcher = dispatcher
|
||||||
|
|
||||||
r.balancers = make(map[string]*Balancer, len(config.BalancingRule))
|
r.rules.Store(new([]*Rule))
|
||||||
for _, rule := range config.BalancingRule {
|
r.balancers.Store(&map[string]*Balancer{})
|
||||||
balancer, err := rule.Build(ohm, dispatcher)
|
return r.ReloadRules(config, false)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
balancer.InjectContext(ctx)
|
|
||||||
r.balancers[rule.Tag] = balancer
|
|
||||||
}
|
|
||||||
|
|
||||||
r.rules = make([]*Rule, 0, len(config.Rule))
|
|
||||||
for _, rule := range config.Rule {
|
|
||||||
cond, err := rule.BuildCondition()
|
|
||||||
if err != nil {
|
|
||||||
r.closeWebhooks()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rr := &Rule{
|
|
||||||
Condition: cond,
|
|
||||||
Tag: rule.GetTag(),
|
|
||||||
RuleTag: rule.GetRuleTag(),
|
|
||||||
}
|
|
||||||
if wh := rule.GetWebhook(); wh != nil {
|
|
||||||
notifier, err := NewWebhookNotifier(wh)
|
|
||||||
if err != nil {
|
|
||||||
r.closeWebhooks()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rr.Webhook = notifier
|
|
||||||
}
|
|
||||||
btag := rule.GetBalancingTag()
|
|
||||||
if len(btag) > 0 {
|
|
||||||
brule, found := r.balancers[btag]
|
|
||||||
if !found {
|
|
||||||
if rr.Webhook != nil {
|
|
||||||
rr.Webhook.Close()
|
|
||||||
}
|
|
||||||
r.closeWebhooks()
|
|
||||||
return errors.New("balancer ", btag, " not found")
|
|
||||||
}
|
|
||||||
rr.Balancer = brule
|
|
||||||
}
|
|
||||||
r.rules = append(r.rules, rr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PickRoute implements routing.Router.
|
// PickRoute implements routing.Router.
|
||||||
@@ -124,18 +83,22 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
|
|||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
if !shouldAppend {
|
oldRules := *r.rules.Load()
|
||||||
for _, rule := range r.rules {
|
oldBalancers := *r.balancers.Load()
|
||||||
if rule.Webhook != nil {
|
|
||||||
rule.Webhook.Close()
|
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 {
|
for _, rule := range config.BalancingRule {
|
||||||
_, found := r.balancers[rule.Tag]
|
if _, found := newBalancers[rule.Tag]; found {
|
||||||
if found {
|
|
||||||
return errors.New("duplicate balancer tag")
|
return errors.New("duplicate balancer tag")
|
||||||
}
|
}
|
||||||
balancer, err := rule.Build(r.ohm, r.dispatcher)
|
balancer, err := rule.Build(r.ohm, r.dispatcher)
|
||||||
@@ -143,27 +106,12 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
balancer.InjectContext(r.ctx)
|
balancer.InjectContext(r.ctx)
|
||||||
r.balancers[rule.Tag] = balancer
|
newBalancers[rule.Tag] = balancer
|
||||||
}
|
|
||||||
|
|
||||||
startIdx := len(r.rules)
|
|
||||||
closeNewWebhooks := func() {
|
|
||||||
for i := startIdx; i < len(r.rules); i++ {
|
|
||||||
if r.rules[i].Webhook != nil {
|
|
||||||
r.rules[i].Webhook.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r.rules = r.rules[:startIdx]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, rule := range config.Rule {
|
for _, rule := range config.Rule {
|
||||||
if r.RuleExists(rule.GetRuleTag()) {
|
|
||||||
closeNewWebhooks()
|
|
||||||
return errors.New("duplicate ruleTag ", rule.GetRuleTag())
|
|
||||||
}
|
|
||||||
cond, err := rule.BuildCondition()
|
cond, err := rule.BuildCondition()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
closeNewWebhooks()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rr := &Rule{
|
rr := &Rule{
|
||||||
@@ -171,69 +119,64 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
|
|||||||
Tag: rule.GetTag(),
|
Tag: rule.GetTag(),
|
||||||
RuleTag: rule.GetRuleTag(),
|
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 {
|
if wh := rule.GetWebhook(); wh != nil {
|
||||||
notifier, err := NewWebhookNotifier(wh)
|
notifier, err := NewWebhookNotifier(wh)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
closeNewWebhooks()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rr.Webhook = notifier
|
rr.Webhook = notifier
|
||||||
}
|
}
|
||||||
btag := rule.GetBalancingTag()
|
if btag := rule.GetBalancingTag(); len(btag) > 0 {
|
||||||
if len(btag) > 0 {
|
brule, found := newBalancers[btag]
|
||||||
brule, found := r.balancers[btag]
|
|
||||||
if !found {
|
if !found {
|
||||||
if rr.Webhook != nil {
|
|
||||||
rr.Webhook.Close()
|
|
||||||
}
|
|
||||||
closeNewWebhooks()
|
|
||||||
return errors.New("balancer ", btag, " not found")
|
return errors.New("balancer ", btag, " not found")
|
||||||
}
|
}
|
||||||
rr.Balancer = brule
|
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
|
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.
|
// RemoveRule implements routing.Router.
|
||||||
func (r *Router) RemoveRule(tag string) error {
|
func (r *Router) RemoveRule(tag string) error {
|
||||||
|
if tag == "" {
|
||||||
|
return errors.New("empty tag name!")
|
||||||
|
}
|
||||||
|
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
newRules := []*Rule{}
|
oldRules := *r.rules.Load()
|
||||||
if tag != "" {
|
newRules := make([]*Rule, 0, len(oldRules))
|
||||||
for _, rule := range r.rules {
|
var removed []*Rule
|
||||||
if rule.RuleTag != tag {
|
for _, rule := range oldRules {
|
||||||
newRules = append(newRules, rule)
|
if rule.RuleTag != tag {
|
||||||
} else if rule.Webhook != nil {
|
newRules = append(newRules, rule)
|
||||||
rule.Webhook.Close()
|
} 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
|
// ListRule implements routing.Router
|
||||||
func (r *Router) ListRule() []routing.Route {
|
func (r *Router) ListRule() []routing.Route {
|
||||||
r.mu.Lock()
|
rules := *r.rules.Load()
|
||||||
defer r.mu.Unlock()
|
ruleList := make([]routing.Route, 0, len(rules))
|
||||||
ruleList := make([]routing.Route, 0)
|
for _, rule := range rules {
|
||||||
for _, rule := range r.rules {
|
|
||||||
ruleList = append(ruleList, &Route{
|
ruleList = append(ruleList, &Route{
|
||||||
outboundTag: rule.Tag,
|
outboundTag: rule.Tag,
|
||||||
ruleTag: rule.RuleTag,
|
ruleTag: rule.RuleTag,
|
||||||
@@ -252,7 +195,9 @@ func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context,
|
|||||||
ctx = routing_dns.ContextWithDNSClient(ctx, r.dns)
|
ctx = routing_dns.ContextWithDNSClient(ctx, r.dns)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, rule := range r.rules {
|
rules := *r.rules.Load()
|
||||||
|
|
||||||
|
for _, rule := range rules {
|
||||||
if rule.Apply(ctx) {
|
if rule.Apply(ctx) {
|
||||||
return rule, ctx, nil
|
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)
|
ctx = routing_dns.ContextWithDNSClient(ctx, r.dns)
|
||||||
|
|
||||||
// Try applying rules again if we have IPs.
|
// Try applying rules again if we have IPs.
|
||||||
for _, rule := range r.rules {
|
for _, rule := range rules {
|
||||||
if rule.Apply(ctx) {
|
if rule.Apply(ctx) {
|
||||||
return rule, ctx, nil
|
return rule, ctx, nil
|
||||||
}
|
}
|
||||||
@@ -279,9 +224,9 @@ func (r *Router) Start() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// closeWebhooks closes all webhook notifiers in the current rule set.
|
// closeWebhooks closes all webhook notifiers in the given rule set.
|
||||||
func (r *Router) closeWebhooks() {
|
func closeWebhooks(rules []*Rule) {
|
||||||
for _, rule := range r.rules {
|
for _, rule := range rules {
|
||||||
if rule.Webhook != nil {
|
if rule.Webhook != nil {
|
||||||
rule.Webhook.Close()
|
rule.Webhook.Close()
|
||||||
}
|
}
|
||||||
@@ -292,7 +237,7 @@ func (r *Router) closeWebhooks() {
|
|||||||
func (r *Router) Close() error {
|
func (r *Router) Close() error {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
r.closeWebhooks()
|
closeWebhooks(*r.rules.Load())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-23
@@ -8,6 +8,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
@@ -40,6 +41,7 @@ type WebhookNotifier struct {
|
|||||||
deduplication uint32
|
deduplication uint32
|
||||||
client *http.Client
|
client *http.Client
|
||||||
seen sync.Map
|
seen sync.Map
|
||||||
|
lastSweep atomic.Int64
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
closeOnce sync.Once
|
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
|
return h, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +198,7 @@ func (h *WebhookNotifier) isDuplicate(email string) bool {
|
|||||||
}
|
}
|
||||||
ttl := time.Duration(h.deduplication) * time.Second
|
ttl := time.Duration(h.deduplication) * time.Second
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
h.maybeSweep(now, ttl)
|
||||||
if v, loaded := h.seen.LoadOrStore(email, now); loaded {
|
if v, loaded := h.seen.LoadOrStore(email, now); loaded {
|
||||||
if now.Sub(v.(time.Time)) < ttl {
|
if now.Sub(v.(time.Time)) < ttl {
|
||||||
return true
|
return true
|
||||||
@@ -210,27 +208,23 @@ func (h *WebhookNotifier) isDuplicate(email string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *WebhookNotifier) cleanupLoop() {
|
func (h *WebhookNotifier) maybeSweep(now time.Time, ttl time.Duration) {
|
||||||
defer h.wg.Done()
|
last := h.lastSweep.Load()
|
||||||
ttl := time.Duration(h.deduplication) * time.Second
|
if now.UnixNano()-last < int64(ttl) {
|
||||||
ticker := time.NewTicker(ttl)
|
return
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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 {
|
func (h *WebhookNotifier) Close() error {
|
||||||
h.closeOnce.Do(func() {
|
h.closeOnce.Do(func() {
|
||||||
close(h.done)
|
close(h.done)
|
||||||
|
|||||||
@@ -198,7 +198,9 @@ func darwinSocketInfoMatchLevel(info []byte, network string, srcAddr netip.Addr,
|
|||||||
|
|
||||||
vflag := info[darwinInSockInfoVFlagOff]
|
vflag := info[darwinInSockInfoVFlagOff]
|
||||||
if srcAddr.Is4() {
|
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
|
return darwinSocketNoMatch
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -52,6 +52,57 @@ func TestFindProcessDarwinTCP(t *testing.T) {
|
|||||||
assertCurrentProcess(t, pid, name, path)
|
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) {
|
func TestFindProcessDarwinUDP(t *testing.T) {
|
||||||
conn, err := stdnet.ListenUDP("udp", &stdnet.UDPAddr{IP: stdnet.ParseIP("127.0.0.1")})
|
conn, err := stdnet.ListenUDP("udp", &stdnet.UDPAddr{IP: stdnet.ParseIP("127.0.0.1")})
|
||||||
if err != nil {
|
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 {
|
func newDarwinSocketInfo(network string, local netip.Addr, localPort uint16, remote netip.Addr, remotePort uint16) []byte {
|
||||||
info := make([]byte, darwinSocketFDInfoSize)
|
info := make([]byte, darwinSocketFDInfoSize)
|
||||||
switch network {
|
switch network {
|
||||||
|
|||||||
@@ -3,11 +3,8 @@ package bittorrent
|
|||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"math"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common"
|
"github.com/xtls/xray-core/common"
|
||||||
"github.com/xtls/xray-core/common/buf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SniffHeader struct{}
|
type SniffHeader struct{}
|
||||||
@@ -39,50 +36,44 @@ func SniffUTP(b []byte) (*SniffHeader, error) {
|
|||||||
return nil, common.ErrNoClue
|
return nil, common.ErrNoClue
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer := buf.FromBytes(b)
|
// type 4 (ST_SYN), version 1
|
||||||
|
if b[0] != 0x41 {
|
||||||
var typeAndVersion uint8
|
|
||||||
|
|
||||||
if binary.Read(buffer, binary.BigEndian, &typeAndVersion) != nil {
|
|
||||||
return nil, common.ErrNoClue
|
|
||||||
} else if b[0]>>4&0xF > 4 || b[0]&0xF != 1 {
|
|
||||||
return nil, errNotBittorrent
|
return nil, errNotBittorrent
|
||||||
}
|
}
|
||||||
|
|
||||||
var extension uint8
|
// timestamp_difference is always 0 in new connections
|
||||||
|
if binary.BigEndian.Uint32(b[8:12]) != 0 {
|
||||||
if binary.Read(buffer, binary.BigEndian, &extension) != nil {
|
|
||||||
return nil, common.ErrNoClue
|
|
||||||
} else if extension != 0 && extension != 1 {
|
|
||||||
return nil, errNotBittorrent
|
return nil, errNotBittorrent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Walk the extension chain. Selective ack (1) and extension bits (2)
|
||||||
|
extension, offset := b[1], 20
|
||||||
for extension != 0 {
|
for extension != 0 {
|
||||||
if extension != 1 {
|
if len(b) < offset+2 {
|
||||||
return nil, errNotBittorrent
|
return nil, errNotBittorrent
|
||||||
}
|
}
|
||||||
if binary.Read(buffer, binary.BigEndian, &extension) != nil {
|
length := int(b[offset+1])
|
||||||
return nil, common.ErrNoClue
|
switch extension {
|
||||||
|
case 1: // selective ack
|
||||||
|
if length < 4 || length%4 != 0 {
|
||||||
|
return nil, errNotBittorrent
|
||||||
|
}
|
||||||
|
case 2: // extension bits: fixed 8 bytes, sent in ST_SYN by µTorrent
|
||||||
|
if length != 8 {
|
||||||
|
return nil, errNotBittorrent
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, errNotBittorrent
|
||||||
}
|
}
|
||||||
|
if len(b) < offset+2+length {
|
||||||
var length uint8
|
return nil, errNotBittorrent
|
||||||
if err := binary.Read(buffer, binary.BigEndian, &length); err != nil {
|
|
||||||
return nil, common.ErrNoClue
|
|
||||||
}
|
|
||||||
if common.Error2(buffer.ReadBytes(int32(length))) != nil {
|
|
||||||
return nil, common.ErrNoClue
|
|
||||||
}
|
}
|
||||||
|
extension = b[offset]
|
||||||
|
offset += 2 + length
|
||||||
}
|
}
|
||||||
|
|
||||||
if common.Error2(buffer.ReadBytes(2)) != nil {
|
// extensions should consume all ST_SYN payload
|
||||||
return nil, common.ErrNoClue
|
if len(b) != offset {
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
return nil, errNotBittorrent
|
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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,35 +4,35 @@ go 1.26
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716
|
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716
|
||||||
github.com/cloudflare/circl v1.6.4
|
github.com/cloudflare/circl v1.6.5
|
||||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
|
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
|
||||||
github.com/golang/mock v1.7.0-rc.1
|
github.com/golang/mock v1.7.0-rc.1
|
||||||
github.com/google/go-cmp v0.7.0
|
github.com/google/go-cmp v0.7.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/klauspost/cpuid/v2 v2.4.0
|
github.com/klauspost/cpuid/v2 v2.4.0
|
||||||
github.com/miekg/dns v1.1.72
|
github.com/miekg/dns v1.1.73
|
||||||
github.com/pelletier/go-toml v1.9.5
|
github.com/pelletier/go-toml v1.9.5
|
||||||
github.com/pion/stun/v3 v3.1.6
|
github.com/pion/stun/v3 v3.1.7
|
||||||
github.com/pires/go-proxyproto v0.15.0
|
github.com/pires/go-proxyproto v0.15.0
|
||||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
|
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/sagernet/sing v0.5.1
|
github.com/sagernet/sing v0.5.1
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.7
|
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/vishvananda/netlink v1.3.1
|
||||||
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f
|
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||||
golang.org/x/crypto v0.54.0
|
golang.org/x/crypto v0.55.0
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
||||||
golang.org/x/net v0.57.0
|
golang.org/x/net v0.58.0
|
||||||
golang.org/x/sync v0.22.0
|
golang.org/x/sync v0.22.0
|
||||||
golang.org/x/sys v0.47.0
|
golang.org/x/sys v0.47.0
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
||||||
golang.zx2c4.com/wireguard/windows v1.0.1
|
golang.zx2c4.com/wireguard/windows v1.0.1
|
||||||
google.golang.org/grpc v1.82.1
|
google.golang.org/grpc v1.83.1
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.12
|
||||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
|
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
|
||||||
h12.io/socks v1.0.3
|
h12.io/socks v1.0.3
|
||||||
lukechampine.com/blake3 v1.4.1
|
lukechampine.com/blake3 v1.4.1
|
||||||
@@ -40,23 +40,19 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
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/btree v1.1.2 // indirect
|
||||||
github.com/juju/ratelimit v1.0.2 // indirect
|
github.com/juju/ratelimit v1.0.2 // indirect
|
||||||
github.com/klauspost/compress v1.17.4 // indirect
|
github.com/klauspost/compress v1.17.4 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/pion/dtls/v3 v3.1.4 // indirect
|
github.com/pion/dtls/v3 v3.1.5 // indirect
|
||||||
github.com/pion/logging v0.2.4 // indirect
|
github.com/pion/logging v0.2.4 // indirect
|
||||||
github.com/pion/transport/v4 v4.0.2 // indirect
|
github.com/pion/transport/v4 v4.1.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/vishvananda/netns v0.0.5 // indirect
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
github.com/wlynxg/anet v0.0.5 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
golang.org/x/mod v0.37.0 // indirect
|
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||||
golang.org/x/text v0.40.0 // indirect
|
golang.org/x/text v0.41.0 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
golang.org/x/tools v0.47.0 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,11 +4,9 @@ github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkd
|
|||||||
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
|
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
|
||||||
github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY=
|
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/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 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
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=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
@@ -39,24 +37,22 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE=
|
||||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
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 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
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 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
|
||||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
|
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.5 h1:9xJtVsHwMYeSjPp5Hh1FTis4DchnQWtnOa5o+6ygqfc=
|
||||||
github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc=
|
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 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
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.7 h1:uRXMTlGLf89WgItGNyZ6aR5jMTX0NBbybXADpQCzn+E=
|
||||||
github.com/pion/stun/v3 v3.1.6/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs=
|
github.com/pion/stun/v3 v3.1.7/go.mod h1:Nq77RW4aRrSNrltf2ksUJLjxWeipj4lnlgdsYIxC8g8=
|
||||||
github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk=
|
github.com/pion/transport/v4 v4.1.0 h1:8S+nF2reM2cJuqC6g78OVy2BBgmbdns+acx3jA97BvQ=
|
||||||
github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
|
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 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI=
|
||||||
github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24=
|
github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
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/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
|
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
|
||||||
@@ -69,8 +65,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 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 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
|
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.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
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 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||||
@@ -82,34 +78,34 @@ 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=
|
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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
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.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
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 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
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 h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
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-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.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
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 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
|
||||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-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-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.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
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-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.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||||
@@ -127,15 +123,13 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
|
|||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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.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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
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 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
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-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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
|
||||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -147,20 +141,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=
|
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 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
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-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
|
||||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
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 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
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.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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
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 h1:Lk6hARj5UPY47dBep70OD/TIMwikJ5fGUGX0Rm3Xigk=
|
||||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
|
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
|
||||||
h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo=
|
h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo=
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) {
|
|||||||
LocalIP *StringList `json:"localIP"`
|
LocalIP *StringList `json:"localIP"`
|
||||||
LocalPort *PortList `json:"localPort"`
|
LocalPort *PortList `json:"localPort"`
|
||||||
Process *StringList `json:"process"`
|
Process *StringList `json:"process"`
|
||||||
|
LocalOS *StringList `json:"localOS"`
|
||||||
Webhook *WebhookRuleConfig `json:"webhook"`
|
Webhook *WebhookRuleConfig `json:"webhook"`
|
||||||
}
|
}
|
||||||
rawFieldRule := new(RawFieldRule)
|
rawFieldRule := new(RawFieldRule)
|
||||||
@@ -261,6 +262,10 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) {
|
|||||||
rule.Process = *rawFieldRule.Process
|
rule.Process = *rawFieldRule.Process
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rawFieldRule.LocalOS != nil && len(*rawFieldRule.LocalOS) > 0 {
|
||||||
|
rule.LocalOs = *rawFieldRule.LocalOS
|
||||||
|
}
|
||||||
|
|
||||||
if rawFieldRule.Webhook != nil && rawFieldRule.Webhook.URL != "" {
|
if rawFieldRule.Webhook != nil && rawFieldRule.Webhook.URL != "" {
|
||||||
rule.Webhook = &router.WebhookConfig{
|
rule.Webhook = &router.WebhookConfig{
|
||||||
Url: rawFieldRule.Webhook.URL,
|
Url: rawFieldRule.Webhook.URL,
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ type WireGuardConfig struct {
|
|||||||
MTU int32 `json:"mtu"`
|
MTU int32 `json:"mtu"`
|
||||||
Reserved []byte `json:"reserved"`
|
Reserved []byte `json:"reserved"`
|
||||||
DomainStrategy string `json:"domainStrategy"`
|
DomainStrategy string `json:"domainStrategy"`
|
||||||
|
DNS []string `json:"remoteDNS"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WireGuardConfig) Build() (proto.Message, error) {
|
func (c *WireGuardConfig) Build() (proto.Message, error) {
|
||||||
@@ -141,6 +142,7 @@ func (c *WireGuardConfig) Build() (proto.Message, error) {
|
|||||||
|
|
||||||
config.IsClient = c.IsClient
|
config.IsClient = c.IsClient
|
||||||
config.NoKernelTun = c.NoKernelTun
|
config.NoKernelTun = c.NoKernelTun
|
||||||
|
config.DNS = c.DNS
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
//go:build !linux && !openbsd
|
||||||
// +build !linux
|
// +build !linux,!openbsd
|
||||||
|
|
||||||
package dokodemo
|
package dokodemo
|
||||||
|
|
||||||
|
|||||||
+136
-5
@@ -11,6 +11,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/buf"
|
"github.com/xtls/xray-core/common/buf"
|
||||||
@@ -38,21 +40,111 @@ const (
|
|||||||
ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h
|
ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:linkname procyield runtime.procyield
|
|
||||||
func procyield(cycles uint32)
|
|
||||||
|
|
||||||
type DarwinTun struct {
|
type DarwinTun struct {
|
||||||
tunFile *os.File
|
tunFile *os.File
|
||||||
options *Config
|
options *Config
|
||||||
tunFd int
|
tunFd int
|
||||||
ownsFd bool // true for macOS (we created the fd), false for iOS (fd from system)
|
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
|
routeMonitor *os.File
|
||||||
routeMonitorOnce sync.Once
|
routeMonitorOnce sync.Once
|
||||||
systemRoutes []netip.Prefix
|
systemRoutes []netip.Prefix
|
||||||
gateway 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 (
|
var (
|
||||||
_ Tun = (*DarwinTun)(nil)
|
_ Tun = (*DarwinTun)(nil)
|
||||||
_ GVisorDevice = (*DarwinTun)(nil)
|
_ GVisorDevice = (*DarwinTun)(nil)
|
||||||
@@ -77,6 +169,7 @@ func NewTun(options *Config) (Tun, error) {
|
|||||||
options: options,
|
options: options,
|
||||||
tunFd: fd,
|
tunFd: fd,
|
||||||
ownsFd: false,
|
ownsFd: false,
|
||||||
|
waitKq: newWaitKqueue(fd),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +196,7 @@ func NewTun(options *Config) (Tun, error) {
|
|||||||
options: options,
|
options: options,
|
||||||
tunFd: int(tunFile.Fd()),
|
tunFd: int(tunFile.Fd()),
|
||||||
ownsFd: true,
|
ownsFd: true,
|
||||||
|
waitKq: newWaitKqueue(int(tunFile.Fd())),
|
||||||
gateway: gateway,
|
gateway: gateway,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -134,6 +228,9 @@ func (t *DarwinTun) Close() error {
|
|||||||
_ = t.routeMonitor.Close()
|
_ = t.routeMonitor.Close()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if t.waitKq != nil {
|
||||||
|
t.waitKq.close()
|
||||||
|
}
|
||||||
routeErr := t.unsetSystemRoutes()
|
routeErr := t.unsetSystemRoutes()
|
||||||
if t.ownsFd {
|
if t.ownsFd {
|
||||||
return xerrors.Combine(routeErr, t.tunFile.Close())
|
return xerrors.Combine(routeErr, t.tunFile.Close())
|
||||||
@@ -242,9 +339,43 @@ func (t *DarwinTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
|
|||||||
}), nil
|
}), 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() {
|
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) {
|
func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) {
|
||||||
|
|||||||
@@ -3,7 +3,11 @@
|
|||||||
package tun
|
package tun
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSelectDarwinGatewayDefault(t *testing.T) {
|
func TestSelectDarwinGatewayDefault(t *testing.T) {
|
||||||
@@ -47,3 +51,183 @@ func TestSelectDarwinGatewayRequiresUsableLocalAddress(t *testing.T) {
|
|||||||
t.Fatal("expected error")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+56
-17
@@ -5,9 +5,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
gonet "net"
|
gonet "net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
reflect "reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.zx2c4.com/wireguard/tun"
|
"golang.zx2c4.com/wireguard/tun"
|
||||||
|
|
||||||
@@ -30,6 +31,11 @@ import (
|
|||||||
"golang.zx2c4.com/wireguard/device"
|
"golang.zx2c4.com/wireguard/device"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type entry struct {
|
||||||
|
got []net.IP
|
||||||
|
time time.Time
|
||||||
|
}
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
conf *DeviceConfig
|
conf *DeviceConfig
|
||||||
policyManager policy.Manager
|
policyManager policy.Manager
|
||||||
@@ -43,6 +49,11 @@ type Handler struct {
|
|||||||
tnet *Net
|
tnet *Net
|
||||||
dev *device.Device
|
dev *device.Device
|
||||||
mu sync.Mutex
|
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) {
|
func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||||
@@ -98,6 +109,20 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
|||||||
return nil, err
|
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()
|
kernelTunSupported, err := KernelTunSupported()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errors.LogWarningInner(context.Background(), err, "Failed to check kernel TUN support")
|
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
|
var tnet *Net
|
||||||
if !conf.NoKernelTun && kernelTunSupported {
|
if !conf.NoKernelTun && kernelTunSupported {
|
||||||
errors.LogWarning(context.Background(), "Using kernel TUN")
|
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 {
|
} else {
|
||||||
errors.LogWarning(context.Background(), "Using gVisor TUN")
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -126,6 +151,9 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
|||||||
|
|
||||||
tun: tun,
|
tun: tun,
|
||||||
tnet: tnet,
|
tnet: tnet,
|
||||||
|
|
||||||
|
local: local,
|
||||||
|
cache: make(map[string]entry),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +166,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
|||||||
}
|
}
|
||||||
ob.Name = "wireguard"
|
ob.Name = "wireguard"
|
||||||
ob.CanSpliceCopy = 3
|
ob.CanSpliceCopy = 3
|
||||||
|
dialer.SetOutboundGateway(ctx, ob)
|
||||||
|
|
||||||
if err := h.init(ctx); err != nil {
|
if err := h.init(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -342,31 +371,34 @@ func (h *Handler) init(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) resolveLocal(host string) (net.IP, error) {
|
func (h *Handler) resolveLocal(host string) (net.IP, error) {
|
||||||
return resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, error) {
|
return h.resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, uint32, error) {
|
||||||
ips, _, err := h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
|
return h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
|
||||||
return ips, err
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) resolveRemote(host string) (net.IP, error) {
|
func (h *Handler) resolveRemote(host string) (net.IP, error) {
|
||||||
return resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, error) {
|
return h.resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, uint32, error) {
|
||||||
addrs, err := h.tnet.LookupHost(host)
|
if h.local {
|
||||||
if err != nil {
|
return h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
ips := make([]net.IP, 0, len(addrs))
|
return h.tnet.LookupHost(host)
|
||||||
for _, addr := range addrs {
|
|
||||||
ips = append(ips, net.ParseIP(addr))
|
|
||||||
}
|
|
||||||
return ips, nil
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
if ip := net.ParseIP(host); ip != nil {
|
||||||
return 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -406,6 +438,13 @@ func resolveDomain(host string, strategy DeviceConfig_DomainStrategy, lookupIP f
|
|||||||
if len(got) == 0 {
|
if len(got) == 0 {
|
||||||
return nil, dns.ErrEmptyResponse
|
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
|
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"`
|
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"`
|
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"`
|
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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@@ -261,6 +262,13 @@ func (x *DeviceConfig) GetNoKernelTun() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *DeviceConfig) GetDNS() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.DNS
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var File_proxy_wireguard_config_proto protoreflect.FileDescriptor
|
var File_proxy_wireguard_config_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
const file_proxy_wireguard_config_proto_rawDesc = "" +
|
const file_proxy_wireguard_config_proto_rawDesc = "" +
|
||||||
@@ -275,7 +283,7 @@ const file_proxy_wireguard_config_proto_rawDesc = "" +
|
|||||||
"\n" +
|
"\n" +
|
||||||
"keep_alive\x18\x04 \x01(\tR\tkeepAlive\x12\x1f\n" +
|
"keep_alive\x18\x04 \x01(\tR\tkeepAlive\x12\x1f\n" +
|
||||||
"\vallowed_ips\x18\x05 \x03(\tR\n" +
|
"\vallowed_ips\x18\x05 \x03(\tR\n" +
|
||||||
"allowedIps\"\xdc\x03\n" +
|
"allowedIps\"\xee\x03\n" +
|
||||||
"\fDeviceConfig\x12\x1d\n" +
|
"\fDeviceConfig\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"secret_key\x18\x01 \x01(\tR\tsecretKey\x12\x1a\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" +
|
"\breserved\x18\x06 \x01(\fR\breserved\x12Z\n" +
|
||||||
"\x0fdomain_strategy\x18\a \x01(\x0e21.xray.proxy.wireguard.DeviceConfig.DomainStrategyR\x0edomainStrategy\x12\x1b\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" +
|
"\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" +
|
"\x0eDomainStrategy\x12\f\n" +
|
||||||
"\bFORCE_IP\x10\x00\x12\r\n" +
|
"\bFORCE_IP\x10\x00\x12\r\n" +
|
||||||
"\tFORCE_IP4\x10\x01\x12\r\n" +
|
"\tFORCE_IP4\x10\x01\x12\r\n" +
|
||||||
|
|||||||
@@ -34,4 +34,5 @@ message DeviceConfig {
|
|||||||
DomainStrategy domain_strategy = 7;
|
DomainStrategy domain_strategy = 7;
|
||||||
bool is_client = 8;
|
bool is_client = 8;
|
||||||
bool no_kernel_tun = 9;
|
bool no_kernel_tun = 9;
|
||||||
|
repeated string DNS = 10;
|
||||||
}
|
}
|
||||||
@@ -248,7 +248,7 @@ var (
|
|||||||
errTimeout = errors.New("i/o timeout")
|
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)
|
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
|
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) {
|
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)
|
zlen := len(host)
|
||||||
if strings.IndexByte(host, ':') != -1 {
|
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 {
|
if ip, err := netip.ParseAddr(host[:zlen]); err == nil {
|
||||||
return []string{ip.String()}, nil
|
return []net.IP{ip.AsSlice()}, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isDomainName(host) {
|
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 {
|
type result struct {
|
||||||
p dnsmessage.Parser
|
p dnsmessage.Parser
|
||||||
@@ -611,6 +611,7 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
|||||||
lane <- result{p, server, err}
|
lane <- result{p, server, err}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
ttl := uint32(300)
|
||||||
for l := 0; l < lanes; l++ {
|
for l := 0; l < lanes; l++ {
|
||||||
result := <-lane
|
result := <-lane
|
||||||
if result.error != nil {
|
if result.error != nil {
|
||||||
@@ -644,6 +645,7 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
|||||||
}
|
}
|
||||||
break loop
|
break loop
|
||||||
}
|
}
|
||||||
|
ttl = min(ttl, h.TTL)
|
||||||
addrsV4 = append(addrsV4, netip.AddrFrom4(a.A))
|
addrsV4 = append(addrsV4, netip.AddrFrom4(a.A))
|
||||||
|
|
||||||
case dnsmessage.TypeAAAA:
|
case dnsmessage.TypeAAAA:
|
||||||
@@ -656,6 +658,7 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
|||||||
}
|
}
|
||||||
break loop
|
break loop
|
||||||
}
|
}
|
||||||
|
ttl = min(ttl, h.TTL)
|
||||||
addrsV6 = append(addrsV6, netip.AddrFrom16(aaaa.AAAA))
|
addrsV6 = append(addrsV6, netip.AddrFrom16(aaaa.AAAA))
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -680,11 +683,11 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(addrs) == 0 && lastErr != nil {
|
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 {
|
for _, ip := range addrs {
|
||||||
saddrs = append(saddrs, ip.String())
|
ips = append(ips, ip.AsSlice())
|
||||||
}
|
}
|
||||||
return saddrs, nil
|
return ips, ttl, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
//go:build openbsd
|
||||||
|
// +build openbsd
|
||||||
|
|
||||||
|
package internet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/xtls/xray-core/common/errors"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func applyOutboundSocketOptions(network string, address string, fd uintptr, config *SocketConfig) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) error {
|
||||||
|
if config.ReceiveOriginalDestAddress && isUDPSocket(network) {
|
||||||
|
// Only the options matching the socket's address family are accepted,
|
||||||
|
// so one of the two pairs succeeding is enough.
|
||||||
|
err6 := unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_RECVPKTINFO, 1)
|
||||||
|
if err6 == nil {
|
||||||
|
err6 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_RECVDSTPORT, 1)
|
||||||
|
}
|
||||||
|
err4 := unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_RECVDSTADDR, 1)
|
||||||
|
if err4 == nil {
|
||||||
|
err4 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_RECVDSTPORT, 1)
|
||||||
|
}
|
||||||
|
if err4 != nil && err6 != nil {
|
||||||
|
return errors.New("failed to enable receiving the original destination").Base(err4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setReuseAddr(fd uintptr) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setReusePort(fd uintptr) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
//go:build js || netbsd || openbsd || solaris
|
//go:build js || netbsd || solaris
|
||||||
// +build js netbsd openbsd solaris
|
// +build js netbsd solaris
|
||||||
|
|
||||||
package internet
|
package internet
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptrace"
|
"net/http/httptrace"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/apernet/quic-go/http3"
|
"github.com/apernet/quic-go/http3"
|
||||||
"github.com/xtls/xray-core/common"
|
"github.com/xtls/xray-core/common"
|
||||||
@@ -32,7 +33,7 @@ type DialerClient interface {
|
|||||||
type DefaultDialerClient struct {
|
type DefaultDialerClient struct {
|
||||||
transportConfig *Config
|
transportConfig *Config
|
||||||
client *http.Client
|
client *http.Client
|
||||||
closed bool
|
closed atomic.Bool
|
||||||
httpVersion string
|
httpVersion string
|
||||||
// pool of net.Conn, created using dialUploadConn
|
// pool of net.Conn, created using dialUploadConn
|
||||||
uploadRawPool *sync.Pool
|
uploadRawPool *sync.Pool
|
||||||
@@ -40,7 +41,7 @@ type DefaultDialerClient struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *DefaultDialerClient) IsClosed() bool {
|
func (c *DefaultDialerClient) IsClosed() bool {
|
||||||
return c.closed
|
return c.closed.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessionId string, body io.Reader, uploadOnly bool) (wrc io.ReadCloser, remoteAddr, localAddr net.Addr, err error) {
|
func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessionId string, body io.Reader, uploadOnly bool) (wrc io.ReadCloser, remoteAddr, localAddr net.Addr, err error) {
|
||||||
@@ -72,7 +73,7 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessio
|
|||||||
resp, err := c.client.Do(req)
|
resp, err := c.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !uploadOnly { // stream-down is enough
|
if !uploadOnly { // stream-down is enough
|
||||||
c.closed = true
|
c.closed.Store(true)
|
||||||
errors.LogInfoInner(ctx, err, "failed to "+method+" "+url)
|
errors.LogInfoInner(ctx, err, "failed to "+method+" "+url)
|
||||||
}
|
}
|
||||||
gotConn.Close()
|
gotConn.Close()
|
||||||
@@ -108,7 +109,7 @@ func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, sessio
|
|||||||
if c.httpVersion != "1.1" {
|
if c.httpVersion != "1.1" {
|
||||||
resp, err := c.client.Do(req)
|
resp, err := c.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.closed = true
|
c.closed.Store(true)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +149,7 @@ func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, sessio
|
|||||||
if h1UploadConn.UnreadedResponsesCount > 0 {
|
if h1UploadConn.UnreadedResponsesCount > 0 {
|
||||||
resp, err := http.ReadResponse(h1UploadConn.RespBufReader, req)
|
resp, err := http.ReadResponse(h1UploadConn.RespBufReader, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.closed = true
|
c.closed.Store(true)
|
||||||
return fmt.Errorf("error while reading response: %s", err.Error())
|
return fmt.Errorf("error while reading response: %s", err.Error())
|
||||||
}
|
}
|
||||||
io.Copy(io.Discard, resp.Body)
|
io.Copy(io.Discard, resp.Body)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package splithttp
|
package splithttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -330,14 +331,18 @@ func (c *Config) FillStreamRequest(request *http.Request, sessionId string, seqS
|
|||||||
func (c *Config) FillPacketRequest(request *http.Request, sessionId string, seqStr string, payload buf.MultiBuffer) error {
|
func (c *Config) FillPacketRequest(request *http.Request, sessionId string, seqStr string, payload buf.MultiBuffer) error {
|
||||||
dataPlacement := c.GetNormalizedUplinkDataPlacement()
|
dataPlacement := c.GetNormalizedUplinkDataPlacement()
|
||||||
|
|
||||||
|
data := make([]byte, payload.Len())
|
||||||
|
payload.Copy(data)
|
||||||
|
buf.ReleaseMulti(payload)
|
||||||
|
|
||||||
if dataPlacement == PlacementBody || dataPlacement == PlacementAuto {
|
if dataPlacement == PlacementBody || dataPlacement == PlacementAuto {
|
||||||
request.Header = c.GetRequestHeader()
|
request.Header = c.GetRequestHeader()
|
||||||
request.Body = io.NopCloser(&buf.MultiBufferContainer{MultiBuffer: payload})
|
request.Body = io.NopCloser(bytes.NewReader(data))
|
||||||
request.ContentLength = int64(payload.Len())
|
request.ContentLength = int64(len(data))
|
||||||
|
request.GetBody = func() (io.ReadCloser, error) {
|
||||||
|
return io.NopCloser(bytes.NewReader(data)), nil
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
data := make([]byte, payload.Len())
|
|
||||||
payload.Copy(data)
|
|
||||||
buf.ReleaseMulti(payload)
|
|
||||||
switch dataPlacement {
|
switch dataPlacement {
|
||||||
case PlacementHeader:
|
case PlacementHeader:
|
||||||
request.Header = c.GetRequestHeaderWithPayload(data)
|
request.Header = c.GetRequestHeaderWithPayload(data)
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package splithttp_test
|
package splithttp_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/xtls/xray-core/common"
|
||||||
|
"github.com/xtls/xray-core/common/buf"
|
||||||
. "github.com/xtls/xray-core/transport/internet/splithttp"
|
. "github.com/xtls/xray-core/transport/internet/splithttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -77,3 +81,35 @@ func Test_GetNormalizedPath(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_FillPacketRequest_GetBody(t *testing.T) {
|
||||||
|
data := []byte("hello xray")
|
||||||
|
payload := buf.MergeBytes(nil, data)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", "https://example.com/", nil)
|
||||||
|
common.Must(err)
|
||||||
|
|
||||||
|
config := &Config{}
|
||||||
|
config.FillPacketRequest(req, "sess", "0", payload)
|
||||||
|
|
||||||
|
if req.GetBody == nil {
|
||||||
|
t.Fatalf("Expected GetBody to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
first, err := io.ReadAll(req.Body)
|
||||||
|
common.Must(err)
|
||||||
|
|
||||||
|
if string(data) != string(first) {
|
||||||
|
t.Fatalf("Body mismatch. Format %q and %q are not equal", data, first)
|
||||||
|
}
|
||||||
|
|
||||||
|
body2, err := req.GetBody()
|
||||||
|
common.Must(err)
|
||||||
|
|
||||||
|
second, err := io.ReadAll(body2)
|
||||||
|
common.Must(err)
|
||||||
|
|
||||||
|
if string(data) != string(second) {
|
||||||
|
t.Fatalf("Replayed body mismatch. Format %q and %q are not equal", data, second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -595,11 +595,12 @@ func (w uploadWriter) Write(b []byte) (int, error) {
|
|||||||
|
|
||||||
var writed int
|
var writed int
|
||||||
for _, buff := range buffer.MultiBuffer {
|
for _, buff := range buffer.MultiBuffer {
|
||||||
|
n := int(buff.Len())
|
||||||
err := w.WriteMultiBuffer(buf.MultiBuffer{buff})
|
err := w.WriteMultiBuffer(buf.MultiBuffer{buff})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return writed, err
|
return writed, err
|
||||||
}
|
}
|
||||||
writed += int(buff.Len())
|
writed += n
|
||||||
}
|
}
|
||||||
return writed, nil
|
return writed, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
//go:build openbsd
|
||||||
|
// +build openbsd
|
||||||
|
|
||||||
|
package udp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common/net"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func retrieveOriginalDestFromControlMessages(msgs []unix.SocketControlMessage) net.Destination {
|
||||||
|
var ip []byte
|
||||||
|
var port uint16
|
||||||
|
var haveAddress bool
|
||||||
|
var havePort bool
|
||||||
|
|
||||||
|
for _, msg := range msgs {
|
||||||
|
switch msg.Header.Level {
|
||||||
|
case unix.IPPROTO_IP:
|
||||||
|
switch msg.Header.Type {
|
||||||
|
case unix.IP_RECVDSTADDR:
|
||||||
|
if len(msg.Data) < 4 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ip = append(ip[:0], msg.Data[:4]...)
|
||||||
|
haveAddress = true
|
||||||
|
case unix.IP_RECVDSTPORT:
|
||||||
|
if len(msg.Data) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
port = binary.BigEndian.Uint16(msg.Data[:2])
|
||||||
|
havePort = true
|
||||||
|
}
|
||||||
|
case unix.IPPROTO_IPV6:
|
||||||
|
switch msg.Header.Type {
|
||||||
|
case unix.IPV6_PKTINFO:
|
||||||
|
// struct in6_pktinfo: the destination address is followed by
|
||||||
|
// the interface index.
|
||||||
|
if len(msg.Data) < 16 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ip = append(ip[:0], msg.Data[:16]...)
|
||||||
|
haveAddress = true
|
||||||
|
case unix.IPV6_RECVDSTPORT:
|
||||||
|
if len(msg.Data) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
port = binary.BigEndian.Uint16(msg.Data[:2])
|
||||||
|
havePort = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !haveAddress || !havePort || port == 0 {
|
||||||
|
return net.Destination{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return net.UDPDestination(net.IPAddress(ip), net.Port(port))
|
||||||
|
}
|
||||||
|
|
||||||
|
func RetrieveOriginalDest(oob []byte) net.Destination {
|
||||||
|
msgs, err := unix.ParseSocketControlMessage(oob)
|
||||||
|
if err != nil {
|
||||||
|
return net.Destination{}
|
||||||
|
}
|
||||||
|
return retrieveOriginalDestFromControlMessages(msgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadUDPMsg(conn *net.UDPConn, payload []byte, oob []byte) (int, int, int, *net.UDPAddr, error) {
|
||||||
|
return conn.ReadMsgUDP(payload, oob)
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
//go:build openbsd
|
||||||
|
// +build openbsd
|
||||||
|
|
||||||
|
package udp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRetrieveOriginalDestFromControlMessages(t *testing.T) {
|
||||||
|
msgs := []unix.SocketControlMessage{
|
||||||
|
{
|
||||||
|
Header: unix.Cmsghdr{Level: unix.IPPROTO_IP, Type: unix.IP_RECVDSTPORT},
|
||||||
|
Data: []byte{0x30, 0x39},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: unix.Cmsghdr{Level: unix.IPPROTO_IP, Type: unix.IP_RECVDSTADDR},
|
||||||
|
Data: []byte{203, 0, 113, 7},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := retrieveOriginalDestFromControlMessages(msgs)
|
||||||
|
if !dest.IsValid() {
|
||||||
|
t.Fatal("destination is invalid")
|
||||||
|
}
|
||||||
|
if got, want := dest.Address.String(), "203.0.113.7"; got != want {
|
||||||
|
t.Fatalf("address = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := dest.Port.Value(), uint16(12345); got != want {
|
||||||
|
t.Fatalf("port = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetrieveOriginalDestFromControlMessagesIPv6(t *testing.T) {
|
||||||
|
// 2001:db8::7 followed by the interface index, as in struct in6_pktinfo.
|
||||||
|
pktinfo := []byte{
|
||||||
|
0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0x07,
|
||||||
|
0x02, 0, 0, 0,
|
||||||
|
}
|
||||||
|
msgs := []unix.SocketControlMessage{
|
||||||
|
{
|
||||||
|
Header: unix.Cmsghdr{Level: unix.IPPROTO_IPV6, Type: unix.IPV6_RECVDSTPORT},
|
||||||
|
Data: []byte{0x30, 0x39},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: unix.Cmsghdr{Level: unix.IPPROTO_IPV6, Type: unix.IPV6_PKTINFO},
|
||||||
|
Data: pktinfo,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := retrieveOriginalDestFromControlMessages(msgs)
|
||||||
|
if !dest.IsValid() {
|
||||||
|
t.Fatal("destination is invalid")
|
||||||
|
}
|
||||||
|
if got, want := dest.Address.IP().String(), "2001:db8::7"; got != want {
|
||||||
|
t.Fatalf("address = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := dest.Port.Value(), uint16(12345); got != want {
|
||||||
|
t.Fatalf("port = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetrieveOriginalDestRequiresAddressAndPort(t *testing.T) {
|
||||||
|
tests := [][]unix.SocketControlMessage{
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Header: unix.Cmsghdr{Level: unix.IPPROTO_IP, Type: unix.IP_RECVDSTADDR},
|
||||||
|
Data: []byte{203, 0, 113, 7},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Header: unix.Cmsghdr{Level: unix.IPPROTO_IP, Type: unix.IP_RECVDSTPORT},
|
||||||
|
Data: []byte{0x30, 0x39},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Header: unix.Cmsghdr{Level: unix.IPPROTO_IPV6, Type: unix.IPV6_PKTINFO},
|
||||||
|
Data: make([]byte, 20),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Header: unix.Cmsghdr{Level: unix.IPPROTO_IPV6, Type: unix.IPV6_RECVDSTPORT},
|
||||||
|
Data: []byte{0x30, 0x39},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, msgs := range tests {
|
||||||
|
if dest := retrieveOriginalDestFromControlMessages(msgs); dest.IsValid() {
|
||||||
|
t.Fatalf("unexpected destination: %v", dest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
//go:build !linux && !freebsd && !darwin
|
//go:build !linux && !freebsd && !darwin && !openbsd
|
||||||
// +build !linux,!freebsd,!darwin
|
// +build !linux,!freebsd,!darwin,!openbsd
|
||||||
|
|
||||||
package udp
|
package udp
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user