mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-09-16 14:30:28 +00:00
Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
521a9f2bca | ||
|
|
5e245b082e | ||
|
|
d9c54026c5 | ||
|
|
c1958dba04 | ||
|
|
540b9070f5 | ||
|
|
ada99a4eb0 | ||
|
|
65458e919f | ||
|
|
aa3d6589da | ||
|
|
25c11e2d2b | ||
|
|
dffc7ada5e | ||
|
|
77f98eba09 | ||
|
|
f124daf5a3 | ||
|
|
598bde7412 | ||
|
|
9b373e39ca | ||
|
|
c7e569b037 | ||
|
|
f02a357861 | ||
|
|
5fe6d6217a | ||
|
|
0604ffa957 | ||
|
|
d3f1a24285 | ||
|
|
2323273e37 | ||
|
|
09107b71dc | ||
|
|
7021606ad3 | ||
|
|
7d214f8b09 | ||
|
|
8b419d833d | ||
|
|
a12801c13b | ||
|
|
a000371b2a | ||
|
|
bc6e966af8 | ||
|
|
fc5620de98 | ||
|
|
b02bdcf4cc | ||
|
|
2b329b3675 |
@@ -82,7 +82,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
||||
@@ -78,6 +78,12 @@ func (o *Observer) background() {
|
||||
sleepTime = time.Duration(o.config.ProbeInterval)
|
||||
}
|
||||
|
||||
if len(outbounds) == 0 {
|
||||
errors.LogWarning(o.ctx, "no outbound matches subjectSelector ", o.config.SubjectSelector)
|
||||
time.Sleep(sleepTime)
|
||||
continue
|
||||
}
|
||||
|
||||
if !o.config.EnableConcurrency {
|
||||
sort.Strings(outbounds)
|
||||
for _, v := range outbounds {
|
||||
|
||||
@@ -347,7 +347,6 @@ func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound)
|
||||
// case addr.Family().IsDomain():
|
||||
default:
|
||||
ob.Gateway = addr
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ func (b *Balancer) SelectOutbounds() ([]string, error) {
|
||||
|
||||
// GetPrincipleTarget implements routing.BalancerPrincipleTarget
|
||||
func (r *Router) GetPrincipleTarget(tag string) ([]string, error) {
|
||||
if b, ok := r.balancers[tag]; ok {
|
||||
if b, ok := (*r.balancers.Load())[tag]; ok {
|
||||
if s, ok := b.strategy.(BalancingPrincipleTarget); ok {
|
||||
candidates, err := b.SelectOutbounds()
|
||||
if err != nil {
|
||||
@@ -151,7 +151,7 @@ func (r *Router) GetPrincipleTarget(tag string) ([]string, error) {
|
||||
|
||||
// SetOverrideTarget implements routing.BalancerOverrider
|
||||
func (r *Router) SetOverrideTarget(tag, target string) error {
|
||||
if b, ok := r.balancers[tag]; ok {
|
||||
if b, ok := (*r.balancers.Load())[tag]; ok {
|
||||
b.override.Put(target)
|
||||
return nil
|
||||
}
|
||||
@@ -160,7 +160,7 @@ func (r *Router) SetOverrideTarget(tag, target string) error {
|
||||
|
||||
// GetOverrideTarget implements routing.BalancerOverrider
|
||||
func (r *Router) GetOverrideTarget(tag string) (string, error) {
|
||||
if b, ok := r.balancers[tag]; ok {
|
||||
if b, ok := (*r.balancers.Load())[tag]; ok {
|
||||
return b.override.Get(), nil
|
||||
}
|
||||
return "", errors.New("cannot find tag")
|
||||
|
||||
@@ -2,25 +2,8 @@ package router
|
||||
|
||||
import (
|
||||
sync "sync"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
func (r *Router) OverrideBalancer(balancer string, target string) error {
|
||||
var b *Balancer
|
||||
for tag, bl := range r.balancers {
|
||||
if tag == balancer {
|
||||
b = bl
|
||||
break
|
||||
}
|
||||
}
|
||||
if b == nil {
|
||||
return errors.New("balancer '", balancer, "' not found")
|
||||
}
|
||||
b.override.Put(target)
|
||||
return nil
|
||||
}
|
||||
|
||||
type overrideSettings struct {
|
||||
target string
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
@@ -393,3 +394,22 @@ func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// LocalOSMatcher matches the operating system Xray itself is running on. That never
|
||||
// changes while Xray is running, so the result is resolved when the rule is built.
|
||||
type LocalOSMatcher struct {
|
||||
matched bool
|
||||
}
|
||||
|
||||
func NewLocalOSMatcher(names []string) *LocalOSMatcher {
|
||||
return &LocalOSMatcher{
|
||||
matched: slices.ContainsFunc(names, func(name string) bool {
|
||||
return strings.EqualFold(name, runtime.GOOS)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// Apply implements Condition.
|
||||
func (m *LocalOSMatcher) Apply(_ routing.Context) bool {
|
||||
return m.matched
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package router_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/xtls/xray-core/app/router"
|
||||
@@ -343,6 +345,31 @@ func TestChinaSites(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalOSRule(t *testing.T) {
|
||||
otherOS := "plan9"
|
||||
if runtime.GOOS == otherOS {
|
||||
otherOS = "linux"
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
localOS []string
|
||||
output bool
|
||||
}{
|
||||
{localOS: []string{runtime.GOOS}, output: true},
|
||||
{localOS: []string{otherOS}, output: false},
|
||||
{localOS: []string{otherOS, runtime.GOOS}, output: true},
|
||||
{localOS: []string{strings.ToUpper(runtime.GOOS)}, output: true},
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
cond, err := (&RoutingRule{LocalOs: test.localOS}).BuildCondition()
|
||||
common.Must(err)
|
||||
if got := cond.Apply(withBackground()); got != test.output {
|
||||
t.Errorf("for localOS %v on %s: expected %v, got %v", test.localOS, runtime.GOOS, test.output, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMphDomainMatcher(b *testing.B) {
|
||||
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
||||
rules, err := geodata.ParseDomainRules([]string{"geosite:cn"}, geodata.Domain_Substr)
|
||||
|
||||
@@ -33,6 +33,10 @@ func (r *Rule) Apply(ctx routing.Context) bool {
|
||||
func (rr *RoutingRule) BuildCondition() (Condition, error) {
|
||||
conds := NewConditionChan()
|
||||
|
||||
if len(rr.LocalOs) > 0 {
|
||||
conds.Add(NewLocalOSMatcher(rr.LocalOs))
|
||||
}
|
||||
|
||||
if len(rr.InboundTag) > 0 {
|
||||
conds.Add(NewInboundTagMatcher(rr.InboundTag))
|
||||
}
|
||||
|
||||
+14
-4
@@ -107,8 +107,10 @@ type RoutingRule struct {
|
||||
VlessRouteList *net.PortList `protobuf:"bytes,20,opt,name=vless_route_list,json=vlessRouteList,proto3" json:"vless_route_list,omitempty"`
|
||||
Process []string `protobuf:"bytes,21,rep,name=process,proto3" json:"process,omitempty"`
|
||||
Webhook *WebhookConfig `protobuf:"bytes,22,opt,name=webhook,proto3" json:"webhook,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
// List of operating systems for matching the one Xray itself is running on.
|
||||
LocalOs []string `protobuf:"bytes,23,rep,name=local_os,json=localOs,proto3" json:"local_os,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RoutingRule) Reset() {
|
||||
@@ -278,6 +280,13 @@ func (x *RoutingRule) GetWebhook() *WebhookConfig {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *RoutingRule) GetLocalOs() []string {
|
||||
if x != nil {
|
||||
return x.LocalOs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isRoutingRule_TargetTag interface {
|
||||
isRoutingRule_TargetTag()
|
||||
}
|
||||
@@ -637,7 +646,7 @@ var File_app_router_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_app_router_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x17app/router/config.proto\x12\x0fxray.app.router\x1a!common/serial/typed_message.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\x1a\x1bcommon/geodata/geodat.proto\"\xc1\a\n" +
|
||||
"\x17app/router/config.proto\x12\x0fxray.app.router\x1a!common/serial/typed_message.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\x1a\x1bcommon/geodata/geodat.proto\"\xdc\a\n" +
|
||||
"\vRoutingRule\x12\x12\n" +
|
||||
"\x03tag\x18\x01 \x01(\tH\x00R\x03tag\x12%\n" +
|
||||
"\rbalancing_tag\x18\f \x01(\tH\x00R\fbalancingTag\x12\x19\n" +
|
||||
@@ -661,7 +670,8 @@ const file_app_router_config_proto_rawDesc = "" +
|
||||
"\x0flocal_port_list\x18\x12 \x01(\v2\x19.xray.common.net.PortListR\rlocalPortList\x12C\n" +
|
||||
"\x10vless_route_list\x18\x14 \x01(\v2\x19.xray.common.net.PortListR\x0evlessRouteList\x12\x18\n" +
|
||||
"\aprocess\x18\x15 \x03(\tR\aprocess\x128\n" +
|
||||
"\awebhook\x18\x16 \x01(\v2\x1e.xray.app.router.WebhookConfigR\awebhook\x1a=\n" +
|
||||
"\awebhook\x18\x16 \x01(\v2\x1e.xray.app.router.WebhookConfigR\awebhook\x12\x19\n" +
|
||||
"\blocal_os\x18\x17 \x03(\tR\alocalOs\x1a=\n" +
|
||||
"\x0fAttributesEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\f\n" +
|
||||
|
||||
@@ -56,6 +56,9 @@ message RoutingRule {
|
||||
|
||||
repeated string process = 21;
|
||||
WebhookConfig webhook = 22;
|
||||
|
||||
// List of operating systems for matching the one Xray itself is running on.
|
||||
repeated string local_os = 23;
|
||||
}
|
||||
|
||||
message WebhookConfig {
|
||||
|
||||
+59
-114
@@ -2,7 +2,9 @@ package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
@@ -17,8 +19,8 @@ import (
|
||||
// Router is an implementation of routing.Router.
|
||||
type Router struct {
|
||||
domainStrategy Config_DomainStrategy
|
||||
rules []*Rule
|
||||
balancers map[string]*Balancer
|
||||
rules atomic.Pointer[[]*Rule]
|
||||
balancers atomic.Pointer[map[string]*Balancer]
|
||||
dns dns.Client
|
||||
|
||||
ctx context.Context
|
||||
@@ -43,52 +45,9 @@ func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm out
|
||||
r.ohm = ohm
|
||||
r.dispatcher = dispatcher
|
||||
|
||||
r.balancers = make(map[string]*Balancer, len(config.BalancingRule))
|
||||
for _, rule := range config.BalancingRule {
|
||||
balancer, err := rule.Build(ohm, dispatcher)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
balancer.InjectContext(ctx)
|
||||
r.balancers[rule.Tag] = balancer
|
||||
}
|
||||
|
||||
r.rules = make([]*Rule, 0, len(config.Rule))
|
||||
for _, rule := range config.Rule {
|
||||
cond, err := rule.BuildCondition()
|
||||
if err != nil {
|
||||
r.closeWebhooks()
|
||||
return err
|
||||
}
|
||||
rr := &Rule{
|
||||
Condition: cond,
|
||||
Tag: rule.GetTag(),
|
||||
RuleTag: rule.GetRuleTag(),
|
||||
}
|
||||
if wh := rule.GetWebhook(); wh != nil {
|
||||
notifier, err := NewWebhookNotifier(wh)
|
||||
if err != nil {
|
||||
r.closeWebhooks()
|
||||
return err
|
||||
}
|
||||
rr.Webhook = notifier
|
||||
}
|
||||
btag := rule.GetBalancingTag()
|
||||
if len(btag) > 0 {
|
||||
brule, found := r.balancers[btag]
|
||||
if !found {
|
||||
if rr.Webhook != nil {
|
||||
rr.Webhook.Close()
|
||||
}
|
||||
r.closeWebhooks()
|
||||
return errors.New("balancer ", btag, " not found")
|
||||
}
|
||||
rr.Balancer = brule
|
||||
}
|
||||
r.rules = append(r.rules, rr)
|
||||
}
|
||||
|
||||
return nil
|
||||
r.rules.Store(new([]*Rule))
|
||||
r.balancers.Store(&map[string]*Balancer{})
|
||||
return r.ReloadRules(config, false)
|
||||
}
|
||||
|
||||
// PickRoute implements routing.Router.
|
||||
@@ -124,18 +83,22 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if !shouldAppend {
|
||||
for _, rule := range r.rules {
|
||||
if rule.Webhook != nil {
|
||||
rule.Webhook.Close()
|
||||
}
|
||||
oldRules := *r.rules.Load()
|
||||
oldBalancers := *r.balancers.Load()
|
||||
|
||||
var newRules []*Rule
|
||||
newBalancers := make(map[string]*Balancer)
|
||||
existTags := make(map[string]bool, len(oldRules)+len(config.Rule))
|
||||
if shouldAppend {
|
||||
newRules = append(newRules, oldRules...)
|
||||
maps.Copy(newBalancers, oldBalancers)
|
||||
for _, rule := range oldRules {
|
||||
existTags[rule.RuleTag] = true
|
||||
}
|
||||
r.balancers = make(map[string]*Balancer, len(config.BalancingRule))
|
||||
r.rules = make([]*Rule, 0, len(config.Rule))
|
||||
}
|
||||
|
||||
for _, rule := range config.BalancingRule {
|
||||
_, found := r.balancers[rule.Tag]
|
||||
if found {
|
||||
if _, found := newBalancers[rule.Tag]; found {
|
||||
return errors.New("duplicate balancer tag")
|
||||
}
|
||||
balancer, err := rule.Build(r.ohm, r.dispatcher)
|
||||
@@ -143,27 +106,12 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
|
||||
return err
|
||||
}
|
||||
balancer.InjectContext(r.ctx)
|
||||
r.balancers[rule.Tag] = balancer
|
||||
}
|
||||
|
||||
startIdx := len(r.rules)
|
||||
closeNewWebhooks := func() {
|
||||
for i := startIdx; i < len(r.rules); i++ {
|
||||
if r.rules[i].Webhook != nil {
|
||||
r.rules[i].Webhook.Close()
|
||||
}
|
||||
}
|
||||
r.rules = r.rules[:startIdx]
|
||||
newBalancers[rule.Tag] = balancer
|
||||
}
|
||||
|
||||
for _, rule := range config.Rule {
|
||||
if r.RuleExists(rule.GetRuleTag()) {
|
||||
closeNewWebhooks()
|
||||
return errors.New("duplicate ruleTag ", rule.GetRuleTag())
|
||||
}
|
||||
cond, err := rule.BuildCondition()
|
||||
if err != nil {
|
||||
closeNewWebhooks()
|
||||
return err
|
||||
}
|
||||
rr := &Rule{
|
||||
@@ -171,69 +119,64 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
|
||||
Tag: rule.GetTag(),
|
||||
RuleTag: rule.GetRuleTag(),
|
||||
}
|
||||
if rr.RuleTag != "" && existTags[rr.RuleTag] {
|
||||
return errors.New("duplicate ruleTag ", rr.RuleTag)
|
||||
}
|
||||
existTags[rr.RuleTag] = true
|
||||
if wh := rule.GetWebhook(); wh != nil {
|
||||
notifier, err := NewWebhookNotifier(wh)
|
||||
if err != nil {
|
||||
closeNewWebhooks()
|
||||
return err
|
||||
}
|
||||
rr.Webhook = notifier
|
||||
}
|
||||
btag := rule.GetBalancingTag()
|
||||
if len(btag) > 0 {
|
||||
brule, found := r.balancers[btag]
|
||||
if btag := rule.GetBalancingTag(); len(btag) > 0 {
|
||||
brule, found := newBalancers[btag]
|
||||
if !found {
|
||||
if rr.Webhook != nil {
|
||||
rr.Webhook.Close()
|
||||
}
|
||||
closeNewWebhooks()
|
||||
return errors.New("balancer ", btag, " not found")
|
||||
}
|
||||
rr.Balancer = brule
|
||||
}
|
||||
r.rules = append(r.rules, rr)
|
||||
newRules = append(newRules, rr)
|
||||
}
|
||||
|
||||
r.balancers.Store(&newBalancers)
|
||||
r.rules.Store(&newRules)
|
||||
if !shouldAppend {
|
||||
closeWebhooks(oldRules)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) RuleExists(tag string) bool {
|
||||
if tag != "" {
|
||||
for _, rule := range r.rules {
|
||||
if rule.RuleTag == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RemoveRule implements routing.Router.
|
||||
func (r *Router) RemoveRule(tag string) error {
|
||||
if tag == "" {
|
||||
return errors.New("empty tag name!")
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
newRules := []*Rule{}
|
||||
if tag != "" {
|
||||
for _, rule := range r.rules {
|
||||
if rule.RuleTag != tag {
|
||||
newRules = append(newRules, rule)
|
||||
} else if rule.Webhook != nil {
|
||||
rule.Webhook.Close()
|
||||
}
|
||||
oldRules := *r.rules.Load()
|
||||
newRules := make([]*Rule, 0, len(oldRules))
|
||||
var removed []*Rule
|
||||
for _, rule := range oldRules {
|
||||
if rule.RuleTag != tag {
|
||||
newRules = append(newRules, rule)
|
||||
} else {
|
||||
removed = append(removed, rule)
|
||||
}
|
||||
r.rules = newRules
|
||||
return nil
|
||||
}
|
||||
return errors.New("empty tag name!")
|
||||
r.rules.Store(&newRules)
|
||||
closeWebhooks(removed)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListRule implements routing.Router
|
||||
func (r *Router) ListRule() []routing.Route {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
ruleList := make([]routing.Route, 0)
|
||||
for _, rule := range r.rules {
|
||||
rules := *r.rules.Load()
|
||||
ruleList := make([]routing.Route, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
ruleList = append(ruleList, &Route{
|
||||
outboundTag: rule.Tag,
|
||||
ruleTag: rule.RuleTag,
|
||||
@@ -252,7 +195,9 @@ func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context,
|
||||
ctx = routing_dns.ContextWithDNSClient(ctx, r.dns)
|
||||
}
|
||||
|
||||
for _, rule := range r.rules {
|
||||
rules := *r.rules.Load()
|
||||
|
||||
for _, rule := range rules {
|
||||
if rule.Apply(ctx) {
|
||||
return rule, ctx, nil
|
||||
}
|
||||
@@ -265,7 +210,7 @@ func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context,
|
||||
ctx = routing_dns.ContextWithDNSClient(ctx, r.dns)
|
||||
|
||||
// Try applying rules again if we have IPs.
|
||||
for _, rule := range r.rules {
|
||||
for _, rule := range rules {
|
||||
if rule.Apply(ctx) {
|
||||
return rule, ctx, nil
|
||||
}
|
||||
@@ -279,9 +224,9 @@ func (r *Router) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// closeWebhooks closes all webhook notifiers in the current rule set.
|
||||
func (r *Router) closeWebhooks() {
|
||||
for _, rule := range r.rules {
|
||||
// closeWebhooks closes all webhook notifiers in the given rule set.
|
||||
func closeWebhooks(rules []*Rule) {
|
||||
for _, rule := range rules {
|
||||
if rule.Webhook != nil {
|
||||
rule.Webhook.Close()
|
||||
}
|
||||
@@ -292,7 +237,7 @@ func (r *Router) closeWebhooks() {
|
||||
func (r *Router) Close() error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.closeWebhooks()
|
||||
closeWebhooks(*r.rules.Load())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+17
-23
@@ -8,6 +8,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
@@ -40,6 +41,7 @@ type WebhookNotifier struct {
|
||||
deduplication uint32
|
||||
client *http.Client
|
||||
seen sync.Map
|
||||
lastSweep atomic.Int64
|
||||
done chan struct{}
|
||||
wg sync.WaitGroup
|
||||
closeOnce sync.Once
|
||||
@@ -77,11 +79,6 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if h.deduplication > 0 {
|
||||
h.wg.Add(1)
|
||||
go h.cleanupLoop()
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
@@ -201,6 +198,7 @@ func (h *WebhookNotifier) isDuplicate(email string) bool {
|
||||
}
|
||||
ttl := time.Duration(h.deduplication) * time.Second
|
||||
now := time.Now()
|
||||
h.maybeSweep(now, ttl)
|
||||
if v, loaded := h.seen.LoadOrStore(email, now); loaded {
|
||||
if now.Sub(v.(time.Time)) < ttl {
|
||||
return true
|
||||
@@ -210,27 +208,23 @@ func (h *WebhookNotifier) isDuplicate(email string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *WebhookNotifier) cleanupLoop() {
|
||||
defer h.wg.Done()
|
||||
ttl := time.Duration(h.deduplication) * time.Second
|
||||
ticker := time.NewTicker(ttl)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-h.done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
now := time.Now()
|
||||
h.seen.Range(func(key, value any) bool {
|
||||
if now.Sub(value.(time.Time)) >= ttl {
|
||||
h.seen.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
func (h *WebhookNotifier) maybeSweep(now time.Time, ttl time.Duration) {
|
||||
last := h.lastSweep.Load()
|
||||
if now.UnixNano()-last < int64(ttl) {
|
||||
return
|
||||
}
|
||||
if !h.lastSweep.CompareAndSwap(last, now.UnixNano()) {
|
||||
return // another goroutine did the sweep
|
||||
}
|
||||
h.seen.Range(func(key, value any) bool {
|
||||
if now.Sub(value.(time.Time)) >= ttl {
|
||||
h.seen.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Only need to call if the Notifier is really used, otherwise GC can clean it
|
||||
func (h *WebhookNotifier) Close() error {
|
||||
h.closeOnce.Do(func() {
|
||||
close(h.done)
|
||||
|
||||
@@ -198,7 +198,9 @@ func darwinSocketInfoMatchLevel(info []byte, network string, srcAddr netip.Addr,
|
||||
|
||||
vflag := info[darwinInSockInfoVFlagOff]
|
||||
if srcAddr.Is4() {
|
||||
if family != unix.AF_INET || vflag&darwinInSockInfoIPv4 == 0 {
|
||||
// Dual-stack sockets expose IPv4-mapped connections as AF_INET6
|
||||
// while marking the endpoint as IPv4 in ini_vflag.
|
||||
if (family != unix.AF_INET && family != unix.AF_INET6) || vflag&darwinInSockInfoIPv4 == 0 {
|
||||
return darwinSocketNoMatch
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -52,6 +52,57 @@ func TestFindProcessDarwinTCP(t *testing.T) {
|
||||
assertCurrentProcess(t, pid, name, path)
|
||||
}
|
||||
|
||||
func TestFindProcessDarwinTCPIPv4Mapped(t *testing.T) {
|
||||
listener, err := stdnet.Listen("tcp4", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
accepted := make(chan stdnet.Conn, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err == nil {
|
||||
accepted <- conn
|
||||
return
|
||||
}
|
||||
close(accepted)
|
||||
}()
|
||||
|
||||
listenerAddr := listener.Addr().(*stdnet.TCPAddr)
|
||||
fd, err := unix.Socket(unix.AF_INET6, unix.SOCK_STREAM, unix.IPPROTO_TCP)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
|
||||
mappedAddr := [16]byte{10: 0xff, 11: 0xff, 12: 127, 15: 1}
|
||||
if err := unix.Connect(fd, &unix.SockaddrInet6{
|
||||
Port: listenerAddr.Port,
|
||||
Addr: mappedAddr,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
serverConn := <-accepted
|
||||
if serverConn == nil {
|
||||
t.Fatal("server did not accept tcp connection")
|
||||
}
|
||||
defer serverConn.Close()
|
||||
|
||||
local, err := unix.Getsockname(fd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
localPort := local.(*unix.SockaddrInet6).Port
|
||||
|
||||
pid, name, path, err := FindProcess("tcp", "127.0.0.1", uint16(localPort), "127.0.0.1", uint16(listenerAddr.Port))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertCurrentProcess(t, pid, name, path)
|
||||
}
|
||||
|
||||
func TestFindProcessDarwinUDP(t *testing.T) {
|
||||
conn, err := stdnet.ListenUDP("udp", &stdnet.UDPAddr{IP: stdnet.ParseIP("127.0.0.1")})
|
||||
if err != nil {
|
||||
@@ -264,6 +315,18 @@ func TestDarwinSocketInfoMatchLevelFallbacks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDarwinSocketInfoMatchLevelIPv4Mapped(t *testing.T) {
|
||||
src := netip.MustParseAddr("127.0.0.1")
|
||||
dst := netip.MustParseAddr("203.0.113.10")
|
||||
info := newDarwinSocketInfo("tcp", src, 12345, dst, 443)
|
||||
writeDarwinNativeUint32(info, darwinSocketInfoFamilyOff, uint32(unix.AF_INET6))
|
||||
|
||||
level := darwinSocketInfoMatchLevel(info, "tcp", src, 12345, dst, 443, true)
|
||||
if level != darwinSocketExactMatch {
|
||||
t.Fatalf("unexpected match level: got %d, want %d", level, darwinSocketExactMatch)
|
||||
}
|
||||
}
|
||||
|
||||
func newDarwinSocketInfo(network string, local netip.Addr, localPort uint16, remote netip.Addr, remotePort uint16) []byte {
|
||||
info := make([]byte, darwinSocketFDInfoSize)
|
||||
switch network {
|
||||
|
||||
@@ -3,11 +3,8 @@ package bittorrent
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
)
|
||||
|
||||
type SniffHeader struct{}
|
||||
@@ -39,50 +36,44 @@ func SniffUTP(b []byte) (*SniffHeader, error) {
|
||||
return nil, common.ErrNoClue
|
||||
}
|
||||
|
||||
buffer := buf.FromBytes(b)
|
||||
|
||||
var typeAndVersion uint8
|
||||
|
||||
if binary.Read(buffer, binary.BigEndian, &typeAndVersion) != nil {
|
||||
return nil, common.ErrNoClue
|
||||
} else if b[0]>>4&0xF > 4 || b[0]&0xF != 1 {
|
||||
// type 4 (ST_SYN), version 1
|
||||
if b[0] != 0x41 {
|
||||
return nil, errNotBittorrent
|
||||
}
|
||||
|
||||
var extension uint8
|
||||
|
||||
if binary.Read(buffer, binary.BigEndian, &extension) != nil {
|
||||
return nil, common.ErrNoClue
|
||||
} else if extension != 0 && extension != 1 {
|
||||
// timestamp_difference is always 0 in new connections
|
||||
if binary.BigEndian.Uint32(b[8:12]) != 0 {
|
||||
return nil, errNotBittorrent
|
||||
}
|
||||
|
||||
// Walk the extension chain. Selective ack (1) and extension bits (2)
|
||||
extension, offset := b[1], 20
|
||||
for extension != 0 {
|
||||
if extension != 1 {
|
||||
if len(b) < offset+2 {
|
||||
return nil, errNotBittorrent
|
||||
}
|
||||
if binary.Read(buffer, binary.BigEndian, &extension) != nil {
|
||||
return nil, common.ErrNoClue
|
||||
length := int(b[offset+1])
|
||||
switch extension {
|
||||
case 1: // selective ack
|
||||
if length < 4 || length%4 != 0 {
|
||||
return nil, errNotBittorrent
|
||||
}
|
||||
case 2: // extension bits: fixed 8 bytes, sent in ST_SYN by µTorrent
|
||||
if length != 8 {
|
||||
return nil, errNotBittorrent
|
||||
}
|
||||
default:
|
||||
return nil, errNotBittorrent
|
||||
}
|
||||
|
||||
var length uint8
|
||||
if err := binary.Read(buffer, binary.BigEndian, &length); err != nil {
|
||||
return nil, common.ErrNoClue
|
||||
}
|
||||
if common.Error2(buffer.ReadBytes(int32(length))) != nil {
|
||||
return nil, common.ErrNoClue
|
||||
if len(b) < offset+2+length {
|
||||
return nil, errNotBittorrent
|
||||
}
|
||||
extension = b[offset]
|
||||
offset += 2 + length
|
||||
}
|
||||
|
||||
if common.Error2(buffer.ReadBytes(2)) != nil {
|
||||
return nil, common.ErrNoClue
|
||||
}
|
||||
|
||||
var timestamp uint32
|
||||
if err := binary.Read(buffer, binary.BigEndian, ×tamp); err != nil {
|
||||
return nil, common.ErrNoClue
|
||||
}
|
||||
if math.Abs(float64(time.Now().UnixMicro()-int64(timestamp))) > float64(24*time.Hour) {
|
||||
// extensions should consume all ST_SYN payload
|
||||
if len(b) != offset {
|
||||
return nil, errNotBittorrent
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package bittorrent
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
)
|
||||
|
||||
// utpPacket builds the fixed 20-byte header defined by BEP 29.
|
||||
func utpPacket(packetType, extension byte, tsDiff uint32, payload ...byte) []byte {
|
||||
b := make([]byte, 20)
|
||||
b[0] = packetType<<4 | 1
|
||||
b[1] = extension
|
||||
binary.BigEndian.PutUint16(b[2:4], 0x4a3f) // connection_id, random
|
||||
binary.BigEndian.PutUint32(b[4:8], 0x8c3a91d2) // timestamp_microseconds, sender's clock
|
||||
binary.BigEndian.PutUint32(b[8:12], tsDiff)
|
||||
binary.BigEndian.PutUint32(b[12:16], 0x00100000) // wnd_size
|
||||
binary.BigEndian.PutUint16(b[16:18], 0x71ee) // seq_nr, random in libutp/libtorrent
|
||||
binary.BigEndian.PutUint16(b[18:20], 0x0000) // ack_nr
|
||||
return append(b, payload...)
|
||||
}
|
||||
|
||||
func TestSniffUTP(t *testing.T) {
|
||||
selectiveAck := []byte{0, 4, 0xff, 0x00, 0xff, 0x00}
|
||||
wrongVersion := utpPacket(4, 0, 0)
|
||||
wrongVersion[0] = 4<<4 | 2
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
payload []byte
|
||||
err error
|
||||
}{
|
||||
{"syn", utpPacket(4, 0, 0), nil},
|
||||
{"syn with selective ack", append(utpPacket(4, 1, 0), selectiveAck...), nil},
|
||||
{"syn with extension bits", append(utpPacket(4, 2, 0), 0, 8, 1, 2, 3, 4, 5, 6, 7, 8), nil},
|
||||
{"extension bits with wrong length", append(utpPacket(4, 2, 0), 0, 4, 1, 2, 3, 4), errNotBittorrent},
|
||||
{"syn with nonzero timestamp_difference", utpPacket(4, 0, 0x1234), errNotBittorrent},
|
||||
{"syn with trailing payload", utpPacket(4, 0, 0, 'x'), errNotBittorrent},
|
||||
// txid 0x4100, no EDNS0: the worst case colliding with the uTP header
|
||||
{"dns query", []byte{
|
||||
0x41, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x01, 'a', 0x02, 'c', 'o', 0x00, 0x00, 0x01, 0x00, 0x01,
|
||||
}, errNotBittorrent},
|
||||
{"established connection packets", utpPacket(0, 0, 0x5678, 'x', 'y', 'z'), errNotBittorrent},
|
||||
{"state", utpPacket(2, 0, 0x5678), errNotBittorrent},
|
||||
{"fin", utpPacket(1, 0, 0x5678), errNotBittorrent},
|
||||
{"wrong version", wrongVersion, errNotBittorrent},
|
||||
{"unknown packet type", utpPacket(5, 0, 0), errNotBittorrent},
|
||||
{"unknown extension", utpPacket(4, 2, 0), errNotBittorrent},
|
||||
{"extension chain past the datagram", utpPacket(4, 1, 0, 0, 8, 0xff), errNotBittorrent},
|
||||
{"selective ack not in multiples of 4", append(utpPacket(4, 1, 0), 0, 3, 0xff, 0x00, 0xff), errNotBittorrent},
|
||||
{"shorter than the header", utpPacket(4, 0, 0)[:19], common.ErrNoClue},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
h, err := SniffUTP(c.payload)
|
||||
if err != c.err {
|
||||
t.Fatalf("expected error %v, got %v", c.err, err)
|
||||
}
|
||||
if err == nil && h == nil {
|
||||
t.Fatal("expected a sniff header, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,10 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/cipher"
|
||||
_ "crypto/tls"
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
type CipherSuiteTLS13 struct {
|
||||
ID uint16
|
||||
KeyLen int
|
||||
AEAD func(key, fixedNonce []byte) cipher.AEAD
|
||||
Hash crypto.Hash
|
||||
}
|
||||
|
||||
//go:linkname AEADAESGCMTLS13 crypto/tls.aeadAESGCMTLS13
|
||||
func AEADAESGCMTLS13(key, nonceMask []byte) cipher.AEAD
|
||||
|
||||
@@ -3,7 +3,6 @@ package quic
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/aes"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
@@ -28,22 +27,43 @@ func (s SniffHeader) Domain() string {
|
||||
return s.domain
|
||||
}
|
||||
|
||||
const (
|
||||
versionDraft29 uint32 = 0xff00001d
|
||||
version1 uint32 = 0x1
|
||||
var (
|
||||
errNotQUIC = errors.New("not quic")
|
||||
errNotQUICInitial = errors.New("not initial packet")
|
||||
)
|
||||
|
||||
type quicVersionSpec struct {
|
||||
ver uint32
|
||||
typeInitial byte
|
||||
initialSalt []byte
|
||||
labelPrefix string
|
||||
}
|
||||
|
||||
var (
|
||||
quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99}
|
||||
quicSalt = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a}
|
||||
initialSuite = &CipherSuiteTLS13{
|
||||
ID: tls.TLS_AES_128_GCM_SHA256,
|
||||
KeyLen: 16,
|
||||
AEAD: AEADAESGCMTLS13,
|
||||
Hash: crypto.SHA256,
|
||||
quicDraft29 = quicVersionSpec{
|
||||
ver: 0xff00001d,
|
||||
typeInitial: 0b00,
|
||||
initialSalt: []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99},
|
||||
labelPrefix: "quic",
|
||||
}
|
||||
quicV1 = quicVersionSpec{
|
||||
ver: 0x1,
|
||||
typeInitial: 0b00,
|
||||
initialSalt: []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a},
|
||||
labelPrefix: "quic",
|
||||
}
|
||||
quicV2 = quicVersionSpec{
|
||||
ver: 0x6b3343cf,
|
||||
typeInitial: 0b01,
|
||||
initialSalt: []byte{0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9},
|
||||
labelPrefix: "quicv2",
|
||||
}
|
||||
|
||||
quicVersionSpecMap = map[uint32]*quicVersionSpec{
|
||||
quicDraft29.ver: &quicDraft29,
|
||||
quicV1.ver: &quicV1,
|
||||
quicV2.ver: &quicV2,
|
||||
}
|
||||
errNotQuic = errors.New("not quic")
|
||||
errNotQuicInitial = errors.New("not initial packet")
|
||||
)
|
||||
|
||||
func SniffQUIC(b []byte) (*SniffHeader, error) {
|
||||
@@ -63,60 +83,61 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
|
||||
buffer := buf.FromBytes(b)
|
||||
typeByte, err := buffer.ReadByte()
|
||||
if err != nil {
|
||||
return nil, errNotQuic
|
||||
return nil, errNotQUIC
|
||||
}
|
||||
|
||||
isLongHeader := typeByte&0x80 > 0
|
||||
if !isLongHeader || typeByte&0x40 == 0 {
|
||||
return nil, errNotQuicInitial
|
||||
return nil, errNotQUICInitial
|
||||
}
|
||||
|
||||
vb, err := buffer.ReadBytes(4)
|
||||
if err != nil {
|
||||
return nil, errNotQuic
|
||||
return nil, errNotQUIC
|
||||
}
|
||||
|
||||
versionNumber := binary.BigEndian.Uint32(vb)
|
||||
if versionNumber != 0 && typeByte&0x40 == 0 {
|
||||
return nil, errNotQuic
|
||||
} else if versionNumber != versionDraft29 && versionNumber != version1 {
|
||||
return nil, errNotQuic
|
||||
var s *quicVersionSpec
|
||||
if v, ok := quicVersionSpecMap[versionNumber]; ok {
|
||||
s = v
|
||||
} else {
|
||||
return nil, errNotQUIC
|
||||
}
|
||||
|
||||
packetType := (typeByte & 0x30) >> 4
|
||||
isQuicInitial := packetType == 0x0
|
||||
|
||||
var destConnID []byte
|
||||
if l, err := buffer.ReadByte(); err != nil {
|
||||
return nil, errNotQuic
|
||||
return nil, errNotQUIC
|
||||
} else if destConnID, err = buffer.ReadBytes(int32(l)); err != nil {
|
||||
return nil, errNotQuic
|
||||
return nil, errNotQUIC
|
||||
}
|
||||
|
||||
if l, err := buffer.ReadByte(); err != nil {
|
||||
return nil, errNotQuic
|
||||
return nil, errNotQUIC
|
||||
} else if common.Error2(buffer.ReadBytes(int32(l))) != nil {
|
||||
return nil, errNotQuic
|
||||
return nil, errNotQUIC
|
||||
}
|
||||
|
||||
if isQuicInitial { // Only initial packets have token, see https://datatracker.ietf.org/doc/html/rfc9000#section-17.2.2
|
||||
tokenLen, err := readShortQuicVarint(buffer)
|
||||
packetType := (typeByte & 0x30) >> 4
|
||||
isQUICInitial := packetType == s.typeInitial
|
||||
|
||||
if isQUICInitial { // Only initial packets have token, see https://datatracker.ietf.org/doc/html/rfc9000#section-17.2.2
|
||||
tokenLen, err := readShortQUICVarint(buffer)
|
||||
if err != nil || tokenLen > int32(len(b)) {
|
||||
return nil, errNotQuic
|
||||
return nil, errNotQUIC
|
||||
}
|
||||
|
||||
if _, err = buffer.ReadBytes(tokenLen); err != nil {
|
||||
return nil, errNotQuic
|
||||
return nil, errNotQUIC
|
||||
}
|
||||
}
|
||||
|
||||
packetLen, err := readShortQuicVarint(buffer)
|
||||
packetLen, err := readShortQUICVarint(buffer)
|
||||
if err != nil {
|
||||
return nil, errNotQuic
|
||||
return nil, errNotQUIC
|
||||
}
|
||||
// packetLen is impossible to be shorter than this
|
||||
if packetLen < 4 {
|
||||
return nil, errNotQuic
|
||||
return nil, errNotQUIC
|
||||
}
|
||||
|
||||
hdrLen := len(b) - int(buffer.Len())
|
||||
@@ -125,26 +146,22 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
|
||||
}
|
||||
|
||||
restPayload := b[hdrLen+int(packetLen):]
|
||||
if !isQuicInitial { // Skip this packet if it's not initial packet
|
||||
if !isQUICInitial { // Skip this packet if it's not initial packet
|
||||
b = restPayload
|
||||
continue
|
||||
}
|
||||
|
||||
var salt []byte
|
||||
if versionNumber == version1 {
|
||||
salt = quicSalt
|
||||
} else {
|
||||
salt = quicSaltOld
|
||||
}
|
||||
salt := s.initialSalt
|
||||
label := s.labelPrefix
|
||||
initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt)
|
||||
secret := hkdfExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size())
|
||||
hpKey := hkdfExpandLabel(initialSuite.Hash, secret, []byte{}, "quic hp", initialSuite.KeyLen)
|
||||
secret := hkdfExpandLabel(initialSecret, "client in", crypto.SHA256.Size())
|
||||
hpKey := hkdfExpandLabel(secret, label+" hp", 16)
|
||||
block, err := aes.NewCipher(hpKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(b) < hdrLen+4+block.BlockSize() {
|
||||
return nil, errNotQuic
|
||||
return nil, errNotQUIC
|
||||
}
|
||||
cache.Clear()
|
||||
mask := cache.Extend(int32(block.BlockSize()))
|
||||
@@ -155,8 +172,8 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
|
||||
b[hdrLen+i] ^= mask[i+1]
|
||||
}
|
||||
|
||||
key := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16)
|
||||
iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12)
|
||||
key := hkdfExpandLabel(secret, label+" key", 16)
|
||||
iv := hkdfExpandLabel(secret, label+" iv", 12)
|
||||
cipher := AEADAESGCMTLS13(key, iv)
|
||||
|
||||
nonce := cache.Extend(int32(cipher.NonceSize()))
|
||||
@@ -181,44 +198,44 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
|
||||
case 0x00: // PADDING frame
|
||||
case 0x01: // PING frame
|
||||
case 0x02, 0x03: // ACK frame
|
||||
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Largest Acknowledged
|
||||
if _, err = readShortQUICVarint(buffer); err != nil { // Field: Largest Acknowledged
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Delay
|
||||
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Delay
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
ackRangeCount, err := readShortQuicVarint(buffer) // Field: ACK Range Count
|
||||
ackRangeCount, err := readShortQUICVarint(buffer) // Field: ACK Range Count
|
||||
if err != nil {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err = readShortQuicVarint(buffer); err != nil { // Field: First ACK Range
|
||||
if _, err = readShortQUICVarint(buffer); err != nil { // Field: First ACK Range
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
for i := 0; i < int(ackRangeCount); i++ { // Field: ACK Range
|
||||
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Range -> Gap
|
||||
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Range -> Gap
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Range -> ACK Range Length
|
||||
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Range -> ACK Range Length
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
}
|
||||
if frameType == 0x03 {
|
||||
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ECN Counts -> ECT0 Count
|
||||
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ECN Counts -> ECT0 Count
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ECN Counts -> ECT1 Count
|
||||
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ECN Counts -> ECT1 Count
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err = readShortQuicVarint(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count
|
||||
if _, err = readShortQUICVarint(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
}
|
||||
case 0x06: // CRYPTO frame, we will use this frame
|
||||
offset, err := readShortQuicVarint(buffer) // Field: Offset
|
||||
offset, err := readShortQUICVarint(buffer) // Field: Offset
|
||||
if err != nil {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
length, err := readShortQuicVarint(buffer) // Field: Length
|
||||
length, err := readShortQUICVarint(buffer) // Field: Length
|
||||
if err != nil || length > buffer.Len() {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
@@ -234,13 +251,13 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet
|
||||
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Error Code
|
||||
if _, err = readShortQUICVarint(buffer); err != nil { // Field: Error Code
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Frame Type
|
||||
if _, err = readShortQUICVarint(buffer); err != nil { // Field: Frame Type
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
length, err := readShortQuicVarint(buffer) // Field: Reason Phrase Length
|
||||
length, err := readShortQUICVarint(buffer) // Field: Reason Phrase Length
|
||||
if err != nil {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
@@ -250,7 +267,7 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
|
||||
default:
|
||||
// Only above frame types are permitted in initial packet.
|
||||
// See https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2.2-8
|
||||
return nil, errNotQuicInitial
|
||||
return nil, errNotQUICInitial
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,35 +285,33 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
|
||||
return nil, protocol.ErrProtoNeedMoreData
|
||||
}
|
||||
|
||||
func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte {
|
||||
b := make([]byte, 3, 3+6+len(label)+1+len(context))
|
||||
binary.BigEndian.PutUint16(b, uint16(length))
|
||||
b[2] = uint8(6 + len(label))
|
||||
b = append(b, []byte("tls13 ")...)
|
||||
b = append(b, []byte(label)...)
|
||||
b = b[:3+6+len(label)+1]
|
||||
b[3+6+len(label)] = uint8(len(context))
|
||||
b = append(b, context...)
|
||||
func hkdfExpandLabel(secret []byte, label string, length int) []byte {
|
||||
b := make([]byte, 0, 2+1+6+len(label)+1)
|
||||
b = binary.BigEndian.AppendUint16(b, uint16(length))
|
||||
b = append(b, byte(6+len(label)))
|
||||
b = append(b, "tls13 "...)
|
||||
b = append(b, label...)
|
||||
b = append(b, 0) // context
|
||||
|
||||
out := make([]byte, length)
|
||||
n, err := hkdf.Expand(hash.New, secret, b).Read(out)
|
||||
n, err := hkdf.Expand(crypto.SHA256.New, secret, b).Read(out)
|
||||
if err != nil || n != length {
|
||||
panic("quic: HKDF-Expand-Label invocation failed unexpectedly")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// readShortQuicVarint wraps quicvarint.Read with a max limit for length related fields.
|
||||
// readShortQUICVarint wraps quicvarint.Read with a max limit for length related fields.
|
||||
// we only handle QUIC Initial so these numbers should not exceed 65535
|
||||
// returns int32 to reduce type conversion
|
||||
func readShortQuicVarint(reader io.ByteReader) (int32, error) {
|
||||
func readShortQUICVarint(reader io.ByteReader) (int32, error) {
|
||||
v, err := quicvarint.Read(reader)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if v > 65535 {
|
||||
// not used(
|
||||
return 0, errNotQuicInitial
|
||||
return 0, errNotQUICInitial
|
||||
}
|
||||
return int32(v), nil
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -207,6 +207,7 @@ func getConfig() string {
|
||||
"tag": "XHTTP_IN",
|
||||
"streamSettings": {
|
||||
"network": "xhttp",
|
||||
"security": "tls",
|
||||
"xhttpSettings": {
|
||||
"host": "bing.com",
|
||||
"path": "/xhttp_client_upload",
|
||||
|
||||
@@ -3,36 +3,37 @@ module github.com/xtls/xray-core
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716
|
||||
github.com/cloudflare/circl v1.6.4
|
||||
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
|
||||
github.com/cloudflare/circl v1.6.5
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
|
||||
github.com/golang/mock v1.7.0-rc.1
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/klauspost/cpuid/v2 v2.4.0
|
||||
github.com/miekg/dns v1.1.72
|
||||
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138
|
||||
github.com/miekg/dns v1.1.73
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/pion/stun/v3 v3.1.6
|
||||
github.com/pion/stun/v3 v3.1.7
|
||||
github.com/pires/go-proxyproto v0.15.0
|
||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sagernet/sing v0.5.1
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/crypto v0.54.0
|
||||
golang.org/x/crypto v0.55.0
|
||||
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/sys v0.47.0
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1
|
||||
google.golang.org/grpc v1.82.1
|
||||
google.golang.org/protobuf v1.36.11
|
||||
google.golang.org/grpc v1.83.1
|
||||
google.golang.org/protobuf v1.36.12
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
|
||||
h12.io/socks v1.0.3
|
||||
lukechampine.com/blake3 v1.4.1
|
||||
@@ -40,23 +41,24 @@ require (
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/gopacket v1.1.19 // indirect
|
||||
github.com/huin/goupnp v1.2.0 // indirect
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
|
||||
github.com/juju/ratelimit v1.0.2 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/koron/go-ssdp v0.0.4 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/pion/dtls/v3 v3.1.4 // indirect
|
||||
github.com/libp2p/go-netroute v0.2.1 // indirect
|
||||
github.com/pion/dtls/v3 v3.1.5 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/transport/v4 v4.0.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/pion/transport/v4 v4.1.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/mod v0.37.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.47.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkdYbw5JPGwBqIHs2J8tiEP7Py9lPqkN2I=
|
||||
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
|
||||
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg=
|
||||
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U=
|
||||
github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY=
|
||||
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
|
||||
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
@@ -23,40 +21,52 @@ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364 h1:5XxdakFhqd9dnXoAZy1Mb2R/DZ6D1e+0bGC/JhucGYI=
|
||||
github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c=
|
||||
github.com/huin/goupnp v1.2.0 h1:uOKW26NG1hsSSbXIZ1IR7XP9Gjd1U8pnLaCMgntmkmY=
|
||||
github.com/huin/goupnp v1.2.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
|
||||
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
|
||||
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138 h1:YohuNPT/1k3VcThCQlBZ43PCPWPfMRS1zcxWBF2SLK8=
|
||||
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138/go.mod h1:TXQg5tfSy+bUjnhT5728j5j/MBj7keIYqqZ1+8k/ui8=
|
||||
github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU=
|
||||
github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ=
|
||||
github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE=
|
||||
github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
|
||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
|
||||
github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY=
|
||||
github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc=
|
||||
github.com/pion/dtls/v3 v3.1.5 h1:9xJtVsHwMYeSjPp5Hh1FTis4DchnQWtnOa5o+6ygqfc=
|
||||
github.com/pion/dtls/v3 v3.1.5/go.mod h1:gz1K4jg6c+fq86oQMH4pilpCEOEPwmEr2jY+VcF/mkU=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/stun/v3 v3.1.6 h1:WnhsD0eHCiwCfKNkVx0VJJwr2Y3eV4Ueih3KJ+dfZy8=
|
||||
github.com/pion/stun/v3 v3.1.6/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs=
|
||||
github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk=
|
||||
github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
|
||||
github.com/pion/stun/v3 v3.1.7 h1:uRXMTlGLf89WgItGNyZ6aR5jMTX0NBbybXADpQCzn+E=
|
||||
github.com/pion/stun/v3 v3.1.7/go.mod h1:Nq77RW4aRrSNrltf2ksUJLjxWeipj4lnlgdsYIxC8g8=
|
||||
github.com/pion/transport/v4 v4.1.0 h1:8S+nF2reM2cJuqC6g78OVy2BBgmbdns+acx3jA97BvQ=
|
||||
github.com/pion/transport/v4 v4.1.0/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
|
||||
github.com/pires/go-proxyproto v0.15.0 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI=
|
||||
github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24=
|
||||
github.com/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/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
|
||||
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
|
||||
@@ -69,8 +79,8 @@ github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
|
||||
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||
@@ -82,34 +92,36 @@ github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt7
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
@@ -127,15 +139,14 @@ 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.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.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -147,20 +158,18 @@ golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
|
||||
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h1:Lk6hARj5UPY47dBep70OD/TIMwikJ5fGUGX0Rm3Xigk=
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
|
||||
h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo=
|
||||
|
||||
@@ -65,6 +65,9 @@ func (v *Address) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
|
||||
func (v *Address) Build() *net.IPOrDomain {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
return net.NewIPOrDomain(v.Address)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ func (o *ObservatoryConfig) Build() (proto.Message, error) {
|
||||
type BurstObservatoryConfig struct {
|
||||
SubjectSelector []string `json:"subjectSelector"`
|
||||
// health check settings
|
||||
HealthCheck *healthCheckSettings `json:"pingConfig,omitempty"`
|
||||
HealthCheck *HealthCheckSettings `json:"pingConfig,omitempty"`
|
||||
}
|
||||
|
||||
func (b BurstObservatoryConfig) Build() (proto.Message, error) {
|
||||
|
||||
@@ -148,6 +148,7 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) {
|
||||
LocalIP *StringList `json:"localIP"`
|
||||
LocalPort *PortList `json:"localPort"`
|
||||
Process *StringList `json:"process"`
|
||||
LocalOS *StringList `json:"localOS"`
|
||||
Webhook *WebhookRuleConfig `json:"webhook"`
|
||||
}
|
||||
rawFieldRule := new(RawFieldRule)
|
||||
@@ -261,6 +262,10 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) {
|
||||
rule.Process = *rawFieldRule.Process
|
||||
}
|
||||
|
||||
if rawFieldRule.LocalOS != nil && len(*rawFieldRule.LocalOS) > 0 {
|
||||
rule.LocalOs = *rawFieldRule.LocalOS
|
||||
}
|
||||
|
||||
if rawFieldRule.Webhook != nil && rawFieldRule.Webhook.URL != "" {
|
||||
rule.Webhook = &router.WebhookConfig{
|
||||
Url: rawFieldRule.Webhook.URL,
|
||||
|
||||
@@ -43,8 +43,8 @@ type strategyLeastLoadConfig struct {
|
||||
Tolerance float64 `json:"tolerance,omitempty"`
|
||||
}
|
||||
|
||||
// healthCheckSettings holds settings for health Checker
|
||||
type healthCheckSettings struct {
|
||||
// HealthCheckSettings holds settings for health Checker
|
||||
type HealthCheckSettings struct {
|
||||
Destination string `json:"destination"`
|
||||
Connectivity string `json:"connectivity"`
|
||||
Interval duration.Duration `json:"interval"`
|
||||
@@ -53,7 +53,7 @@ type healthCheckSettings struct {
|
||||
HttpMethod string `json:"httpMethod"`
|
||||
}
|
||||
|
||||
func (h healthCheckSettings) Build() (proto.Message, error) {
|
||||
func (h HealthCheckSettings) Build() (proto.Message, error) {
|
||||
var httpMethod string
|
||||
if h.HttpMethod == "" {
|
||||
httpMethod = "HEAD"
|
||||
|
||||
@@ -816,9 +816,11 @@ func (c *Xicmp) Build() (proto.Message, error) {
|
||||
}
|
||||
|
||||
type Realm struct {
|
||||
Url string `json:"url"`
|
||||
StunServers []string `json:"stunServers"`
|
||||
TlsConfig *TLSConfig `json:"tlsConfig"`
|
||||
Url string `json:"url"`
|
||||
StunServers []string `json:"stunServers"`
|
||||
TlsConfig *TLSConfig `json:"tlsConfig"`
|
||||
IPMode string `json:"ipMode"`
|
||||
PortMapping *realm.PortMapping `json:"portMapping"`
|
||||
}
|
||||
|
||||
func (c *Realm) Build() (proto.Message, error) {
|
||||
@@ -898,6 +900,8 @@ func (c *Realm) Build() (proto.Message, error) {
|
||||
ID: id,
|
||||
StunServers: stunServers,
|
||||
TlsConfig: tlsConfig,
|
||||
IPMode: strings.ToLower(c.IPMode),
|
||||
PortMapping: c.PortMapping,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -928,20 +932,24 @@ func (c *Mask) Build(tcp bool) (proto.Message, error) {
|
||||
}
|
||||
|
||||
type QuicParamsConfig struct {
|
||||
Congestion string `json:"congestion"`
|
||||
Debug bool `json:"debug"`
|
||||
BbrProfile string `json:"bbrProfile"`
|
||||
BrutalUp Bandwidth `json:"brutalUp"`
|
||||
BrutalDown Bandwidth `json:"brutalDown"`
|
||||
UdpHop UdpHop `json:"udpHop"`
|
||||
InitStreamReceiveWindow uint64 `json:"initStreamReceiveWindow"`
|
||||
MaxStreamReceiveWindow uint64 `json:"maxStreamReceiveWindow"`
|
||||
InitConnectionReceiveWindow uint64 `json:"initConnectionReceiveWindow"`
|
||||
MaxConnectionReceiveWindow uint64 `json:"maxConnectionReceiveWindow"`
|
||||
MaxIdleTimeout int64 `json:"maxIdleTimeout"`
|
||||
KeepAlivePeriod int64 `json:"keepAlivePeriod"`
|
||||
DisablePathMTUDiscovery bool `json:"disablePathMTUDiscovery"`
|
||||
MaxIncomingStreams int64 `json:"maxIncomingStreams"`
|
||||
Congestion string `json:"congestion"`
|
||||
Debug bool `json:"debug"`
|
||||
BbrProfile string `json:"bbrProfile"`
|
||||
BrutalUp Bandwidth `json:"brutalUp"`
|
||||
BrutalDown Bandwidth `json:"brutalDown"`
|
||||
BrutalDisableLossCompensation bool `json:"brutalDisableLossCompensation"`
|
||||
UdpHop UdpHop `json:"udpHop"`
|
||||
InitStreamReceiveWindow uint64 `json:"initStreamReceiveWindow"`
|
||||
MaxStreamReceiveWindow uint64 `json:"maxStreamReceiveWindow"`
|
||||
InitConnectionReceiveWindow uint64 `json:"initConnectionReceiveWindow"`
|
||||
MaxConnectionReceiveWindow uint64 `json:"maxConnectionReceiveWindow"`
|
||||
MaxIdleTimeout int64 `json:"maxIdleTimeout"`
|
||||
KeepAlivePeriod int64 `json:"keepAlivePeriod"`
|
||||
DisablePathMTUDiscovery bool `json:"disablePathMTUDiscovery"`
|
||||
DisableChromeParrot bool `json:"disableChromeParrot"`
|
||||
DisableGSO bool `json:"disableGSO"`
|
||||
MaxIncomingStreams int64 `json:"maxIncomingStreams"`
|
||||
DisableStatelessReset bool `json:"disableStatelessReset"`
|
||||
}
|
||||
|
||||
type FinalMask struct {
|
||||
|
||||
@@ -285,10 +285,11 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
|
||||
}
|
||||
|
||||
config.QuicParams = &internet.QuicParams{
|
||||
Congestion: c.FinalMask.QuicParams.Congestion,
|
||||
BbrProfile: profile,
|
||||
BrutalUp: up,
|
||||
BrutalDown: down,
|
||||
Congestion: c.FinalMask.QuicParams.Congestion,
|
||||
BbrProfile: profile,
|
||||
BrutalUp: up,
|
||||
BrutalDown: down,
|
||||
BrutalDisableLossCompensation: c.FinalMask.QuicParams.BrutalDisableLossCompensation,
|
||||
UdpHop: &internet.UdpHop{
|
||||
Ports: c.FinalMask.QuicParams.UdpHop.PortList.Build().Ports(),
|
||||
IntervalMin: int64(c.FinalMask.QuicParams.UdpHop.Interval.From),
|
||||
@@ -301,7 +302,10 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
|
||||
MaxIdleTimeout: c.FinalMask.QuicParams.MaxIdleTimeout,
|
||||
KeepAlivePeriod: c.FinalMask.QuicParams.KeepAlivePeriod,
|
||||
DisablePathMtuDiscovery: c.FinalMask.QuicParams.DisablePathMTUDiscovery,
|
||||
DisableChromeParrot: c.FinalMask.QuicParams.DisableChromeParrot,
|
||||
DisableGSO: c.FinalMask.QuicParams.DisableGSO,
|
||||
MaxIncomingStreams: c.FinalMask.QuicParams.MaxIncomingStreams,
|
||||
DisableStatelessReset: c.FinalMask.QuicParams.DisableStatelessReset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -751,6 +751,7 @@ type Masquerade struct {
|
||||
|
||||
Url string `json:"url"`
|
||||
RewriteHost bool `json:"rewriteHost"`
|
||||
XForwarded bool `json:"xForwarded"`
|
||||
Insecure bool `json:"insecure"`
|
||||
|
||||
Content string `json:"content"`
|
||||
@@ -791,6 +792,7 @@ func (c *HysteriaConfig) Build() (proto.Message, error) {
|
||||
config.MasqFile = c.Masquerade.Dir
|
||||
config.MasqUrl = c.Masquerade.Url
|
||||
config.MasqUrlRewriteHost = c.Masquerade.RewriteHost
|
||||
config.MasqUrlXForwarded = c.Masquerade.XForwarded
|
||||
config.MasqUrlInsecure = c.Masquerade.Insecure
|
||||
config.MasqString = c.Masquerade.Content
|
||||
config.MasqStringHeaders = c.Masquerade.Headers
|
||||
|
||||
@@ -66,6 +66,7 @@ type WireGuardConfig struct {
|
||||
MTU int32 `json:"mtu"`
|
||||
Reserved []byte `json:"reserved"`
|
||||
DomainStrategy string `json:"domainStrategy"`
|
||||
DNS []string `json:"remoteDNS"`
|
||||
}
|
||||
|
||||
func (c *WireGuardConfig) Build() (proto.Message, error) {
|
||||
@@ -141,6 +142,7 @@ func (c *WireGuardConfig) Build() (proto.Message, error) {
|
||||
|
||||
config.IsClient = c.IsClient
|
||||
config.NoKernelTun = c.NoKernelTun
|
||||
config.DNS = c.DNS
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
+6
-6
@@ -140,7 +140,7 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
|
||||
// TUN inbound doesn't need port configuration as it uses network interface instead
|
||||
if strings.ToLower(c.Protocol) == "tun" {
|
||||
// Skip port validation for TUN
|
||||
} else if c.ListenOn == nil {
|
||||
} else if c.ListenOn == nil || len(c.ListenOn.String()) == 0 {
|
||||
// Listen on anyip, must set PortList
|
||||
if c.PortList == nil {
|
||||
return nil, errors.New("Listen on AnyIP but no Port(s) set in InboundDetour.")
|
||||
@@ -251,13 +251,13 @@ func validateOutboundTransportSecurity(rawConfig interface{}, senderSettings *pr
|
||||
if vlessCfg.Encryption != "" && vlessCfg.Encryption != "none" {
|
||||
return nil
|
||||
}
|
||||
if requiresTransportSecurity(vlessCfg.Address) {
|
||||
if requiresTransportSecurity(vlessCfg.Vnext[0].Address) {
|
||||
return errors.New("vless without TLS or other encryption is prohibited unless the server address is a private IP or domain")
|
||||
}
|
||||
}
|
||||
|
||||
if tjCfg, ok := rawConfig.(*TrojanClientConfig); ok {
|
||||
if requiresTransportSecurity(tjCfg.Address) {
|
||||
if requiresTransportSecurity(tjCfg.Servers[0].Address) {
|
||||
return errors.New("trojan without TLS is prohibited unless the server address is a private IP or domain")
|
||||
}
|
||||
}
|
||||
@@ -358,13 +358,13 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to load outbound detour config for protocol ", c.Protocol).Base(err)
|
||||
}
|
||||
if err := validateOutboundTransportSecurity(rawConfig, senderSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ts, err := rawConfig.(Buildable).Build()
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to build outbound handler for protocol ", c.Protocol).Base(err)
|
||||
}
|
||||
if err := validateOutboundTransportSecurity(rawConfig, senderSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &core.OutboundHandlerConfig{
|
||||
SenderSettings: serial.ToTypedMessage(senderSettings),
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
//go:build openbsd
|
||||
// +build openbsd
|
||||
|
||||
package dokodemo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func FakeUDP(addr *net.UDPAddr, mark int) (net.PacketConn, error) {
|
||||
domain := unix.AF_INET6
|
||||
var sockaddr unix.Sockaddr
|
||||
if ip4 := addr.IP.To4(); ip4 != nil {
|
||||
domain = unix.AF_INET
|
||||
sa := &unix.SockaddrInet4{Port: addr.Port}
|
||||
copy(sa.Addr[:], ip4)
|
||||
sockaddr = sa
|
||||
} else if ip6 := addr.IP.To16(); ip6 != nil {
|
||||
sa := &unix.SockaddrInet6{Port: addr.Port}
|
||||
copy(sa.Addr[:], ip6)
|
||||
if addr.Zone != "" {
|
||||
iface, err := net.InterfaceByName(addr.Zone)
|
||||
if err != nil {
|
||||
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("resolve zone %s: %w", addr.Zone, err)}
|
||||
}
|
||||
sa.ZoneId = uint32(iface.Index)
|
||||
}
|
||||
sockaddr = sa
|
||||
} else {
|
||||
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("unsupported address %v", addr.IP)}
|
||||
}
|
||||
|
||||
fd, err := unix.Socket(domain, unix.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("socket open: %w", err)}
|
||||
}
|
||||
|
||||
closeFD := true
|
||||
defer func() {
|
||||
if closeFD {
|
||||
unix.Close(fd)
|
||||
}
|
||||
}()
|
||||
|
||||
if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_BINDANY, 1); err != nil {
|
||||
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("set socket option SO_BINDANY: %w", err)}
|
||||
}
|
||||
if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1); err != nil {
|
||||
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("set socket option SO_REUSEADDR: %w", err)}
|
||||
}
|
||||
// Several client sessions can be answered from the same original
|
||||
// destination at the same time, so the address has to be shareable.
|
||||
if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
|
||||
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("set socket option SO_REUSEPORT: %w", err)}
|
||||
}
|
||||
|
||||
if err = unix.Bind(fd, sockaddr); err != nil {
|
||||
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("bind %s: %w", addr.String(), err)}
|
||||
}
|
||||
|
||||
fdFile := os.NewFile(uintptr(fd), fmt.Sprintf("net-udp-bindany-%s", addr.String()))
|
||||
if fdFile == nil {
|
||||
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("convert descriptor to file")}
|
||||
}
|
||||
defer fdFile.Close()
|
||||
|
||||
packetConn, err := net.FilePacketConn(fdFile)
|
||||
if err != nil {
|
||||
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("convert descriptor to packet connection: %w", err)}
|
||||
}
|
||||
|
||||
closeFD = false
|
||||
return packetConn, nil
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
//go:build !linux && !openbsd
|
||||
// +build !linux,!openbsd
|
||||
|
||||
package dokodemo
|
||||
|
||||
|
||||
@@ -173,15 +173,18 @@ func fillRequestHeader(ctx context.Context, header []*Header) ([]*Header, error)
|
||||
outbounds := session.OutboundsFromContext(ctx)
|
||||
ob := outbounds[len(outbounds)-1]
|
||||
|
||||
if inbound == nil || ob == nil {
|
||||
return nil, errors.New("missing inbound or outbound metadata from context")
|
||||
var src net.Destination
|
||||
if inbound != nil {
|
||||
src = inbound.Source
|
||||
} else {
|
||||
src = net.TCPDestination(net.AnyIP, 0)
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Source net.Destination
|
||||
Target net.Destination
|
||||
}{
|
||||
Source: inbound.Source,
|
||||
Source: src,
|
||||
Target: ob.Target,
|
||||
}
|
||||
|
||||
|
||||
+136
-5
@@ -11,6 +11,8 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
@@ -38,21 +40,111 @@ const (
|
||||
ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h
|
||||
)
|
||||
|
||||
//go:linkname procyield runtime.procyield
|
||||
func procyield(cycles uint32)
|
||||
|
||||
type DarwinTun struct {
|
||||
tunFile *os.File
|
||||
options *Config
|
||||
tunFd int
|
||||
ownsFd bool // true for macOS (we created the fd), false for iOS (fd from system)
|
||||
|
||||
// Genuinely blocks Wait() until tunFd is readable, instead of the
|
||||
// previous procyield-only busy-spin (dispatchLoop in
|
||||
// stack_gvisor_endpoint.go calls ReadPacket() then Wait() in a tight
|
||||
// loop with no other throttling whenever the queue is empty -- with
|
||||
// only procyield(1), that pins a full CPU core for as long as the
|
||||
// tunnel is up, observed causing severe device heating/thermal
|
||||
// shutdown). nil if kqueue setup failed, in which case Wait() falls
|
||||
// back to a bounded time.Sleep instead. See waitKqueue's own doc
|
||||
// comment for why this is a dedicated type rather than a bare fd.
|
||||
waitKq *waitKqueue
|
||||
|
||||
routeMonitor *os.File
|
||||
routeMonitorOnce sync.Once
|
||||
systemRoutes []netip.Prefix
|
||||
gateway netip.Prefix
|
||||
}
|
||||
|
||||
// waitKqueue owns a kqueue fd used by DarwinTun.Wait() to block on
|
||||
// read-readiness. Closing and waiting can race from different goroutines
|
||||
// (Close() from the caller that tears down the tunnel, Wait() from
|
||||
// dispatchLoop's own goroutine) -- reviewer feedback on XTLS/Xray-core#6580
|
||||
// found that a bare `int` fd field let Close() race Wait()'s use of the
|
||||
// same fd number, and on Darwin a closed fd number can be reused by an
|
||||
// unrelated concurrent open() before Wait() gets to call Kevent on it,
|
||||
// so Wait() could end up polling (or Close() could end up closing) a
|
||||
// completely unrelated file descriptor. This type makes closing
|
||||
// idempotent (sync.Once) and gates every Kevent call behind an atomic
|
||||
// "closed" flag checked immediately before the syscall, so Wait() never
|
||||
// issues a kevent syscall against a fd number that Close() has already
|
||||
// (or is concurrently) invalidated -- there's still a narrow window where
|
||||
// Wait() checks-then-uses the fd, but Close() only actually closes it
|
||||
// after Wait() cannot start a new syscall on it (the flag is set first,
|
||||
// synchronized with acquire/release semantics), which is sufficient since
|
||||
// Wait()'s Kevent call itself is what's being raced, not a fd read/write.
|
||||
type waitKqueue struct {
|
||||
fd int
|
||||
closed atomic.Bool
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// newWaitKqueue creates a kqueue registered for read-readiness on fd, for
|
||||
// Wait() to block on. Returns nil if anything fails, so callers can fall
|
||||
// back to a bounded sleep rather than error out of NewTun over what is
|
||||
// purely a CPU-efficiency concern.
|
||||
func newWaitKqueue(fd int) *waitKqueue {
|
||||
kq, err := unix.Kqueue()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
_, err = unix.Kevent(kq, []unix.Kevent_t{{
|
||||
Ident: uint64(fd),
|
||||
Filter: unix.EVFILT_READ,
|
||||
Flags: unix.EV_ADD | unix.EV_ENABLE,
|
||||
}}, nil, nil)
|
||||
if err != nil {
|
||||
_ = unix.Close(kq)
|
||||
return nil
|
||||
}
|
||||
return &waitKqueue{fd: kq}
|
||||
}
|
||||
|
||||
// wait blocks until the registered fd is readable, timeout elapses, or a
|
||||
// benign interrupt occurs -- all three are "this kqueue is still healthy,
|
||||
// the caller should just try again" and return true; the caller
|
||||
// (DarwinTun.Wait) doesn't need to distinguish them since it always calls
|
||||
// ReadPacket() right after anyway, and that already handles "nothing was
|
||||
// actually there" via ErrQueueEmpty. Returns false only when the kqueue
|
||||
// itself is no longer usable -- already closed, or the kevent syscall
|
||||
// failed for a reason other than EINTR -- see its own call site in
|
||||
// DarwinTun.Wait for why a persistent failure must not be silently
|
||||
// retried forever (reviewer feedback, XTLS/Xray-core#6580 P2).
|
||||
func (w *waitKqueue) wait(timeout time.Duration) (ok bool) {
|
||||
if w.closed.Load() {
|
||||
return false
|
||||
}
|
||||
events := make([]unix.Kevent_t, 1)
|
||||
ts := unix.NsecToTimespec(timeout.Nanoseconds())
|
||||
_, err := unix.Kevent(w.fd, nil, events, &ts)
|
||||
if err != nil {
|
||||
return errors.Is(err, unix.EINTR)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// close marks the kqueue as unusable (so any Wait() call that hasn't yet
|
||||
// entered the kevent syscall bails out instead) and closes the underlying
|
||||
// fd exactly once, regardless of how many times close is called or
|
||||
// whether it races a Wait() already inside its kevent syscall (that call
|
||||
// either completes against the still-open fd or returns an error safely
|
||||
// -- either way, no other goroutine can be handed this fd number in
|
||||
// between the atomic flag flip and the actual close, since nothing else
|
||||
// in this type ever creates a new kqueue with the same field).
|
||||
func (w *waitKqueue) close() {
|
||||
w.once.Do(func() {
|
||||
w.closed.Store(true)
|
||||
_ = unix.Close(w.fd)
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
_ Tun = (*DarwinTun)(nil)
|
||||
_ GVisorDevice = (*DarwinTun)(nil)
|
||||
@@ -77,6 +169,7 @@ func NewTun(options *Config) (Tun, error) {
|
||||
options: options,
|
||||
tunFd: fd,
|
||||
ownsFd: false,
|
||||
waitKq: newWaitKqueue(fd),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -103,6 +196,7 @@ func NewTun(options *Config) (Tun, error) {
|
||||
options: options,
|
||||
tunFd: int(tunFile.Fd()),
|
||||
ownsFd: true,
|
||||
waitKq: newWaitKqueue(int(tunFile.Fd())),
|
||||
gateway: gateway,
|
||||
}, nil
|
||||
}
|
||||
@@ -134,6 +228,9 @@ func (t *DarwinTun) Close() error {
|
||||
_ = t.routeMonitor.Close()
|
||||
}
|
||||
})
|
||||
if t.waitKq != nil {
|
||||
t.waitKq.close()
|
||||
}
|
||||
routeErr := t.unsetSystemRoutes()
|
||||
if t.ownsFd {
|
||||
return xerrors.Combine(routeErr, t.tunFile.Close())
|
||||
@@ -242,9 +339,43 @@ func (t *DarwinTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
|
||||
}), nil
|
||||
}
|
||||
|
||||
// Wait some cpu cycles
|
||||
// Wait blocks until tunFd is readable (or a short timeout elapses), rather
|
||||
// than spinning the CPU -- see the waitKq field's own doc comment. A bounded
|
||||
// timeout (not an indefinite wait) keeps this responsive to a Close() that
|
||||
// happens to race a call already parked here.
|
||||
//
|
||||
// Reviewer feedback (XTLS/Xray-core#6580, P2): the original version
|
||||
// discarded every error from the underlying kevent syscall. dispatchLoop
|
||||
// (stack_gvisor_endpoint.go) calls ReadPacket() then Wait() in an
|
||||
// unconditional tight loop -- if kevent started failing at runtime for a
|
||||
// persistent reason (not just a benign EINTR), Wait() returning
|
||||
// immediately every time reintroduces exactly the busy-spin this whole
|
||||
// change exists to remove, just routed through a failing syscall instead
|
||||
// of procyield. waitKq.wait's own bool return distinguishes "genuinely
|
||||
// interrupted, try again" from "this kqueue is unusable now" -- Wait()
|
||||
// permanently falls back to the sleep path once that happens, rather than
|
||||
// retrying the same broken kqueue forever.
|
||||
func (t *DarwinTun) Wait() {
|
||||
procyield(1)
|
||||
if t.waitKq != nil && t.waitKq.wait(time.Second) {
|
||||
return
|
||||
}
|
||||
if t.waitKq != nil {
|
||||
// Persistent kevent failure (not a benign EINTR, and not just
|
||||
// "the 1s timeout elapsed with nothing to read" -- wait() already
|
||||
// returned true for both of those cases above). Stop trusting
|
||||
// this kqueue for the rest of this DarwinTun's lifetime instead of
|
||||
// re-attempting a syscall that's already shown it won't succeed.
|
||||
t.waitKq.close()
|
||||
t.waitKq = nil
|
||||
}
|
||||
// Reviewer feedback (XTLS/Xray-core#6580): procyield here is the same
|
||||
// busy-spin this whole change exists to remove, just gated behind an
|
||||
// edge case (kqueue setup failing, which practically never happens on
|
||||
// real Darwin systems, or having just failed permanently above)
|
||||
// instead of always -- a genuine bounded sleep actually yields the CPU
|
||||
// instead of being a near-instant scheduler hint that lets the tight
|
||||
// dispatchLoop caller spin just as hot as before.
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
|
||||
func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) {
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestSelectDarwinGatewayDefault(t *testing.T) {
|
||||
@@ -47,3 +51,183 @@ func TestSelectDarwinGatewayRequiresUsableLocalAddress(t *testing.T) {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
// newTestSocketpair returns a connected AF_UNIX/SOCK_DGRAM pair -- a real
|
||||
// fd DarwinTun.Wait's kqueue can register EVFILT_READ against, without
|
||||
// needing an actual utun interface (which requires root/network
|
||||
// entitlements this test environment doesn't have). Datagram sockets
|
||||
// (unlike pipes) support both "write makes readable" and "close makes
|
||||
// readable" the same way a tun fd's read-readiness behaves.
|
||||
func newTestSocketpair(t *testing.T) (a, b int) {
|
||||
t.Helper()
|
||||
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("socketpair: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = unix.Close(fds[0])
|
||||
_ = unix.Close(fds[1])
|
||||
})
|
||||
return fds[0], fds[1]
|
||||
}
|
||||
|
||||
// Reviewer feedback, XTLS/Xray-core#6580: "blocking with no data" case --
|
||||
// wait() must not return before the timeout when nothing is written.
|
||||
func TestWaitKqueueBlocksWithNoData(t *testing.T) {
|
||||
a, _ := newTestSocketpair(t)
|
||||
kq := newWaitKqueue(a)
|
||||
if kq == nil {
|
||||
t.Fatal("newWaitKqueue returned nil")
|
||||
}
|
||||
defer kq.close()
|
||||
|
||||
start := time.Now()
|
||||
ok := kq.wait(150 * time.Millisecond)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if !ok {
|
||||
t.Fatal("wait() returned false on a healthy kqueue with a plain timeout")
|
||||
}
|
||||
if elapsed < 100*time.Millisecond {
|
||||
t.Fatalf("wait() returned after only %v, expected it to block close to the 150ms timeout", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
// Reviewer feedback: "wake up with a readable fd" case.
|
||||
func TestWaitKqueueWakesOnReadable(t *testing.T) {
|
||||
a, b := newTestSocketpair(t)
|
||||
kq := newWaitKqueue(a)
|
||||
if kq == nil {
|
||||
t.Fatal("newWaitKqueue returned nil")
|
||||
}
|
||||
defer kq.close()
|
||||
|
||||
done := make(chan bool, 1)
|
||||
go func() {
|
||||
done <- kq.wait(5 * time.Second)
|
||||
}()
|
||||
|
||||
time.Sleep(20 * time.Millisecond) // let wait() actually enter the syscall first
|
||||
if _, err := unix.Write(b, []byte{0x1}); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case ok := <-done:
|
||||
if !ok {
|
||||
t.Fatal("wait() returned false after the fd became readable")
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("wait() did not wake up within 2s of the fd becoming readable")
|
||||
}
|
||||
}
|
||||
|
||||
// Reviewer feedback: "timeout" case, explicitly (distinct from the
|
||||
// no-data test above, which also checks blocking duration -- this one
|
||||
// only checks the return value).
|
||||
func TestWaitKqueueTimesOut(t *testing.T) {
|
||||
a, _ := newTestSocketpair(t)
|
||||
kq := newWaitKqueue(a)
|
||||
if kq == nil {
|
||||
t.Fatal("newWaitKqueue returned nil")
|
||||
}
|
||||
defer kq.close()
|
||||
|
||||
if !kq.wait(50 * time.Millisecond) {
|
||||
t.Fatal("wait() returned false on a plain timeout with no error condition")
|
||||
}
|
||||
}
|
||||
|
||||
// Reviewer feedback: "Close() wakes a blocked wait" case, and the
|
||||
// no-double-close/no-fd-reuse concern (P1) -- close() while wait() is
|
||||
// parked in its syscall must not panic, must not leave wait() hung, and a
|
||||
// second close() call (from a caller that, say, calls Close() twice on
|
||||
// the same DarwinTun) must be safe.
|
||||
func TestWaitKqueueCloseDuringWaitIsSafe(t *testing.T) {
|
||||
a, _ := newTestSocketpair(t)
|
||||
kq := newWaitKqueue(a)
|
||||
if kq == nil {
|
||||
t.Fatal("newWaitKqueue returned nil")
|
||||
}
|
||||
|
||||
started := make(chan struct{})
|
||||
done := make(chan bool, 1)
|
||||
go func() {
|
||||
close(started)
|
||||
done <- kq.wait(5 * time.Second)
|
||||
}()
|
||||
|
||||
<-started
|
||||
time.Sleep(20 * time.Millisecond) // let wait() actually enter the syscall first
|
||||
kq.close()
|
||||
kq.close() // double-close must be idempotent, not panic or double-free the fd
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Either true (the close-of-the-underlying-fd unblocked kevent, a
|
||||
// real kqueue behavior) or false (wait() observed the closed flag
|
||||
// first) is acceptable -- what matters is that it returned at all,
|
||||
// promptly, without hanging or crashing.
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("wait() did not return within 2s of close() being called")
|
||||
}
|
||||
|
||||
// A wait() call *after* close() must return false immediately (the
|
||||
// closed-flag fast path), not attempt a syscall against the
|
||||
// already-closed (and potentially since-reused, on a real system) fd
|
||||
// number.
|
||||
if kq.wait(time.Second) {
|
||||
t.Fatal("wait() returned true after close() -- should short-circuit via the closed flag")
|
||||
}
|
||||
}
|
||||
|
||||
// Reviewer feedback: "multiple/concurrent close guard" case -- many
|
||||
// goroutines calling close() concurrently must close the underlying fd
|
||||
// exactly once.
|
||||
func TestWaitKqueueConcurrentCloseIsSafe(t *testing.T) {
|
||||
a, _ := newTestSocketpair(t)
|
||||
kq := newWaitKqueue(a)
|
||||
if kq == nil {
|
||||
t.Fatal("newWaitKqueue returned nil")
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for range 20 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
kq.close()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if !kq.closed.Load() {
|
||||
t.Fatal("closed flag not set after concurrent close() calls")
|
||||
}
|
||||
}
|
||||
|
||||
// Reviewer feedback: "kevent runtime failure without spinning" case (P2).
|
||||
// Simulates a kqueue that has gone bad (closed out from under it, as if a
|
||||
// concurrent/erroneous close happened) and confirms wait() reports it as
|
||||
// unusable (false) rather than silently returning true forever, which is
|
||||
// what DarwinTun.Wait relies on to permanently fall back to the sleep
|
||||
// path instead of re-entering a failing syscall on every dispatchLoop
|
||||
// iteration.
|
||||
func TestWaitKqueueReportsPersistentFailure(t *testing.T) {
|
||||
a, _ := newTestSocketpair(t)
|
||||
kq := newWaitKqueue(a)
|
||||
if kq == nil {
|
||||
t.Fatal("newWaitKqueue returned nil")
|
||||
}
|
||||
// Close the underlying kqueue fd directly (bypassing kq.close(), which
|
||||
// would also set the closed flag) to simulate the fd going bad for a
|
||||
// reason other than this type's own close() -- e.g. some other code
|
||||
// path in the process closing it, or the kernel invalidating it.
|
||||
_ = unix.Close(kq.fd)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
if kq.wait(50 * time.Millisecond) {
|
||||
t.Fatalf("wait() call %d returned true against a closed underlying fd -- should report failure, not spin", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+729
-25
@@ -3,28 +3,60 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
_ "unsafe"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"gvisor.dev/gvisor/pkg/buffer"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
xerrors "github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
const tunHeaderSize = 4
|
||||
const (
|
||||
tunHeaderSize = 4
|
||||
defaultFreeBSDGateway = "169.254.10.1/30"
|
||||
|
||||
// escapeFib is the routing table outbound sockets are switched to so
|
||||
// their traffic bypasses the TUN routes installed in the default FIB
|
||||
// (FreeBSD's substitute for the per-socket interface binding other
|
||||
// platforms use). Requires the boot tunable net.fibs >= 2.
|
||||
escapeFib = 1
|
||||
)
|
||||
|
||||
//go:linkname procyield runtime.procyield
|
||||
func procyield(cycles uint32)
|
||||
|
||||
type FreeBSDTun struct {
|
||||
device tun.Device
|
||||
mtu uint32
|
||||
device tun.Device
|
||||
options *Config
|
||||
tunIndex int
|
||||
autoInterface bool
|
||||
|
||||
systemRoutes []netip.Prefix
|
||||
escapeMu sync.Mutex
|
||||
escapeRoutes []escapeRoute
|
||||
routeMonitor *os.File
|
||||
routeMonitorOnce sync.Once
|
||||
}
|
||||
|
||||
// escapeRoute remembers one route written into the escape FIB, in the exact
|
||||
// shape needed to delete it again. A zero gateway means an interface route.
|
||||
type escapeRoute struct {
|
||||
prefix netip.Prefix
|
||||
ifIndex int
|
||||
gateway netip.Addr
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -34,20 +66,172 @@ var (
|
||||
|
||||
// NewTun builds new tun interface handler
|
||||
func NewTun(options *Config) (Tun, error) {
|
||||
gateway, local, err := selectFreeBSDGateway(options.Gateway)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// net.fibs is a boot-time constant, so validate the escape routing table
|
||||
// before the shared handler registers a dialer controller that would
|
||||
// otherwise steer every outbound socket into a table that was never set up.
|
||||
if options.AutoOutboundsInterface != "" {
|
||||
if err := checkEscapeFib(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tunDev, err := tun.CreateTUN(options.Name, int(options.MTU))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &FreeBSDTun{device: tunDev, mtu: options.MTU}, nil
|
||||
name, err := tunDev.Name()
|
||||
if err != nil {
|
||||
_ = tunDev.Close()
|
||||
return nil, err
|
||||
}
|
||||
// From here the interface exists in the kernel; the wireguard library does
|
||||
// not remove it on Close, so every failure path must destroy it too or the
|
||||
// next start fails with "interface already exists".
|
||||
iface, err := net.InterfaceByName(name)
|
||||
if err != nil {
|
||||
_ = tunDev.Close()
|
||||
destroyInterface(name)
|
||||
return nil, err
|
||||
}
|
||||
if err := setIPAddress(name, gateway, local, iface.Index); err != nil {
|
||||
_ = tunDev.Close()
|
||||
destroyInterface(name)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &FreeBSDTun{
|
||||
device: tunDev,
|
||||
options: options,
|
||||
tunIndex: iface.Index,
|
||||
autoInterface: options.AutoOutboundsInterface != "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// selectFreeBSDGateway picks the first IPv4 prefix from the configured gateway
|
||||
// list and the local address derived from it (the darwin semantics: the
|
||||
// gateway is the remote side of the point-to-point pair, the local address is
|
||||
// the next one after it), falling back to the same link-local default.
|
||||
func selectFreeBSDGateway(configured []string) (netip.Prefix, netip.Addr, error) {
|
||||
gateway := netip.MustParsePrefix(defaultFreeBSDGateway)
|
||||
if len(configured) > 0 {
|
||||
found := false
|
||||
for _, value := range configured {
|
||||
prefix, err := netip.ParsePrefix(value)
|
||||
if err != nil {
|
||||
return netip.Prefix{}, netip.Addr{}, xerrors.New("invalid FreeBSD gateway ", value).Base(err)
|
||||
}
|
||||
if prefix.Addr().Is4() {
|
||||
gateway, found = prefix, true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return netip.Prefix{}, netip.Addr{}, xerrors.New("FreeBSD gateway requires at least one IPv4 prefix")
|
||||
}
|
||||
}
|
||||
|
||||
local, ok := nextLocalIPv4(gateway)
|
||||
if !ok || !gateway.Contains(local) {
|
||||
return netip.Prefix{}, netip.Addr{}, xerrors.New("FreeBSD gateway ", gateway.String(), " must contain at least one usable local IPv4 address after the gateway address")
|
||||
}
|
||||
return gateway, local, nil
|
||||
}
|
||||
|
||||
func nextLocalIPv4(gateway netip.Prefix) (netip.Addr, bool) {
|
||||
local4 := gateway.Addr().As4()
|
||||
for i := len(local4) - 1; i >= 0; i-- {
|
||||
local4[i]++
|
||||
if local4[i] != 0 {
|
||||
return netip.AddrFrom4(local4), true
|
||||
}
|
||||
}
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) Start() error {
|
||||
if err := t.setSystemRoutes(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Gate on this instance's own option, not the package-global updater,
|
||||
// which a previously-removed inbound may have left set. checkEscapeFib
|
||||
// already ran in NewTun, before the dialer controller was registered.
|
||||
if t.autoInterface {
|
||||
if err := t.syncEscapeFib(); err != nil {
|
||||
_ = t.unsetSystemRoutes()
|
||||
return err
|
||||
}
|
||||
fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
|
||||
if err != nil {
|
||||
t.unsetEscapeFib()
|
||||
_ = t.unsetSystemRoutes()
|
||||
return err
|
||||
}
|
||||
t.routeMonitor = os.NewFile(uintptr(fd), "xray-route-monitor")
|
||||
go t.monitorRouteChanges()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// monitorRouteChanges refreshes the outbound interface and the escape FIB
|
||||
// mirror whenever the system routing table changes.
|
||||
func (t *FreeBSDTun) monitorRouteChanges() {
|
||||
buffer := make([]byte, 64*1024)
|
||||
for {
|
||||
if _, err := t.routeMonitor.Read(buffer); err != nil {
|
||||
if !errors.Is(err, os.ErrClosed) {
|
||||
xerrors.LogInfoInner(context.Background(), err, "[tun] failed to monitor route changes")
|
||||
}
|
||||
return
|
||||
}
|
||||
if updater != nil {
|
||||
updater.Update()
|
||||
}
|
||||
if err := t.syncEscapeFib(); err != nil {
|
||||
xerrors.LogInfoInner(context.Background(), err, "[tun] failed to refresh escape routes")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) Close() error {
|
||||
return t.device.Close()
|
||||
t.routeMonitorOnce.Do(func() {
|
||||
if t.routeMonitor != nil {
|
||||
_ = t.routeMonitor.Close()
|
||||
}
|
||||
})
|
||||
t.unsetEscapeFib()
|
||||
routeErr := t.unsetSystemRoutes()
|
||||
name, nameErr := t.Name()
|
||||
closeErr := t.device.Close()
|
||||
// The wireguard tun device does not tear the interface down on FreeBSD,
|
||||
// so an unclean shutdown would leave utun<n> behind and the next start
|
||||
// would fail with "interface already exists"; destroy it explicitly.
|
||||
if nameErr == nil {
|
||||
destroyInterface(name)
|
||||
}
|
||||
return xerrors.Combine(routeErr, closeErr)
|
||||
}
|
||||
|
||||
func destroyInterface(name string) {
|
||||
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
// struct ifreq: 16-byte name + a 16-byte union (SIOCIFDESTROY's encoded
|
||||
// length is 32 bytes on amd64, and the kernel copies in all of it).
|
||||
var req struct {
|
||||
Name [unix.IFNAMSIZ]byte
|
||||
_ [16]byte
|
||||
}
|
||||
copy(req.Name[:], name)
|
||||
_ = ioctlPtr(fd, unix.SIOCIFDESTROY, unsafe.Pointer(&req))
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) Name() (string, error) {
|
||||
@@ -55,21 +239,13 @@ func (t *FreeBSDTun) Name() (string, error) {
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) Index() (int, error) {
|
||||
name, err := t.Name()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
iface, err := net.InterfaceByName(name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return iface.Index, nil
|
||||
return t.tunIndex, nil
|
||||
}
|
||||
|
||||
// WritePacket implements GVisorDevice method to write one packet to the tun device
|
||||
func (t *FreeBSDTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
|
||||
// request memory to write from reusable buffer pool
|
||||
b := buf.NewWithSize(int32(t.mtu) + tunHeaderSize)
|
||||
b := buf.NewWithSize(int32(t.options.MTU) + tunHeaderSize)
|
||||
defer b.Release()
|
||||
|
||||
// prepare Unix specific packet header
|
||||
@@ -104,7 +280,7 @@ func (t *FreeBSDTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
|
||||
// which will make the stack call Wait which should implement desired push-back
|
||||
func (t *FreeBSDTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
|
||||
// request memory to write from reusable buffer pool
|
||||
b := buf.NewWithSize(int32(t.mtu) + tunHeaderSize)
|
||||
b := buf.NewWithSize(int32(t.options.MTU) + tunHeaderSize)
|
||||
|
||||
// read the bytes to the interface file
|
||||
n, err := b.ReadFrom(t.device.File())
|
||||
@@ -141,23 +317,551 @@ func (t *FreeBSDTun) Wait() {
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) newEndpoint() (stack.LinkEndpoint, error) {
|
||||
return &LinkEndpoint{deviceMTU: t.mtu, device: t}, nil
|
||||
return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil
|
||||
}
|
||||
|
||||
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
|
||||
const (
|
||||
IN6_IFF_NODAD = 0x0020 // netinet6/in6_var.h
|
||||
ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h
|
||||
)
|
||||
|
||||
// ifAliasReq4 is struct in_aliasreq from netinet/in_var.h in the 64-byte
|
||||
// layout unix.SIOCAIFADDR encodes (name + addr/dstaddr/mask sockaddrs).
|
||||
type ifAliasReq4 struct {
|
||||
Name [unix.IFNAMSIZ]byte
|
||||
Addr unix.RawSockaddrInet4
|
||||
Dstaddr unix.RawSockaddrInet4
|
||||
Mask unix.RawSockaddrInet4
|
||||
}
|
||||
|
||||
// ifAliasReq6 is struct in6_aliasreq from netinet6/in6_var.h. The trailing
|
||||
// Vhid field matters: unix.SIOCAIFADDR_IN6 is not in x/sys/unix, so
|
||||
// siocaifaddrIn6 is derived from this struct's size, and the kernel only
|
||||
// accepts the ioctl whose encoded length matches the real struct.
|
||||
type ifAliasReq6 struct {
|
||||
Name [unix.IFNAMSIZ]byte
|
||||
Addr unix.RawSockaddrInet6
|
||||
Dstaddr unix.RawSockaddrInet6
|
||||
Prefixmask unix.RawSockaddrInet6
|
||||
Flags int32
|
||||
Lifetime addrLifetime6
|
||||
Vhid int32
|
||||
}
|
||||
|
||||
// addrLifetime6 is struct in6_addrlifetime (time_t is int64 on freebsd/amd64).
|
||||
type addrLifetime6 struct {
|
||||
Expire int64
|
||||
Preferred int64
|
||||
Vltime uint32
|
||||
Pltime uint32
|
||||
}
|
||||
|
||||
// SIOCAIFADDR_IN6 = _IOW('i', 27, struct in6_aliasreq); x/sys/unix does not
|
||||
// carry the netinet6 ioctls, so encode it from the struct size like the
|
||||
// header macro does.
|
||||
const siocaifaddrIn6 = 0x80000000 | (uintptr(unsafe.Sizeof(ifAliasReq6{})) << 16) | ('i' << 8) | 27
|
||||
|
||||
// setIPAddress assigns the local/remote point-to-point IPv4 pair and a
|
||||
// link-local IPv6 address to the interface, required for the routing to work
|
||||
// (same scheme as the darwin implementation: local address is the one right
|
||||
// after the gateway address).
|
||||
func setIPAddress(name string, gateway netip.Prefix, local netip.Addr, ifIndex int) error {
|
||||
socket4, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unix.Close(socket4)
|
||||
|
||||
local4 := local.As4()
|
||||
|
||||
ifReq4 := ifAliasReq4{
|
||||
Addr: unix.RawSockaddrInet4{
|
||||
Len: unix.SizeofSockaddrInet4,
|
||||
Family: unix.AF_INET,
|
||||
Addr: local4,
|
||||
},
|
||||
Dstaddr: unix.RawSockaddrInet4{
|
||||
Len: unix.SizeofSockaddrInet4,
|
||||
Family: unix.AF_INET,
|
||||
Addr: gateway.Addr().As4(),
|
||||
},
|
||||
Mask: unix.RawSockaddrInet4{
|
||||
Len: unix.SizeofSockaddrInet4,
|
||||
Family: unix.AF_INET,
|
||||
Addr: prefixMask4(gateway.Bits()),
|
||||
},
|
||||
}
|
||||
copy(ifReq4.Name[:], name)
|
||||
if err = ioctlPtr(socket4, unix.SIOCAIFADDR, unsafe.Pointer(&ifReq4)); err != nil {
|
||||
return os.NewSyscallError("SIOCAIFADDR", err)
|
||||
}
|
||||
|
||||
socket6, err := unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unix.Close(socket6)
|
||||
|
||||
// link-local ipv6 address with suffix from ipv4, enough for v6 interface
|
||||
// routes to be attachable (darwin parity); a link-local address needs its
|
||||
// scope, which for FreeBSD ioctls is the interface index
|
||||
local6 := netip.AddrFrom16([16]byte{0: 0xfe, 1: 0x80, 12: local4[0], 13: local4[1], 14: local4[2], 15: local4[3]})
|
||||
|
||||
ifReq6 := ifAliasReq6{
|
||||
Addr: unix.RawSockaddrInet6{
|
||||
Len: unix.SizeofSockaddrInet6,
|
||||
Family: unix.AF_INET6,
|
||||
Addr: local6.As16(),
|
||||
Scope_id: uint32(ifIndex),
|
||||
},
|
||||
Prefixmask: unix.RawSockaddrInet6{
|
||||
Len: unix.SizeofSockaddrInet6,
|
||||
Family: unix.AF_INET6,
|
||||
Addr: prefixMask6(64),
|
||||
},
|
||||
Flags: IN6_IFF_NODAD,
|
||||
Lifetime: addrLifetime6{
|
||||
Vltime: ND6_INFINITE_LIFETIME,
|
||||
Pltime: ND6_INFINITE_LIFETIME,
|
||||
},
|
||||
}
|
||||
copy(ifReq6.Name[:], name)
|
||||
if err = ioctlPtr(socket6, uint(siocaifaddrIn6), unsafe.Pointer(&ifReq6)); err != nil {
|
||||
// non-fatal: FreeBSD auto-configures a link-local address on UP
|
||||
// interfaces, which is all the v6 interface routes need
|
||||
xerrors.LogInfoInner(context.Background(), os.NewSyscallError("SIOCAIFADDR_IN6", err), "[tun] failed to assign the IPv6 link-local address")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
|
||||
if fixedName == "" {
|
||||
return nil, errors.New("automatic outbound interface selection is not supported on this platform")
|
||||
func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error {
|
||||
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg))
|
||||
if errno != 0 {
|
||||
return errno
|
||||
}
|
||||
iface, err := net.InterfaceByName(fixedName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func prefixMask4(bits int) [4]byte {
|
||||
var mask [4]byte
|
||||
copy(mask[:], net.CIDRMask(bits, 32))
|
||||
return mask
|
||||
}
|
||||
|
||||
func prefixMask6(bits int) [16]byte {
|
||||
var mask [16]byte
|
||||
copy(mask[:], net.CIDRMask(bits, 128))
|
||||
return mask
|
||||
}
|
||||
|
||||
// setinterface is the per-socket half of autoOutboundsInterface. FreeBSD has
|
||||
// no SO_BINDTODEVICE/IP_BOUND_IF equivalent, so the socket is pointed at the
|
||||
// escape FIB instead, where Start() mirrors the physical default route; the
|
||||
// iface argument is resolved by the shared updater but unused here (the escape
|
||||
// is table-based, not a per-socket interface bind). checkEscapeFib in NewTun
|
||||
// guarantees the FIB exists before this can run.
|
||||
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
|
||||
return unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_SETFIB, escapeFib)
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) setSystemRoutes() error {
|
||||
routes, err := buildSystemRoutes(t.options.AutoSystemRoutingTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Route through the interface, not a gateway: the tun(4) device is a
|
||||
// broadcast interface here, so its point-to-point peer address doubles as
|
||||
// the subnet broadcast and the kernel refuses to route to it (EACCES).
|
||||
// Interface routes sidestep the gateway entirely (what wg-quick does on
|
||||
// FreeBSD).
|
||||
for _, destination := range routes {
|
||||
if err := execRoute(-1, unix.RTM_ADD, t.tunIndex, destination, netip.Addr{}); err != nil {
|
||||
_ = t.unsetSystemRoutes()
|
||||
return xerrors.New("failed to add system route ", destination).Base(err)
|
||||
}
|
||||
t.systemRoutes = append(t.systemRoutes, destination)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) unsetSystemRoutes() error {
|
||||
var errs []error
|
||||
for i := len(t.systemRoutes) - 1; i >= 0; i-- {
|
||||
destination := t.systemRoutes[i]
|
||||
if err := execRoute(-1, unix.RTM_DELETE, t.tunIndex, destination, netip.Addr{}); err != nil && !errors.Is(err, unix.ESRCH) {
|
||||
errs = append(errs, xerrors.New("failed to delete system route ", destination).Base(err))
|
||||
}
|
||||
}
|
||||
t.systemRoutes = nil
|
||||
return xerrors.Combine(errs...)
|
||||
}
|
||||
|
||||
func buildSystemRoutes(configured []string) ([]netip.Prefix, error) {
|
||||
routes := make([]netip.Prefix, 0, len(configured))
|
||||
seen := make(map[netip.Prefix]struct{})
|
||||
|
||||
appendRoute := func(prefix netip.Prefix) {
|
||||
prefix = prefix.Masked()
|
||||
if _, found := seen[prefix]; found {
|
||||
return
|
||||
}
|
||||
seen[prefix] = struct{}{}
|
||||
routes = append(routes, prefix)
|
||||
}
|
||||
|
||||
for _, value := range configured {
|
||||
prefix, err := netip.ParsePrefix(value)
|
||||
if err != nil {
|
||||
return nil, xerrors.New("invalid system route ", value).Base(err)
|
||||
}
|
||||
if prefix.Bits() == 0 {
|
||||
for _, protected := range protectedDefaultRoutes(prefix.Addr().Is4()) {
|
||||
appendRoute(protected)
|
||||
}
|
||||
continue
|
||||
}
|
||||
appendRoute(prefix)
|
||||
}
|
||||
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
// protectedDefaultRoutes splits a full default route into eight more-specific
|
||||
// prefixes covering everything but the zero /8, so the system's real default
|
||||
// route stays in place for outbound interface discovery (darwin parity).
|
||||
func protectedDefaultRoutes(ipv4 bool) []netip.Prefix {
|
||||
routes := make([]netip.Prefix, 0, 8)
|
||||
for i := 0; i < 8; i++ {
|
||||
if ipv4 {
|
||||
var address [4]byte
|
||||
address[0] = 1 << i
|
||||
routes = append(routes, netip.PrefixFrom(netip.AddrFrom4(address), 8-i))
|
||||
} else {
|
||||
var address [16]byte
|
||||
address[0] = 1 << i
|
||||
routes = append(routes, netip.PrefixFrom(netip.AddrFrom16(address), 8-i))
|
||||
}
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
// execRoute writes one RTM message to a routing socket. fib >= 0 targets that
|
||||
// routing table via SO_SETFIB on the routing socket (what route(8) -fib
|
||||
// does); fib -1 leaves the process default table. An invalid gateway produces
|
||||
// an interface route pinned to interfaceIndex instead of a gateway route.
|
||||
func execRoute(fib int, messageType int, interfaceIndex int, destination netip.Prefix, gateway netip.Addr) error {
|
||||
message := route.RouteMessage{
|
||||
Type: messageType,
|
||||
Version: unix.RTM_VERSION,
|
||||
Flags: unix.RTF_STATIC | unix.RTF_GATEWAY,
|
||||
Seq: 1,
|
||||
}
|
||||
if messageType == unix.RTM_ADD {
|
||||
message.Flags |= unix.RTF_UP
|
||||
}
|
||||
|
||||
var gatewayAddr route.Addr
|
||||
switch {
|
||||
case !gateway.IsValid():
|
||||
message.Flags &^= unix.RTF_GATEWAY
|
||||
message.Index = interfaceIndex
|
||||
gatewayAddr = &route.LinkAddr{Index: interfaceIndex}
|
||||
case gateway.Is4():
|
||||
gatewayAddr = &route.Inet4Addr{IP: gateway.As4()}
|
||||
default:
|
||||
gatewayAddr = &route.Inet6Addr{IP: gateway.As16()}
|
||||
}
|
||||
|
||||
if destination.Addr().Is4() {
|
||||
message.Addrs = []route.Addr{
|
||||
unix.RTAX_DST: &route.Inet4Addr{IP: destination.Addr().As4()},
|
||||
unix.RTAX_NETMASK: &route.Inet4Addr{IP: prefixMask4(destination.Bits())},
|
||||
unix.RTAX_GATEWAY: gatewayAddr,
|
||||
}
|
||||
} else {
|
||||
message.Addrs = []route.Addr{
|
||||
unix.RTAX_DST: &route.Inet6Addr{IP: destination.Addr().As16()},
|
||||
unix.RTAX_NETMASK: &route.Inet6Addr{IP: prefixMask6(destination.Bits())},
|
||||
unix.RTAX_GATEWAY: gatewayAddr,
|
||||
}
|
||||
}
|
||||
|
||||
request, err := message.Marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
if fib >= 0 {
|
||||
if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_SETFIB, fib); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = unix.Write(fd, request)
|
||||
return err
|
||||
}
|
||||
|
||||
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
|
||||
if fixedName != "" {
|
||||
iface, err := net.InterfaceByName(fixedName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if iface.Index == tunIndex {
|
||||
return nil, errors.New("outbound interface cannot be the TUN interface")
|
||||
}
|
||||
return iface, nil
|
||||
}
|
||||
|
||||
physical, err := physicalDefaultRoutes(tunIndex, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if iface.Index == tunIndex {
|
||||
return nil, errors.New("outbound interface cannot be the TUN interface")
|
||||
for _, family := range []int{unix.AF_INET, unix.AF_INET6} {
|
||||
for _, route := range physical {
|
||||
if route.family == family {
|
||||
return route.iface, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, errors.New("default route not found")
|
||||
}
|
||||
|
||||
// physicalRoute describes one physical default route: the interface it
|
||||
// leaves through, its gateway, and the connected prefix that makes the
|
||||
// gateway resolvable.
|
||||
type physicalRoute struct {
|
||||
family int
|
||||
iface *net.Interface
|
||||
gateway netip.Addr
|
||||
connected netip.Prefix
|
||||
}
|
||||
|
||||
// physicalDefaultRoutes scans the default routing table for default routes
|
||||
// that do not go through the TUN interface, at most one per address family
|
||||
// (the first usable one wins, matching the darwin implementation's
|
||||
// preference order). A non-zero onlyIndex restricts the scan to that
|
||||
// interface, for the fixed-name mode of autoOutboundsInterface.
|
||||
func physicalDefaultRoutes(tunIndex int, onlyIndex int) ([]physicalRoute, error) {
|
||||
rib, err := route.FetchRIB(unix.AF_UNSPEC, route.RIBTypeRoute, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
messages, err := route.ParseRIB(route.RIBTypeRoute, rib)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
found := make([]physicalRoute, 0, 2)
|
||||
seen := make(map[int]bool)
|
||||
for _, message := range messages {
|
||||
routeMessage, ok := message.(*route.RouteMessage)
|
||||
if !ok || routeMessage.Index == tunIndex {
|
||||
continue
|
||||
}
|
||||
if onlyIndex != 0 && routeMessage.Index != onlyIndex {
|
||||
continue
|
||||
}
|
||||
if routeMessage.Flags&unix.RTF_UP == 0 || routeMessage.Flags&unix.RTF_GATEWAY == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
family, ok := defaultRouteFamily(routeMessage)
|
||||
if !ok || seen[family] {
|
||||
continue
|
||||
}
|
||||
iface, err := usableInterface(routeMessage.Index)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
gatewayAddr, ok := routeAddrToNetip(routeMessage.Addrs[unix.RTAX_GATEWAY])
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
connected, err := connectedPrefix(iface, gatewayAddr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
seen[family] = true
|
||||
found = append(found, physicalRoute{
|
||||
family: family,
|
||||
iface: iface,
|
||||
gateway: gatewayAddr,
|
||||
connected: connected,
|
||||
})
|
||||
}
|
||||
|
||||
if len(found) == 0 {
|
||||
return nil, errors.New("default route not found")
|
||||
}
|
||||
return found, nil
|
||||
}
|
||||
|
||||
// defaultRouteFamily reports the address family of a RIB message that
|
||||
// represents a true default route (unspecified destination, zero mask).
|
||||
func defaultRouteFamily(message *route.RouteMessage) (int, bool) {
|
||||
if len(message.Addrs) <= unix.RTAX_NETMASK {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
switch destination := message.Addrs[unix.RTAX_DST].(type) {
|
||||
case *route.Inet4Addr:
|
||||
mask, ok := message.Addrs[unix.RTAX_NETMASK].(*route.Inet4Addr)
|
||||
if !ok || destination.IP != netip.IPv4Unspecified().As4() {
|
||||
return 0, false
|
||||
}
|
||||
ones, bits := net.IPMask(mask.IP[:]).Size()
|
||||
return unix.AF_INET, ones == 0 && bits == 32
|
||||
case *route.Inet6Addr:
|
||||
mask, ok := message.Addrs[unix.RTAX_NETMASK].(*route.Inet6Addr)
|
||||
if !ok || destination.IP != netip.IPv6Unspecified().As16() {
|
||||
return 0, false
|
||||
}
|
||||
ones, bits := net.IPMask(mask.IP[:]).Size()
|
||||
return unix.AF_INET6, ones == 0 && bits == 128
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func usableInterface(index int) (*net.Interface, error) {
|
||||
iface, err := net.InterfaceByIndex(index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
|
||||
return nil, errors.New("default route interface is not usable")
|
||||
}
|
||||
return iface, nil
|
||||
}
|
||||
|
||||
func routeAddrToNetip(addr route.Addr) (netip.Addr, bool) {
|
||||
switch typed := addr.(type) {
|
||||
case *route.Inet4Addr:
|
||||
return netip.AddrFrom4(typed.IP), true
|
||||
case *route.Inet6Addr:
|
||||
return netip.AddrFrom16(typed.IP), true
|
||||
default:
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
}
|
||||
|
||||
// checkEscapeFib verifies the system can host the escape routing table at
|
||||
// all: FIBs are a boot-time resource on FreeBSD.
|
||||
func checkEscapeFib() error {
|
||||
fibs, err := unix.SysctlUint32("net.fibs")
|
||||
if err != nil {
|
||||
return xerrors.New("failed to read net.fibs").Base(err)
|
||||
}
|
||||
if fibs < 2 {
|
||||
return errors.New("automatic outbound interface on FreeBSD needs a second routing table: add net.fibs=2 to /boot/loader.conf and reboot")
|
||||
}
|
||||
current, err := unix.SysctlUint32("net.my_fibnum")
|
||||
if err != nil {
|
||||
return xerrors.New("failed to read net.my_fibnum").Base(err)
|
||||
}
|
||||
if current == escapeFib {
|
||||
return errors.New("xray runs inside routing table 1, which is reserved as the escape table; start it in another FIB")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncEscapeFib mirrors the physical default routes (and the connected
|
||||
// prefixes their gateways resolve through) into the escape FIB, replacing
|
||||
// whatever mirror a previous call installed. On discovery failure the old
|
||||
// mirror is kept, since a stale escape route beats none during a transient
|
||||
// route flap.
|
||||
func (t *FreeBSDTun) syncEscapeFib() error {
|
||||
var onlyIndex int
|
||||
if t.options.AutoOutboundsInterface != "" && updater != nil {
|
||||
if iface := updater.Get(); iface != nil {
|
||||
onlyIndex = iface.Index
|
||||
}
|
||||
}
|
||||
physical, err := physicalDefaultRoutes(t.tunIndex, onlyIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desired := make([]escapeRoute, 0, 2*len(physical))
|
||||
for _, p := range physical {
|
||||
desired = append(desired,
|
||||
escapeRoute{prefix: p.connected, ifIndex: p.iface.Index},
|
||||
escapeRoute{prefix: defaultPrefix(p.family), ifIndex: p.iface.Index, gateway: p.gateway},
|
||||
)
|
||||
}
|
||||
|
||||
t.escapeMu.Lock()
|
||||
defer t.escapeMu.Unlock()
|
||||
|
||||
// The route monitor hears our own escape FIB writes too; rewriting an
|
||||
// unchanged mirror on every wake-up would ping-pong forever.
|
||||
if slices.Equal(t.escapeRoutes, desired) {
|
||||
return nil
|
||||
}
|
||||
|
||||
t.unsetEscapeFibLocked()
|
||||
for _, entry := range desired {
|
||||
err := execRoute(escapeFib, unix.RTM_ADD, entry.ifIndex, entry.prefix, entry.gateway)
|
||||
if err != nil && !errors.Is(err, unix.EEXIST) {
|
||||
return xerrors.New("failed to add escape route ", entry.prefix).Base(err)
|
||||
}
|
||||
t.escapeRoutes = append(t.escapeRoutes, entry)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) unsetEscapeFib() {
|
||||
t.escapeMu.Lock()
|
||||
defer t.escapeMu.Unlock()
|
||||
t.unsetEscapeFibLocked()
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) unsetEscapeFibLocked() {
|
||||
for i := len(t.escapeRoutes) - 1; i >= 0; i-- {
|
||||
entry := t.escapeRoutes[i]
|
||||
err := execRoute(escapeFib, unix.RTM_DELETE, entry.ifIndex, entry.prefix, entry.gateway)
|
||||
if err != nil && !errors.Is(err, unix.ESRCH) {
|
||||
xerrors.LogInfoInner(context.Background(), err, "[tun] failed to delete escape route ", entry.prefix)
|
||||
}
|
||||
}
|
||||
t.escapeRoutes = nil
|
||||
}
|
||||
|
||||
func defaultPrefix(family int) netip.Prefix {
|
||||
if family == unix.AF_INET {
|
||||
return netip.PrefixFrom(netip.IPv4Unspecified(), 0)
|
||||
}
|
||||
return netip.PrefixFrom(netip.IPv6Unspecified(), 0)
|
||||
}
|
||||
|
||||
// connectedPrefix finds the interface's address prefix containing the
|
||||
// gateway, which the escape FIB needs as an interface route so the mirrored
|
||||
// default route's gateway is resolvable there.
|
||||
func connectedPrefix(iface *net.Interface, gateway netip.Addr) (netip.Prefix, error) {
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return netip.Prefix{}, err
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
ipNet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ip, ok := netip.AddrFromSlice(ipNet.IP)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ip = ip.Unmap()
|
||||
ones, _ := ipNet.Mask.Size()
|
||||
prefix := netip.PrefixFrom(ip, ones).Masked()
|
||||
if prefix.Contains(gateway.WithZone("").Unmap()) {
|
||||
return prefix, nil
|
||||
}
|
||||
}
|
||||
return netip.Prefix{}, errors.New("no connected prefix contains the gateway")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
//go:build freebsd
|
||||
|
||||
package tun
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestSelectFreeBSDGatewayDefault(t *testing.T) {
|
||||
gateway, local, err := selectFreeBSDGateway(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gateway != netip.MustParsePrefix(defaultFreeBSDGateway) {
|
||||
t.Fatal("expected default gateway, got ", gateway)
|
||||
}
|
||||
if local != netip.MustParseAddr("169.254.10.2") {
|
||||
t.Fatal("wrong local address: ", local)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectFreeBSDGatewayPicksFirstIPv4(t *testing.T) {
|
||||
gateway, local, err := selectFreeBSDGateway([]string{"fd00::1/64", "10.0.0.1/30", "10.9.9.9/24"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gateway != netip.MustParsePrefix("10.0.0.1/30") {
|
||||
t.Fatal("wrong gateway: ", gateway)
|
||||
}
|
||||
if local != netip.MustParseAddr("10.0.0.2") {
|
||||
t.Fatal("wrong local address: ", local)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectFreeBSDGatewayRequiresIPv4(t *testing.T) {
|
||||
if _, _, err := selectFreeBSDGateway([]string{"fd00::1/64"}); err == nil {
|
||||
t.Fatal("expected error when no IPv4 gateway is configured")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectFreeBSDGatewayRejectsGarbage(t *testing.T) {
|
||||
if _, _, err := selectFreeBSDGateway([]string{"not-a-prefix"}); err == nil {
|
||||
t.Fatal("expected error for invalid gateway")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectFreeBSDGatewayRejectsFullPrefix(t *testing.T) {
|
||||
// 10.0.0.255/30: the "next" local address falls outside the prefix
|
||||
if _, _, err := selectFreeBSDGateway([]string{"10.0.0.255/30"}); err == nil {
|
||||
t.Fatal("expected error when no usable local address follows the gateway")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextLocalIPv4(t *testing.T) {
|
||||
local, ok := nextLocalIPv4(netip.MustParsePrefix("169.254.10.1/30"))
|
||||
if !ok || local != netip.MustParseAddr("169.254.10.2") {
|
||||
t.Fatal("wrong local address: ", local)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSystemRoutesSplitsDefault(t *testing.T) {
|
||||
routes, err := buildSystemRoutes([]string{"0.0.0.0/0"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := []string{
|
||||
"1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5",
|
||||
"16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1",
|
||||
}
|
||||
if len(routes) != len(expected) {
|
||||
t.Fatal("expected ", len(expected), " routes, got ", routes)
|
||||
}
|
||||
for i, want := range expected {
|
||||
if routes[i] != netip.MustParsePrefix(want) {
|
||||
t.Fatal("route ", i, ": expected ", want, ", got ", routes[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSystemRoutesSplitsDefaultIPv6(t *testing.T) {
|
||||
routes, err := buildSystemRoutes([]string{"::/0"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(routes) != 8 || routes[7] != netip.MustParsePrefix("8000::/1") {
|
||||
t.Fatal("unexpected v6 split: ", routes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSystemRoutesDeduplicates(t *testing.T) {
|
||||
routes, err := buildSystemRoutes([]string{"10.0.0.0/8", "10.1.2.3/8", "0.0.0.0/0", "0.0.0.0/0"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(routes) != 9 { // 10.0.0.0/8 once + 8 splits once
|
||||
t.Fatal("expected 9 routes, got ", routes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSystemRoutesRejectsGarbage(t *testing.T) {
|
||||
if _, err := buildSystemRoutes([]string{"10.0.0.0/33"}); err == nil {
|
||||
t.Fatal("expected error for invalid route")
|
||||
}
|
||||
}
|
||||
|
||||
func routeMessage(dst, mask route.Addr) *route.RouteMessage {
|
||||
addrs := make([]route.Addr, unix.RTAX_NETMASK+1)
|
||||
addrs[unix.RTAX_DST] = dst
|
||||
addrs[unix.RTAX_NETMASK] = mask
|
||||
return &route.RouteMessage{Addrs: addrs}
|
||||
}
|
||||
|
||||
func TestDefaultRouteFamilyMatchesIPv4Default(t *testing.T) {
|
||||
family, ok := defaultRouteFamily(routeMessage(&route.Inet4Addr{}, &route.Inet4Addr{}))
|
||||
if !ok || family != unix.AF_INET {
|
||||
t.Fatal("expected IPv4 default route match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRouteFamilyMatchesIPv6Default(t *testing.T) {
|
||||
family, ok := defaultRouteFamily(routeMessage(&route.Inet6Addr{}, &route.Inet6Addr{}))
|
||||
if !ok || family != unix.AF_INET6 {
|
||||
t.Fatal("expected IPv6 default route match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRouteFamilyRejectsNonDefault(t *testing.T) {
|
||||
if _, ok := defaultRouteFamily(routeMessage(
|
||||
&route.Inet4Addr{IP: [4]byte{10, 0, 0, 0}},
|
||||
&route.Inet4Addr{IP: [4]byte{255, 0, 0, 0}},
|
||||
)); ok {
|
||||
t.Fatal("non-default destination must not match")
|
||||
}
|
||||
if _, ok := defaultRouteFamily(routeMessage(
|
||||
&route.Inet4Addr{},
|
||||
&route.Inet4Addr{IP: [4]byte{255, 0, 0, 0}},
|
||||
)); ok {
|
||||
t.Fatal("non-zero mask must not match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRouteFamilyRejectsShortAddrs(t *testing.T) {
|
||||
if _, ok := defaultRouteFamily(&route.RouteMessage{}); ok {
|
||||
t.Fatal("message without addresses must not match")
|
||||
}
|
||||
}
|
||||
+56
-17
@@ -5,9 +5,10 @@ import (
|
||||
"fmt"
|
||||
gonet "net"
|
||||
"net/netip"
|
||||
reflect "reflect"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
|
||||
@@ -30,6 +31,11 @@ import (
|
||||
"golang.zx2c4.com/wireguard/device"
|
||||
)
|
||||
|
||||
type entry struct {
|
||||
got []net.IP
|
||||
time time.Time
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
conf *DeviceConfig
|
||||
policyManager policy.Manager
|
||||
@@ -43,6 +49,11 @@ type Handler struct {
|
||||
tnet *Net
|
||||
dev *device.Device
|
||||
mu sync.Mutex
|
||||
|
||||
// TODO: cache cleanup loop
|
||||
local bool
|
||||
cache map[string]entry
|
||||
cacheMu sync.Mutex
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
@@ -98,6 +109,20 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
local := false
|
||||
dns := conf.DNS
|
||||
if len(dns) == 0 {
|
||||
dns = []string{"1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"}
|
||||
}
|
||||
if len(dns) == 1 && dns[0] == "local" {
|
||||
local = true
|
||||
dns = nil
|
||||
}
|
||||
dnses := make([]netip.Addr, 0, len(dns))
|
||||
for _, dns := range dns {
|
||||
dnses = append(dnses, netip.MustParseAddr(dns))
|
||||
}
|
||||
|
||||
kernelTunSupported, err := KernelTunSupported()
|
||||
if err != nil {
|
||||
errors.LogWarningInner(context.Background(), err, "Failed to check kernel TUN support")
|
||||
@@ -106,10 +131,10 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
var tnet *Net
|
||||
if !conf.NoKernelTun && kernelTunSupported {
|
||||
errors.LogWarning(context.Background(), "Using kernel TUN")
|
||||
tun, tnet, err = createKernelTun(localAddresses, []netip.Addr{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("1.0.0.1"), netip.MustParseAddr("2606:4700:4700::1111"), netip.MustParseAddr("2606:4700:4700::1001")}, int(conf.Mtu))
|
||||
tun, tnet, err = createKernelTun(localAddresses, dnses, int(conf.Mtu))
|
||||
} else {
|
||||
errors.LogWarning(context.Background(), "Using gVisor TUN")
|
||||
tun, tnet, _, err = CreateNetTUN(localAddresses, []netip.Addr{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("1.0.0.1"), netip.MustParseAddr("2606:4700:4700::1111"), netip.MustParseAddr("2606:4700:4700::1001")}, int(conf.Mtu), true)
|
||||
tun, tnet, _, err = CreateNetTUN(localAddresses, dnses, int(conf.Mtu), true)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -126,6 +151,9 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
|
||||
tun: tun,
|
||||
tnet: tnet,
|
||||
|
||||
local: local,
|
||||
cache: make(map[string]entry),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -138,6 +166,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
}
|
||||
ob.Name = "wireguard"
|
||||
ob.CanSpliceCopy = 3
|
||||
dialer.SetOutboundGateway(ctx, ob)
|
||||
|
||||
if err := h.init(ctx); err != nil {
|
||||
return err
|
||||
@@ -342,31 +371,34 @@ func (h *Handler) init(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (h *Handler) resolveLocal(host string) (net.IP, error) {
|
||||
return resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, error) {
|
||||
ips, _, err := h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
|
||||
return ips, err
|
||||
return h.resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, uint32, error) {
|
||||
return h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) resolveRemote(host string) (net.IP, error) {
|
||||
return resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, error) {
|
||||
addrs, err := h.tnet.LookupHost(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return h.resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, uint32, error) {
|
||||
if h.local {
|
||||
return h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
|
||||
}
|
||||
ips := make([]net.IP, 0, len(addrs))
|
||||
for _, addr := range addrs {
|
||||
ips = append(ips, net.ParseIP(addr))
|
||||
}
|
||||
return ips, nil
|
||||
return h.tnet.LookupHost(host)
|
||||
})
|
||||
}
|
||||
|
||||
func resolveDomain(host string, strategy DeviceConfig_DomainStrategy, lookupIP func(host string) ([]net.IP, error)) (net.IP, error) {
|
||||
func (h *Handler) resolveDomain(host string, strategy DeviceConfig_DomainStrategy, lookupIP func(host string) ([]net.IP, uint32, error)) (net.IP, error) {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
ips, err := lookupIP(host)
|
||||
h.cacheMu.Lock()
|
||||
if entry, ok := h.cache[host]; ok {
|
||||
if time.Now().Before(entry.time) {
|
||||
h.cacheMu.Unlock()
|
||||
return entry.got[dice.Roll(len(entry.got))], nil
|
||||
}
|
||||
delete(h.cache, host)
|
||||
}
|
||||
h.cacheMu.Unlock()
|
||||
ips, ttl, err := lookupIP(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -406,6 +438,13 @@ func resolveDomain(host string, strategy DeviceConfig_DomainStrategy, lookupIP f
|
||||
if len(got) == 0 {
|
||||
return nil, dns.ErrEmptyResponse
|
||||
}
|
||||
entry := entry{
|
||||
got: got,
|
||||
time: time.Now().Add(time.Duration(ttl) * time.Second),
|
||||
}
|
||||
h.cacheMu.Lock()
|
||||
h.cache[host] = entry
|
||||
h.cacheMu.Unlock()
|
||||
return got[dice.Roll(len(got))], nil
|
||||
}
|
||||
|
||||
|
||||
@@ -164,6 +164,7 @@ type DeviceConfig struct {
|
||||
DomainStrategy DeviceConfig_DomainStrategy `protobuf:"varint,7,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.proxy.wireguard.DeviceConfig_DomainStrategy" json:"domain_strategy,omitempty"`
|
||||
IsClient bool `protobuf:"varint,8,opt,name=is_client,json=isClient,proto3" json:"is_client,omitempty"`
|
||||
NoKernelTun bool `protobuf:"varint,9,opt,name=no_kernel_tun,json=noKernelTun,proto3" json:"no_kernel_tun,omitempty"`
|
||||
DNS []string `protobuf:"bytes,10,rep,name=DNS,proto3" json:"DNS,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -261,6 +262,13 @@ func (x *DeviceConfig) GetNoKernelTun() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *DeviceConfig) GetDNS() []string {
|
||||
if x != nil {
|
||||
return x.DNS
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_proxy_wireguard_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_proxy_wireguard_config_proto_rawDesc = "" +
|
||||
@@ -275,7 +283,7 @@ const file_proxy_wireguard_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"keep_alive\x18\x04 \x01(\tR\tkeepAlive\x12\x1f\n" +
|
||||
"\vallowed_ips\x18\x05 \x03(\tR\n" +
|
||||
"allowedIps\"\xdc\x03\n" +
|
||||
"allowedIps\"\xee\x03\n" +
|
||||
"\fDeviceConfig\x12\x1d\n" +
|
||||
"\n" +
|
||||
"secret_key\x18\x01 \x01(\tR\tsecretKey\x12\x1a\n" +
|
||||
@@ -286,7 +294,9 @@ const file_proxy_wireguard_config_proto_rawDesc = "" +
|
||||
"\breserved\x18\x06 \x01(\fR\breserved\x12Z\n" +
|
||||
"\x0fdomain_strategy\x18\a \x01(\x0e21.xray.proxy.wireguard.DeviceConfig.DomainStrategyR\x0edomainStrategy\x12\x1b\n" +
|
||||
"\tis_client\x18\b \x01(\bR\bisClient\x12\"\n" +
|
||||
"\rno_kernel_tun\x18\t \x01(\bR\vnoKernelTun\"\\\n" +
|
||||
"\rno_kernel_tun\x18\t \x01(\bR\vnoKernelTun\x12\x10\n" +
|
||||
"\x03DNS\x18\n" +
|
||||
" \x03(\tR\x03DNS\"\\\n" +
|
||||
"\x0eDomainStrategy\x12\f\n" +
|
||||
"\bFORCE_IP\x10\x00\x12\r\n" +
|
||||
"\tFORCE_IP4\x10\x01\x12\r\n" +
|
||||
|
||||
@@ -34,4 +34,5 @@ message DeviceConfig {
|
||||
DomainStrategy domain_strategy = 7;
|
||||
bool is_client = 8;
|
||||
bool no_kernel_tun = 9;
|
||||
repeated string DNS = 10;
|
||||
}
|
||||
@@ -248,7 +248,7 @@ var (
|
||||
errTimeout = errors.New("i/o timeout")
|
||||
)
|
||||
|
||||
func (net *Net) LookupHost(host string) (addrs []string, err error) {
|
||||
func (net *Net) LookupHost(host string) (addrs []net.IP, ttl uint32, err error) {
|
||||
return net.LookupContextHost(context.Background(), host)
|
||||
}
|
||||
|
||||
@@ -567,9 +567,9 @@ func (tnet *Net) tryOneName(ctx context.Context, name string, qtype dnsmessage.T
|
||||
return dnsmessage.Parser{}, "", lastErr
|
||||
}
|
||||
|
||||
func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string, error) {
|
||||
func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]net.IP, uint32, error) {
|
||||
if host == "" || (!tnet.hasV6 && !tnet.hasV4) {
|
||||
return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
|
||||
return nil, 0, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
|
||||
}
|
||||
zlen := len(host)
|
||||
if strings.IndexByte(host, ':') != -1 {
|
||||
@@ -578,11 +578,11 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
||||
}
|
||||
}
|
||||
if ip, err := netip.ParseAddr(host[:zlen]); err == nil {
|
||||
return []string{ip.String()}, nil
|
||||
return []net.IP{ip.AsSlice()}, 0, nil
|
||||
}
|
||||
|
||||
if !isDomainName(host) {
|
||||
return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
|
||||
return nil, 0, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
|
||||
}
|
||||
type result struct {
|
||||
p dnsmessage.Parser
|
||||
@@ -611,6 +611,7 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
||||
lane <- result{p, server, err}
|
||||
}()
|
||||
}
|
||||
ttl := uint32(300)
|
||||
for l := 0; l < lanes; l++ {
|
||||
result := <-lane
|
||||
if result.error != nil {
|
||||
@@ -644,6 +645,7 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
||||
}
|
||||
break loop
|
||||
}
|
||||
ttl = min(ttl, h.TTL)
|
||||
addrsV4 = append(addrsV4, netip.AddrFrom4(a.A))
|
||||
|
||||
case dnsmessage.TypeAAAA:
|
||||
@@ -656,6 +658,7 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
||||
}
|
||||
break loop
|
||||
}
|
||||
ttl = min(ttl, h.TTL)
|
||||
addrsV6 = append(addrsV6, netip.AddrFrom16(aaaa.AAAA))
|
||||
|
||||
default:
|
||||
@@ -680,11 +683,11 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
||||
}
|
||||
|
||||
if len(addrs) == 0 && lastErr != nil {
|
||||
return nil, lastErr
|
||||
return nil, 0, lastErr
|
||||
}
|
||||
saddrs := make([]string, 0, len(addrs))
|
||||
ips := make([]net.IP, 0, len(addrs))
|
||||
for _, ip := range addrs {
|
||||
saddrs = append(saddrs, ip.String())
|
||||
ips = append(ips, ip.AsSlice())
|
||||
}
|
||||
return saddrs, nil
|
||||
return ips, ttl, nil
|
||||
}
|
||||
|
||||
@@ -443,22 +443,26 @@ func (x *UdpHop) GetIntervalMax() int64 {
|
||||
}
|
||||
|
||||
type QuicParams struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Congestion string `protobuf:"bytes,1,opt,name=congestion,proto3" json:"congestion,omitempty"`
|
||||
BbrProfile string `protobuf:"bytes,2,opt,name=bbr_profile,json=bbrProfile,proto3" json:"bbr_profile,omitempty"`
|
||||
BrutalUp uint64 `protobuf:"varint,3,opt,name=brutal_up,json=brutalUp,proto3" json:"brutal_up,omitempty"`
|
||||
BrutalDown uint64 `protobuf:"varint,4,opt,name=brutal_down,json=brutalDown,proto3" json:"brutal_down,omitempty"`
|
||||
UdpHop *UdpHop `protobuf:"bytes,5,opt,name=udp_hop,json=udpHop,proto3" json:"udp_hop,omitempty"`
|
||||
InitStreamReceiveWindow uint64 `protobuf:"varint,6,opt,name=init_stream_receive_window,json=initStreamReceiveWindow,proto3" json:"init_stream_receive_window,omitempty"`
|
||||
MaxStreamReceiveWindow uint64 `protobuf:"varint,7,opt,name=max_stream_receive_window,json=maxStreamReceiveWindow,proto3" json:"max_stream_receive_window,omitempty"`
|
||||
InitConnReceiveWindow uint64 `protobuf:"varint,8,opt,name=init_conn_receive_window,json=initConnReceiveWindow,proto3" json:"init_conn_receive_window,omitempty"`
|
||||
MaxConnReceiveWindow uint64 `protobuf:"varint,9,opt,name=max_conn_receive_window,json=maxConnReceiveWindow,proto3" json:"max_conn_receive_window,omitempty"`
|
||||
MaxIdleTimeout int64 `protobuf:"varint,10,opt,name=max_idle_timeout,json=maxIdleTimeout,proto3" json:"max_idle_timeout,omitempty"`
|
||||
KeepAlivePeriod int64 `protobuf:"varint,11,opt,name=keep_alive_period,json=keepAlivePeriod,proto3" json:"keep_alive_period,omitempty"`
|
||||
DisablePathMtuDiscovery bool `protobuf:"varint,12,opt,name=disable_path_mtu_discovery,json=disablePathMtuDiscovery,proto3" json:"disable_path_mtu_discovery,omitempty"`
|
||||
MaxIncomingStreams int64 `protobuf:"varint,13,opt,name=max_incoming_streams,json=maxIncomingStreams,proto3" json:"max_incoming_streams,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Congestion string `protobuf:"bytes,1,opt,name=congestion,proto3" json:"congestion,omitempty"`
|
||||
BbrProfile string `protobuf:"bytes,2,opt,name=bbr_profile,json=bbrProfile,proto3" json:"bbr_profile,omitempty"`
|
||||
BrutalUp uint64 `protobuf:"varint,3,opt,name=brutal_up,json=brutalUp,proto3" json:"brutal_up,omitempty"`
|
||||
BrutalDown uint64 `protobuf:"varint,4,opt,name=brutal_down,json=brutalDown,proto3" json:"brutal_down,omitempty"`
|
||||
BrutalDisableLossCompensation bool `protobuf:"varint,5,opt,name=brutal_disable_loss_compensation,json=brutalDisableLossCompensation,proto3" json:"brutal_disable_loss_compensation,omitempty"`
|
||||
UdpHop *UdpHop `protobuf:"bytes,6,opt,name=udp_hop,json=udpHop,proto3" json:"udp_hop,omitempty"`
|
||||
InitStreamReceiveWindow uint64 `protobuf:"varint,7,opt,name=init_stream_receive_window,json=initStreamReceiveWindow,proto3" json:"init_stream_receive_window,omitempty"`
|
||||
MaxStreamReceiveWindow uint64 `protobuf:"varint,8,opt,name=max_stream_receive_window,json=maxStreamReceiveWindow,proto3" json:"max_stream_receive_window,omitempty"`
|
||||
InitConnReceiveWindow uint64 `protobuf:"varint,9,opt,name=init_conn_receive_window,json=initConnReceiveWindow,proto3" json:"init_conn_receive_window,omitempty"`
|
||||
MaxConnReceiveWindow uint64 `protobuf:"varint,10,opt,name=max_conn_receive_window,json=maxConnReceiveWindow,proto3" json:"max_conn_receive_window,omitempty"`
|
||||
MaxIdleTimeout int64 `protobuf:"varint,11,opt,name=max_idle_timeout,json=maxIdleTimeout,proto3" json:"max_idle_timeout,omitempty"`
|
||||
KeepAlivePeriod int64 `protobuf:"varint,12,opt,name=keep_alive_period,json=keepAlivePeriod,proto3" json:"keep_alive_period,omitempty"`
|
||||
DisablePathMtuDiscovery bool `protobuf:"varint,13,opt,name=disable_path_mtu_discovery,json=disablePathMtuDiscovery,proto3" json:"disable_path_mtu_discovery,omitempty"`
|
||||
DisableChromeParrot bool `protobuf:"varint,14,opt,name=disable_chrome_parrot,json=disableChromeParrot,proto3" json:"disable_chrome_parrot,omitempty"`
|
||||
DisableGSO bool `protobuf:"varint,15,opt,name=disableGSO,proto3" json:"disableGSO,omitempty"`
|
||||
MaxIncomingStreams int64 `protobuf:"varint,16,opt,name=max_incoming_streams,json=maxIncomingStreams,proto3" json:"max_incoming_streams,omitempty"`
|
||||
DisableStatelessReset bool `protobuf:"varint,17,opt,name=disable_stateless_reset,json=disableStatelessReset,proto3" json:"disable_stateless_reset,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *QuicParams) Reset() {
|
||||
@@ -519,6 +523,13 @@ func (x *QuicParams) GetBrutalDown() uint64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *QuicParams) GetBrutalDisableLossCompensation() bool {
|
||||
if x != nil {
|
||||
return x.BrutalDisableLossCompensation
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *QuicParams) GetUdpHop() *UdpHop {
|
||||
if x != nil {
|
||||
return x.UdpHop
|
||||
@@ -575,6 +586,20 @@ func (x *QuicParams) GetDisablePathMtuDiscovery() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *QuicParams) GetDisableChromeParrot() bool {
|
||||
if x != nil {
|
||||
return x.DisableChromeParrot
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *QuicParams) GetDisableGSO() bool {
|
||||
if x != nil {
|
||||
return x.DisableGSO
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *QuicParams) GetMaxIncomingStreams() int64 {
|
||||
if x != nil {
|
||||
return x.MaxIncomingStreams
|
||||
@@ -582,6 +607,13 @@ func (x *QuicParams) GetMaxIncomingStreams() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *QuicParams) GetDisableStatelessReset() bool {
|
||||
if x != nil {
|
||||
return x.DisableStatelessReset
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ProxyConfig struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"`
|
||||
@@ -1020,7 +1052,7 @@ const file_transport_internet_config_proto_rawDesc = "" +
|
||||
"\x06UdpHop\x12\x14\n" +
|
||||
"\x05ports\x18\x01 \x03(\rR\x05ports\x12!\n" +
|
||||
"\finterval_min\x18\x02 \x01(\x03R\vintervalMin\x12!\n" +
|
||||
"\finterval_max\x18\x03 \x01(\x03R\vintervalMax\"\xf2\x04\n" +
|
||||
"\finterval_max\x18\x03 \x01(\x03R\vintervalMax\"\xc7\x06\n" +
|
||||
"\n" +
|
||||
"QuicParams\x12\x1e\n" +
|
||||
"\n" +
|
||||
@@ -1030,17 +1062,23 @@ const file_transport_internet_config_proto_rawDesc = "" +
|
||||
"bbrProfile\x12\x1b\n" +
|
||||
"\tbrutal_up\x18\x03 \x01(\x04R\bbrutalUp\x12\x1f\n" +
|
||||
"\vbrutal_down\x18\x04 \x01(\x04R\n" +
|
||||
"brutalDown\x128\n" +
|
||||
"\audp_hop\x18\x05 \x01(\v2\x1f.xray.transport.internet.UdpHopR\x06udpHop\x12;\n" +
|
||||
"\x1ainit_stream_receive_window\x18\x06 \x01(\x04R\x17initStreamReceiveWindow\x129\n" +
|
||||
"\x19max_stream_receive_window\x18\a \x01(\x04R\x16maxStreamReceiveWindow\x127\n" +
|
||||
"\x18init_conn_receive_window\x18\b \x01(\x04R\x15initConnReceiveWindow\x125\n" +
|
||||
"\x17max_conn_receive_window\x18\t \x01(\x04R\x14maxConnReceiveWindow\x12(\n" +
|
||||
"\x10max_idle_timeout\x18\n" +
|
||||
" \x01(\x03R\x0emaxIdleTimeout\x12*\n" +
|
||||
"\x11keep_alive_period\x18\v \x01(\x03R\x0fkeepAlivePeriod\x12;\n" +
|
||||
"\x1adisable_path_mtu_discovery\x18\f \x01(\bR\x17disablePathMtuDiscovery\x120\n" +
|
||||
"\x14max_incoming_streams\x18\r \x01(\x03R\x12maxIncomingStreams\"Q\n" +
|
||||
"brutalDown\x12G\n" +
|
||||
" brutal_disable_loss_compensation\x18\x05 \x01(\bR\x1dbrutalDisableLossCompensation\x128\n" +
|
||||
"\audp_hop\x18\x06 \x01(\v2\x1f.xray.transport.internet.UdpHopR\x06udpHop\x12;\n" +
|
||||
"\x1ainit_stream_receive_window\x18\a \x01(\x04R\x17initStreamReceiveWindow\x129\n" +
|
||||
"\x19max_stream_receive_window\x18\b \x01(\x04R\x16maxStreamReceiveWindow\x127\n" +
|
||||
"\x18init_conn_receive_window\x18\t \x01(\x04R\x15initConnReceiveWindow\x125\n" +
|
||||
"\x17max_conn_receive_window\x18\n" +
|
||||
" \x01(\x04R\x14maxConnReceiveWindow\x12(\n" +
|
||||
"\x10max_idle_timeout\x18\v \x01(\x03R\x0emaxIdleTimeout\x12*\n" +
|
||||
"\x11keep_alive_period\x18\f \x01(\x03R\x0fkeepAlivePeriod\x12;\n" +
|
||||
"\x1adisable_path_mtu_discovery\x18\r \x01(\bR\x17disablePathMtuDiscovery\x122\n" +
|
||||
"\x15disable_chrome_parrot\x18\x0e \x01(\bR\x13disableChromeParrot\x12\x1e\n" +
|
||||
"\n" +
|
||||
"disableGSO\x18\x0f \x01(\bR\n" +
|
||||
"disableGSO\x120\n" +
|
||||
"\x14max_incoming_streams\x18\x10 \x01(\x03R\x12maxIncomingStreams\x126\n" +
|
||||
"\x17disable_stateless_reset\x18\x11 \x01(\bR\x15disableStatelessReset\"Q\n" +
|
||||
"\vProxyConfig\x12\x10\n" +
|
||||
"\x03tag\x18\x01 \x01(\tR\x03tag\x120\n" +
|
||||
"\x13transportLayerProxy\x18\x02 \x01(\bR\x13transportLayerProxy\"\x93\x01\n" +
|
||||
|
||||
@@ -75,15 +75,19 @@ message QuicParams {
|
||||
string bbr_profile = 2;
|
||||
uint64 brutal_up = 3;
|
||||
uint64 brutal_down = 4;
|
||||
UdpHop udp_hop = 5;
|
||||
uint64 init_stream_receive_window = 6;
|
||||
uint64 max_stream_receive_window = 7;
|
||||
uint64 init_conn_receive_window = 8;
|
||||
uint64 max_conn_receive_window = 9;
|
||||
int64 max_idle_timeout = 10;
|
||||
int64 keep_alive_period = 11;
|
||||
bool disable_path_mtu_discovery = 12;
|
||||
int64 max_incoming_streams = 13;
|
||||
bool brutal_disable_loss_compensation = 5;
|
||||
UdpHop udp_hop = 6;
|
||||
uint64 init_stream_receive_window = 7;
|
||||
uint64 max_stream_receive_window = 8;
|
||||
uint64 init_conn_receive_window = 9;
|
||||
uint64 max_conn_receive_window = 10;
|
||||
int64 max_idle_timeout = 11;
|
||||
int64 keep_alive_period = 12;
|
||||
bool disable_path_mtu_discovery = 13;
|
||||
bool disable_chrome_parrot = 14;
|
||||
bool disableGSO = 15;
|
||||
int64 max_incoming_streams = 16;
|
||||
bool disable_stateless_reset = 17;
|
||||
}
|
||||
|
||||
message ProxyConfig {
|
||||
|
||||
@@ -90,6 +90,17 @@ func (c *fragmentConn) Write(p []byte) (n int, err error) {
|
||||
to = len(data)
|
||||
}
|
||||
l := to - from
|
||||
// if l is 0, don't send empty tls record(will break the connection) but still sleep
|
||||
if l == 0 {
|
||||
if !mergeHello {
|
||||
delayMin, delayMax := c.delayForSegment(int(splitNum))
|
||||
if delayMax > 0 {
|
||||
time.Sleep(time.Duration(crypto.RandBetween(delayMin, delayMax)) * time.Millisecond)
|
||||
}
|
||||
}
|
||||
splitNum++
|
||||
continue
|
||||
}
|
||||
if 5+l > len(buff) {
|
||||
buff = make([]byte, 5+l)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pion/stun/v3"
|
||||
@@ -15,35 +16,67 @@ import (
|
||||
)
|
||||
|
||||
type realmConnClient struct {
|
||||
wg sync.WaitGroup
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
net.PacketConn
|
||||
peer *net.UDPAddr
|
||||
|
||||
realmClient *Client
|
||||
realmID string
|
||||
stunServers []string
|
||||
family Family
|
||||
mapper *PortMapper
|
||||
stunTimeout time.Duration
|
||||
punchTimeout time.Duration
|
||||
punchInterval time.Duration
|
||||
}
|
||||
|
||||
func NewConnClient(config *Config, raw net.PacketConn) (net.PacketConn, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
family := Family_Dual
|
||||
switch config.IPMode {
|
||||
case "dual":
|
||||
case "v4":
|
||||
family = Family_V4
|
||||
case "v6":
|
||||
family = Family_V6
|
||||
}
|
||||
|
||||
var mapper *PortMapper
|
||||
if config.PortMapping != nil && config.PortMapping.Enabled {
|
||||
var err error
|
||||
start := time.Now()
|
||||
mapper, err = NewPortMapper(context.Background(), raw.LocalAddr().(*net.UDPAddr).Port, PortMapConfig{Timeout: time.Duration(config.PortMapping.Timeout) * time.Second, Lifetime: time.Duration(config.PortMapping.Lifetime) * time.Second})
|
||||
if err != nil {
|
||||
errors.LogErrorInner(context.Background(), err, "[realm] [port mapping] [", raw.LocalAddr().(*net.UDPAddr).Port, "] init failed after ", time.Since(start))
|
||||
} else {
|
||||
errors.LogDebug(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] gateway ", mapper.GatewayType(), ", external ", mapper.ExternalAddr())
|
||||
errors.LogDebug(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] init success with ", time.Since(start))
|
||||
}
|
||||
}
|
||||
|
||||
conn := &realmConnClient{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
PacketConn: raw,
|
||||
|
||||
realmClient: NewClient(config.Scheme, config.Host, config.Port, config.Token, config.TlsConfig),
|
||||
realmID: config.ID,
|
||||
stunServers: config.StunServers,
|
||||
family: family,
|
||||
mapper: mapper,
|
||||
stunTimeout: defaultSTUNTimeout,
|
||||
punchTimeout: defaultPunchTimeout,
|
||||
punchInterval: defaultPunchInterval,
|
||||
}
|
||||
|
||||
return conn.getpeer()
|
||||
}
|
||||
|
||||
func (c *realmConnClient) getpeer() (net.PacketConn, error) {
|
||||
start := time.Now()
|
||||
servers := resolveSTUNServers(c.PacketConn.LocalAddr().(*net.UDPAddr).IP, c.stunServers)
|
||||
servers := resolveSTUNServers(c.PacketConn.LocalAddr().(*net.UDPAddr).IP, c.stunServers, c.family)
|
||||
errors.LogDebug(context.Background(), "[realm] update stun servers ", servers, " with ", time.Since(start))
|
||||
if len(servers) == 0 {
|
||||
return nil, errors.New("empty locals")
|
||||
@@ -70,7 +103,7 @@ func (c *realmConnClient) getpeer() (net.PacketConn, error) {
|
||||
|
||||
peers, _ := parseAddrPorts(resp.Addresses)
|
||||
errors.LogDebug(context.Background(), "[realm] update peers ", peers)
|
||||
filteredPeers, seen := candidatePunchAddrs(locals, peers)
|
||||
filteredPeers, seen := candidatePunchAddrs(locals, peers, c.family)
|
||||
errors.LogDebug(context.Background(), "[realm] filtered peers ", filteredPeers)
|
||||
expandedPeers := expandSymmetricNATCandidates(filteredPeers, seen)
|
||||
errors.LogDebug(context.Background(), "[realm] expanded peers ", expandedPeers)
|
||||
@@ -86,6 +119,11 @@ func (c *realmConnClient) getpeer() (net.PacketConn, error) {
|
||||
}
|
||||
errors.LogDebug(context.Background(), "[realm] punch peer ", peer, " with ", time.Since(start))
|
||||
|
||||
if c.mapper != nil {
|
||||
c.wg.Add(1)
|
||||
go portMapLoop(c.ctx, c.mapper, c.wg.Done)
|
||||
}
|
||||
|
||||
c.peer = peer
|
||||
return c, nil
|
||||
}
|
||||
@@ -116,10 +154,12 @@ func (c *realmConnClient) discover(servers []*net.UDPAddr) []netip.AddrPort {
|
||||
}
|
||||
}
|
||||
c.PacketConn.SetReadDeadline(time.Time{})
|
||||
if c.mapper != nil {
|
||||
results = insertAddr(results, c.mapper.ExternalAddr())
|
||||
}
|
||||
slices.SortFunc(results, func(a, b netip.AddrPort) int {
|
||||
return strings.Compare(a.String(), b.String())
|
||||
})
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
@@ -169,3 +209,48 @@ func (c *realmConnClient) punch(meta PunchMetadata, peers []netip.AddrPort) (*ne
|
||||
func (c *realmConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
return c.PacketConn.WriteTo(p, c.peer)
|
||||
}
|
||||
|
||||
func (c *realmConnClient) Close() error {
|
||||
// Sadly, closing the core does not first close the sockets created for outbound connections
|
||||
c.cancel()
|
||||
c.wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func portMapLoop(ctx context.Context, mapper *PortMapper, done func()) {
|
||||
defer func() {
|
||||
err := mapper.Close()
|
||||
done()
|
||||
errors.LogDebug(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] removed with ", err)
|
||||
}()
|
||||
interval := mapper.Lifetime() / 2
|
||||
if interval <= 0 {
|
||||
interval = time.Minute
|
||||
}
|
||||
t := time.NewTicker(interval)
|
||||
defer t.Stop()
|
||||
failing := false
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
changed, err := mapper.Renew(ctx)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
if !failing {
|
||||
errors.LogError(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] renewal failed")
|
||||
failing = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
errors.LogDebug(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] external ", mapper.ExternalAddr(), ", changed ", changed)
|
||||
if failing {
|
||||
errors.LogError(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] recovered")
|
||||
failing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,115 @@ const (
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type Family int32
|
||||
|
||||
const (
|
||||
Family_Dual Family = 0
|
||||
Family_V4 Family = 1
|
||||
Family_V6 Family = 2
|
||||
)
|
||||
|
||||
// Enum value maps for Family.
|
||||
var (
|
||||
Family_name = map[int32]string{
|
||||
0: "Dual",
|
||||
1: "V4",
|
||||
2: "V6",
|
||||
}
|
||||
Family_value = map[string]int32{
|
||||
"Dual": 0,
|
||||
"V4": 1,
|
||||
"V6": 2,
|
||||
}
|
||||
)
|
||||
|
||||
func (x Family) Enum() *Family {
|
||||
p := new(Family)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x Family) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (Family) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_transport_internet_finalmask_realm_config_proto_enumTypes[0].Descriptor()
|
||||
}
|
||||
|
||||
func (Family) Type() protoreflect.EnumType {
|
||||
return &file_transport_internet_finalmask_realm_config_proto_enumTypes[0]
|
||||
}
|
||||
|
||||
func (x Family) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Family.Descriptor instead.
|
||||
func (Family) EnumDescriptor() ([]byte, []int) {
|
||||
return file_transport_internet_finalmask_realm_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
type PortMapping struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"`
|
||||
Timeout int64 `protobuf:"varint,2,opt,name=timeout,proto3" json:"timeout,omitempty"`
|
||||
Lifetime int64 `protobuf:"varint,3,opt,name=lifetime,proto3" json:"lifetime,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PortMapping) Reset() {
|
||||
*x = PortMapping{}
|
||||
mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *PortMapping) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*PortMapping) ProtoMessage() {}
|
||||
|
||||
func (x *PortMapping) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use PortMapping.ProtoReflect.Descriptor instead.
|
||||
func (*PortMapping) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_finalmask_realm_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *PortMapping) GetEnabled() bool {
|
||||
if x != nil {
|
||||
return x.Enabled
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *PortMapping) GetTimeout() int64 {
|
||||
if x != nil {
|
||||
return x.Timeout
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PortMapping) GetLifetime() int64 {
|
||||
if x != nil {
|
||||
return x.Lifetime
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Scheme string `protobuf:"bytes,1,opt,name=scheme,proto3" json:"scheme,omitempty"`
|
||||
@@ -31,13 +140,15 @@ type Config struct {
|
||||
ID string `protobuf:"bytes,5,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
StunServers []string `protobuf:"bytes,6,rep,name=stun_servers,json=stunServers,proto3" json:"stun_servers,omitempty"`
|
||||
TlsConfig *tls.Config `protobuf:"bytes,7,opt,name=tls_config,json=tlsConfig,proto3" json:"tls_config,omitempty"`
|
||||
IPMode string `protobuf:"bytes,8,opt,name=IPMode,proto3" json:"IPMode,omitempty"`
|
||||
PortMapping *PortMapping `protobuf:"bytes,9,opt,name=port_mapping,json=portMapping,proto3" json:"port_mapping,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Config) Reset() {
|
||||
*x = Config{}
|
||||
mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[0]
|
||||
mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -49,7 +160,7 @@ func (x *Config) String() string {
|
||||
func (*Config) ProtoMessage() {}
|
||||
|
||||
func (x *Config) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[0]
|
||||
mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -62,7 +173,7 @@ func (x *Config) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
|
||||
func (*Config) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_finalmask_realm_config_proto_rawDescGZIP(), []int{0}
|
||||
return file_transport_internet_finalmask_realm_config_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *Config) GetScheme() string {
|
||||
@@ -114,11 +225,29 @@ func (x *Config) GetTlsConfig() *tls.Config {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetIPMode() string {
|
||||
if x != nil {
|
||||
return x.IPMode
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetPortMapping() *PortMapping {
|
||||
if x != nil {
|
||||
return x.PortMapping
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_transport_internet_finalmask_realm_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_transport_internet_finalmask_realm_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"/transport/internet/finalmask/realm/config.proto\x12'xray.transport.internet.finalmask.realm\x1a#transport/internet/tls/config.proto\"\xd5\x01\n" +
|
||||
"/transport/internet/finalmask/realm/config.proto\x12'xray.transport.internet.finalmask.realm\x1a#transport/internet/tls/config.proto\"]\n" +
|
||||
"\vPortMapping\x12\x18\n" +
|
||||
"\aenabled\x18\x01 \x01(\bR\aenabled\x12\x18\n" +
|
||||
"\atimeout\x18\x02 \x01(\x03R\atimeout\x12\x1a\n" +
|
||||
"\blifetime\x18\x03 \x01(\x03R\blifetime\"\xc6\x02\n" +
|
||||
"\x06Config\x12\x16\n" +
|
||||
"\x06scheme\x18\x01 \x01(\tR\x06scheme\x12\x12\n" +
|
||||
"\x04host\x18\x02 \x01(\tR\x04host\x12\x12\n" +
|
||||
@@ -127,7 +256,13 @@ const file_transport_internet_finalmask_realm_config_proto_rawDesc = "" +
|
||||
"\x02ID\x18\x05 \x01(\tR\x02ID\x12!\n" +
|
||||
"\fstun_servers\x18\x06 \x03(\tR\vstunServers\x12B\n" +
|
||||
"\n" +
|
||||
"tls_config\x18\a \x01(\v2#.xray.transport.internet.tls.ConfigR\ttlsConfigB\x97\x01\n" +
|
||||
"tls_config\x18\a \x01(\v2#.xray.transport.internet.tls.ConfigR\ttlsConfig\x12\x16\n" +
|
||||
"\x06IPMode\x18\b \x01(\tR\x06IPMode\x12W\n" +
|
||||
"\fport_mapping\x18\t \x01(\v24.xray.transport.internet.finalmask.realm.PortMappingR\vportMapping*\"\n" +
|
||||
"\x06Family\x12\b\n" +
|
||||
"\x04Dual\x10\x00\x12\x06\n" +
|
||||
"\x02V4\x10\x01\x12\x06\n" +
|
||||
"\x02V6\x10\x02B\x97\x01\n" +
|
||||
"+com.xray.transport.internet.finalmask.realmP\x01Z<github.com/xtls/xray-core/transport/internet/finalmask/realm\xaa\x02'Xray.Transport.Internet.Finalmask.Realmb\x06proto3"
|
||||
|
||||
var (
|
||||
@@ -142,18 +277,22 @@ func file_transport_internet_finalmask_realm_config_proto_rawDescGZIP() []byte {
|
||||
return file_transport_internet_finalmask_realm_config_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_transport_internet_finalmask_realm_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_transport_internet_finalmask_realm_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_transport_internet_finalmask_realm_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_transport_internet_finalmask_realm_config_proto_goTypes = []any{
|
||||
(*Config)(nil), // 0: xray.transport.internet.finalmask.realm.Config
|
||||
(*tls.Config)(nil), // 1: xray.transport.internet.tls.Config
|
||||
(Family)(0), // 0: xray.transport.internet.finalmask.realm.Family
|
||||
(*PortMapping)(nil), // 1: xray.transport.internet.finalmask.realm.PortMapping
|
||||
(*Config)(nil), // 2: xray.transport.internet.finalmask.realm.Config
|
||||
(*tls.Config)(nil), // 3: xray.transport.internet.tls.Config
|
||||
}
|
||||
var file_transport_internet_finalmask_realm_config_proto_depIdxs = []int32{
|
||||
1, // 0: xray.transport.internet.finalmask.realm.Config.tls_config:type_name -> xray.transport.internet.tls.Config
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
3, // 0: xray.transport.internet.finalmask.realm.Config.tls_config:type_name -> xray.transport.internet.tls.Config
|
||||
1, // 1: xray.transport.internet.finalmask.realm.Config.port_mapping:type_name -> xray.transport.internet.finalmask.realm.PortMapping
|
||||
2, // [2:2] is the sub-list for method output_type
|
||||
2, // [2:2] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
2, // [2:2] is the sub-list for extension extendee
|
||||
0, // [0:2] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_transport_internet_finalmask_realm_config_proto_init() }
|
||||
@@ -166,13 +305,14 @@ func file_transport_internet_finalmask_realm_config_proto_init() {
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_realm_config_proto_rawDesc), len(file_transport_internet_finalmask_realm_config_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumEnums: 1,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_transport_internet_finalmask_realm_config_proto_goTypes,
|
||||
DependencyIndexes: file_transport_internet_finalmask_realm_config_proto_depIdxs,
|
||||
EnumInfos: file_transport_internet_finalmask_realm_config_proto_enumTypes,
|
||||
MessageInfos: file_transport_internet_finalmask_realm_config_proto_msgTypes,
|
||||
}.Build()
|
||||
File_transport_internet_finalmask_realm_config_proto = out.File
|
||||
|
||||
@@ -8,6 +8,18 @@ option java_multiple_files = true;
|
||||
|
||||
import "transport/internet/tls/config.proto";
|
||||
|
||||
enum Family {
|
||||
Dual = 0;
|
||||
V4 = 1;
|
||||
V6 = 2;
|
||||
}
|
||||
|
||||
message PortMapping {
|
||||
bool enabled = 1;
|
||||
int64 timeout = 2;
|
||||
int64 lifetime = 3;
|
||||
}
|
||||
|
||||
message Config {
|
||||
string scheme = 1;
|
||||
string host = 2;
|
||||
@@ -16,4 +28,6 @@ message Config {
|
||||
string ID = 5;
|
||||
repeated string stun_servers = 6;
|
||||
xray.transport.internet.tls.Config tls_config = 7;
|
||||
string IPMode = 8;
|
||||
PortMapping port_mapping = 9;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package realm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/libp2p/go-nat"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPortMapTimeout = 10 * time.Second
|
||||
defaultPortMapLifetime = 10 * time.Minute
|
||||
|
||||
portMapDescription = "hysteria-realm"
|
||||
portMapProtocol = "udp"
|
||||
)
|
||||
|
||||
var ErrInvalidPortMapConfig = errors.New("invalid port mapping config")
|
||||
|
||||
type PortMapConfig struct {
|
||||
Timeout time.Duration
|
||||
Lifetime time.Duration
|
||||
}
|
||||
|
||||
func (c PortMapConfig) withDefaults() (PortMapConfig, error) {
|
||||
if c.Timeout == 0 {
|
||||
c.Timeout = defaultPortMapTimeout
|
||||
}
|
||||
if c.Timeout < 0 {
|
||||
return c, fmt.Errorf("%w: timeout must not be negative", ErrInvalidPortMapConfig)
|
||||
}
|
||||
if c.Lifetime == 0 {
|
||||
c.Lifetime = defaultPortMapLifetime
|
||||
}
|
||||
if c.Lifetime < 0 {
|
||||
return c, fmt.Errorf("%w: lifetime must not be negative", ErrInvalidPortMapConfig)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// PortMapper maintains a UDP port mapping on the local gateway via UPnP or
|
||||
// NAT-PMP. It does not renew the mapping by itself; the caller is expected
|
||||
// to call Renew periodically (typically every Lifetime/2).
|
||||
type PortMapper struct {
|
||||
gateway nat.NAT
|
||||
internalPort int
|
||||
config PortMapConfig
|
||||
|
||||
mu sync.Mutex
|
||||
externalAddr netip.AddrPort
|
||||
}
|
||||
|
||||
// NewPortMapper discovers the local gateway and maps internalPort for UDP.
|
||||
// It blocks for up to 2x config.Timeout (discovery + mapping).
|
||||
func NewPortMapper(ctx context.Context, internalPort int, config PortMapConfig) (*PortMapper, error) {
|
||||
if internalPort <= 0 || internalPort > 65535 {
|
||||
return nil, fmt.Errorf("%w: invalid internal port %d", ErrInvalidPortMapConfig, internalPort)
|
||||
}
|
||||
config, err := config.withDefaults()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
discoverCtx, cancel := context.WithTimeout(ctx, config.Timeout)
|
||||
gateway, err := nat.DiscoverGateway(discoverCtx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gateway discovery failed: %w", err)
|
||||
}
|
||||
|
||||
m := &PortMapper{
|
||||
gateway: gateway,
|
||||
internalPort: internalPort,
|
||||
config: config,
|
||||
}
|
||||
if _, err := m.Renew(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Renew (re-)requests the port mapping and refreshes the external address.
|
||||
// It reports whether the external address changed since the last call.
|
||||
func (m *PortMapper) Renew(ctx context.Context) (bool, error) {
|
||||
opCtx, cancel := context.WithTimeout(ctx, m.config.Timeout)
|
||||
defer cancel()
|
||||
externalPort, err := m.gateway.AddPortMapping(opCtx, portMapProtocol, m.internalPort, portMapDescription, m.config.Lifetime)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("add port mapping failed: %w", err)
|
||||
}
|
||||
externalIP, err := m.gateway.GetExternalAddress()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("get external address failed: %w", err)
|
||||
}
|
||||
addr, ok := netip.AddrFromSlice(externalIP)
|
||||
if !ok || addr.IsUnspecified() || addr.IsLoopback() {
|
||||
return false, fmt.Errorf("gateway returned unusable external address: %s", externalIP)
|
||||
}
|
||||
externalAddr := netip.AddrPortFrom(addr.Unmap(), uint16(externalPort))
|
||||
|
||||
m.mu.Lock()
|
||||
changed := externalAddr != m.externalAddr
|
||||
m.externalAddr = externalAddr
|
||||
m.mu.Unlock()
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
// ExternalAddr returns the gateway's external IP and the mapped external port.
|
||||
func (m *PortMapper) ExternalAddr() netip.AddrPort {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.externalAddr
|
||||
}
|
||||
|
||||
// InternalPort returns the mapped local UDP port.
|
||||
func (m *PortMapper) InternalPort() int {
|
||||
return m.internalPort
|
||||
}
|
||||
|
||||
// Lifetime returns the effective mapping lease duration.
|
||||
func (m *PortMapper) Lifetime() time.Duration {
|
||||
return m.config.Lifetime
|
||||
}
|
||||
|
||||
// GatewayType returns the protocol used to talk to the gateway ("UPnP" or "NAT-PMP").
|
||||
func (m *PortMapper) GatewayType() string {
|
||||
return m.gateway.Type()
|
||||
}
|
||||
|
||||
// Close removes the port mapping from the gateway. Best-effort.
|
||||
func (m *PortMapper) Close() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), m.config.Timeout)
|
||||
defer cancel()
|
||||
return m.gateway.DeletePortMapping(ctx, portMapProtocol, m.internalPort)
|
||||
}
|
||||
@@ -33,14 +33,16 @@ type STUNPacketEvent struct {
|
||||
}
|
||||
|
||||
type realmConnServer struct {
|
||||
cleaned chan struct{}
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
net.PacketConn
|
||||
|
||||
realmClient *Client
|
||||
realmID string
|
||||
stunServers []string
|
||||
family Family
|
||||
mapper *PortMapper
|
||||
stunTimeout time.Duration
|
||||
punchTimeout time.Duration
|
||||
punchInterval time.Duration
|
||||
@@ -57,8 +59,29 @@ type realmConnServer struct {
|
||||
func NewConnServer(config *Config, raw net.PacketConn) (net.PacketConn, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
family := Family_Dual
|
||||
switch config.IPMode {
|
||||
case "dual":
|
||||
case "v4":
|
||||
family = Family_V4
|
||||
case "v6":
|
||||
family = Family_V6
|
||||
}
|
||||
|
||||
var mapper *PortMapper
|
||||
if config.PortMapping != nil && config.PortMapping.Enabled {
|
||||
var err error
|
||||
start := time.Now()
|
||||
mapper, err = NewPortMapper(context.Background(), raw.LocalAddr().(*net.UDPAddr).Port, PortMapConfig{Timeout: time.Duration(config.PortMapping.Timeout) * time.Second, Lifetime: time.Duration(config.PortMapping.Lifetime) * time.Second})
|
||||
if err != nil {
|
||||
errors.LogErrorInner(context.Background(), err, "[realm] [port mapping] [", raw.LocalAddr().(*net.UDPAddr).Port, "] init failed after ", time.Since(start))
|
||||
} else {
|
||||
errors.LogDebug(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] gateway ", mapper.GatewayType(), ", external ", mapper.ExternalAddr())
|
||||
errors.LogDebug(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] init success with ", time.Since(start))
|
||||
}
|
||||
}
|
||||
|
||||
conn := &realmConnServer{
|
||||
cleaned: make(chan struct{}),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
PacketConn: raw,
|
||||
@@ -66,6 +89,8 @@ func NewConnServer(config *Config, raw net.PacketConn) (net.PacketConn, error) {
|
||||
realmClient: NewClient(config.Scheme, config.Host, config.Port, config.Token, config.TlsConfig),
|
||||
realmID: config.ID,
|
||||
stunServers: config.StunServers,
|
||||
family: family,
|
||||
mapper: mapper,
|
||||
stunTimeout: defaultSTUNTimeout,
|
||||
punchTimeout: defaultPunchTimeout,
|
||||
punchInterval: defaultPunchInterval,
|
||||
@@ -74,6 +99,12 @@ func NewConnServer(config *Config, raw net.PacketConn) (net.PacketConn, error) {
|
||||
stun: make(chan STUNPacketEvent, defaultEventBuffer),
|
||||
}
|
||||
|
||||
if mapper != nil {
|
||||
conn.wg.Add(1)
|
||||
go portMapLoop(ctx, mapper, conn.wg.Done)
|
||||
}
|
||||
|
||||
conn.wg.Add(1)
|
||||
go conn.run()
|
||||
|
||||
return conn, nil
|
||||
@@ -137,6 +168,8 @@ func (c *realmConnServer) discover(servers []*net.UDPAddr) []netip.AddrPort {
|
||||
results := make([]netip.AddrPort, 0, len(servers))
|
||||
for len(transactionIDs) > 0 {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
goto end
|
||||
case <-deadline.C:
|
||||
goto end
|
||||
case ev := <-c.stun:
|
||||
@@ -148,6 +181,9 @@ func (c *realmConnServer) discover(servers []*net.UDPAddr) []netip.AddrPort {
|
||||
}
|
||||
end:
|
||||
deadline.Stop()
|
||||
if c.mapper != nil {
|
||||
results = insertAddr(results, c.mapper.ExternalAddr())
|
||||
}
|
||||
slices.SortFunc(results, func(a, b netip.AddrPort) int {
|
||||
return strings.Compare(a.String(), b.String())
|
||||
})
|
||||
@@ -159,7 +195,7 @@ func (c *realmConnServer) getlocals(force bool) []netip.AddrPort {
|
||||
c.localsMu.Lock()
|
||||
if force || time.Since(c.localsLast) > defaultStunCacheTTL {
|
||||
start := time.Now()
|
||||
servers := resolveSTUNServers(c.PacketConn.LocalAddr().(*net.UDPAddr).IP, c.stunServers)
|
||||
servers := resolveSTUNServers(c.PacketConn.LocalAddr().(*net.UDPAddr).IP, c.stunServers, c.family)
|
||||
errors.LogDebug(context.Background(), "[realm] update stun servers ", servers, " with ", time.Since(start))
|
||||
if len(servers) > 0 {
|
||||
start = time.Now()
|
||||
@@ -232,7 +268,7 @@ retry:
|
||||
if err != nil {
|
||||
errors.LogErrorInner(context.Background(), err, "[realm] ", c.realmID, " register session err retry in ", backoff)
|
||||
if c.waitctx(c.ctx, backoff) {
|
||||
close(c.cleaned)
|
||||
c.wg.Done()
|
||||
return
|
||||
}
|
||||
backoff *= 2
|
||||
@@ -259,7 +295,7 @@ retry:
|
||||
case <-c.ctx.Done():
|
||||
_ = c.realmClient.Deregister(context.Background(), c.realmID, resp.SessionID)
|
||||
errors.LogDebug(context.Background(), "[realm] ", c.realmID, " ", resp.SessionID, " deregistered")
|
||||
close(c.cleaned)
|
||||
c.wg.Done()
|
||||
return
|
||||
default:
|
||||
goto retry
|
||||
@@ -360,7 +396,7 @@ func (c *realmConnServer) punchEvent(ctx context.Context, sid string, ev *PunchE
|
||||
|
||||
peers, _ := parseAddrPorts(ev.Addresses)
|
||||
errors.LogDebug(context.Background(), "[realm] ", ev.Nonce, " update peers ", peers)
|
||||
filteredPeers, seen := candidatePunchAddrs(locals, peers)
|
||||
filteredPeers, seen := candidatePunchAddrs(locals, peers, c.family)
|
||||
errors.LogDebug(context.Background(), "[realm] ", ev.Nonce, " filtered peers ", filteredPeers)
|
||||
expandedPeers := expandSymmetricNATCandidates(filteredPeers, seen)
|
||||
errors.LogDebug(context.Background(), "[realm] ", ev.Nonce, " expanded peers ", expandedPeers)
|
||||
@@ -398,6 +434,6 @@ func (c *realmConnServer) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||
|
||||
func (c *realmConnServer) Close() error {
|
||||
c.cancel()
|
||||
<-c.cleaned
|
||||
c.wg.Wait()
|
||||
return c.PacketConn.Close()
|
||||
}
|
||||
|
||||
@@ -23,14 +23,23 @@ const (
|
||||
symmetricNATMaxPortsPerHost = 32
|
||||
)
|
||||
|
||||
func resolveSTUNServers(local net.IP, servers []string) []*net.UDPAddr {
|
||||
func resolveSTUNServers(local net.IP, servers []string, family Family) []*net.UDPAddr {
|
||||
var network string
|
||||
if local.IsUnspecified() {
|
||||
network = "ip"
|
||||
} else {
|
||||
if local.To4() != nil {
|
||||
network = "ip4"
|
||||
if family == Family_Dual {
|
||||
if local.IsUnspecified() {
|
||||
network = "ip"
|
||||
} else {
|
||||
if local.To4() != nil {
|
||||
network = "ip4"
|
||||
} else {
|
||||
network = "ip6"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if family == Family_V4 {
|
||||
network = "ip4"
|
||||
}
|
||||
if family == Family_V6 {
|
||||
network = "ip6"
|
||||
}
|
||||
}
|
||||
@@ -104,7 +113,7 @@ func netIPPortToAddrPort(ip net.IP, port int) (netip.AddrPort, error) {
|
||||
return netip.AddrPortFrom(netip.AddrFrom16(addr), uint16(port)), nil
|
||||
}
|
||||
|
||||
func candidatePunchAddrs(locals, peers []netip.AddrPort) ([]netip.AddrPort, map[netip.AddrPort]struct{}) {
|
||||
func candidatePunchAddrs(locals, peers []netip.AddrPort, family Family) ([]netip.AddrPort, map[netip.AddrPort]struct{}) {
|
||||
var allow4, allow6 bool
|
||||
for _, local := range locals {
|
||||
if local.Addr().Is4() {
|
||||
@@ -116,6 +125,12 @@ func candidatePunchAddrs(locals, peers []netip.AddrPort) ([]netip.AddrPort, map[
|
||||
break
|
||||
}
|
||||
}
|
||||
if family == Family_V4 {
|
||||
allow6 = false
|
||||
}
|
||||
if family == Family_V6 {
|
||||
allow4 = false
|
||||
}
|
||||
seen := make(map[netip.AddrPort]struct{}, len(peers))
|
||||
candidates := make([]netip.AddrPort, 0, len(peers))
|
||||
for _, peer := range peers {
|
||||
@@ -218,3 +233,17 @@ func parseAddrPorts(addrs []string) ([]netip.AddrPort, error) {
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func insertAddr(addrs []netip.AddrPort, addr netip.AddrPort) []netip.AddrPort {
|
||||
if !addr.IsValid() {
|
||||
return addrs
|
||||
}
|
||||
out := append([]netip.AddrPort(nil), addrs...)
|
||||
i, found := slices.BinarySearchFunc(out, addr, func(a, b netip.AddrPort) int {
|
||||
return strings.Compare(a.String(), b.String())
|
||||
})
|
||||
if found {
|
||||
return out
|
||||
}
|
||||
return slices.Insert(out, i, addr)
|
||||
}
|
||||
|
||||
@@ -29,10 +29,11 @@ type Config struct {
|
||||
MasqFile string `protobuf:"bytes,5,opt,name=masq_file,json=masqFile,proto3" json:"masq_file,omitempty"`
|
||||
MasqUrl string `protobuf:"bytes,6,opt,name=masq_url,json=masqUrl,proto3" json:"masq_url,omitempty"`
|
||||
MasqUrlRewriteHost bool `protobuf:"varint,7,opt,name=masq_url_rewrite_host,json=masqUrlRewriteHost,proto3" json:"masq_url_rewrite_host,omitempty"`
|
||||
MasqUrlInsecure bool `protobuf:"varint,8,opt,name=masq_url_insecure,json=masqUrlInsecure,proto3" json:"masq_url_insecure,omitempty"`
|
||||
MasqString string `protobuf:"bytes,9,opt,name=masq_string,json=masqString,proto3" json:"masq_string,omitempty"`
|
||||
MasqStringHeaders map[string]string `protobuf:"bytes,10,rep,name=masq_string_headers,json=masqStringHeaders,proto3" json:"masq_string_headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
MasqStringStatusCode int32 `protobuf:"varint,11,opt,name=masq_string_status_code,json=masqStringStatusCode,proto3" json:"masq_string_status_code,omitempty"`
|
||||
MasqUrlXForwarded bool `protobuf:"varint,8,opt,name=masq_url_x_forwarded,json=masqUrlXForwarded,proto3" json:"masq_url_x_forwarded,omitempty"`
|
||||
MasqUrlInsecure bool `protobuf:"varint,9,opt,name=masq_url_insecure,json=masqUrlInsecure,proto3" json:"masq_url_insecure,omitempty"`
|
||||
MasqString string `protobuf:"bytes,10,opt,name=masq_string,json=masqString,proto3" json:"masq_string,omitempty"`
|
||||
MasqStringHeaders map[string]string `protobuf:"bytes,11,rep,name=masq_string_headers,json=masqStringHeaders,proto3" json:"masq_string_headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
MasqStringStatusCode int32 `protobuf:"varint,12,opt,name=masq_string_status_code,json=masqStringStatusCode,proto3" json:"masq_string_status_code,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -109,6 +110,13 @@ func (x *Config) GetMasqUrlRewriteHost() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Config) GetMasqUrlXForwarded() bool {
|
||||
if x != nil {
|
||||
return x.MasqUrlXForwarded
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Config) GetMasqUrlInsecure() bool {
|
||||
if x != nil {
|
||||
return x.MasqUrlInsecure
|
||||
@@ -141,20 +149,21 @@ var File_transport_internet_hysteria_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_transport_internet_hysteria_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"(transport/internet/hysteria/config.proto\x12 xray.transport.internet.hysteria\"\x8f\x04\n" +
|
||||
"(transport/internet/hysteria/config.proto\x12 xray.transport.internet.hysteria\"\xc0\x04\n" +
|
||||
"\x06Config\x12\x12\n" +
|
||||
"\x04auth\x18\x02 \x01(\tR\x04auth\x12(\n" +
|
||||
"\x10udp_idle_timeout\x18\x03 \x01(\x03R\x0eudpIdleTimeout\x12\x1b\n" +
|
||||
"\tmasq_type\x18\x04 \x01(\tR\bmasqType\x12\x1b\n" +
|
||||
"\tmasq_file\x18\x05 \x01(\tR\bmasqFile\x12\x19\n" +
|
||||
"\bmasq_url\x18\x06 \x01(\tR\amasqUrl\x121\n" +
|
||||
"\x15masq_url_rewrite_host\x18\a \x01(\bR\x12masqUrlRewriteHost\x12*\n" +
|
||||
"\x11masq_url_insecure\x18\b \x01(\bR\x0fmasqUrlInsecure\x12\x1f\n" +
|
||||
"\vmasq_string\x18\t \x01(\tR\n" +
|
||||
"\x15masq_url_rewrite_host\x18\a \x01(\bR\x12masqUrlRewriteHost\x12/\n" +
|
||||
"\x14masq_url_x_forwarded\x18\b \x01(\bR\x11masqUrlXForwarded\x12*\n" +
|
||||
"\x11masq_url_insecure\x18\t \x01(\bR\x0fmasqUrlInsecure\x12\x1f\n" +
|
||||
"\vmasq_string\x18\n" +
|
||||
" \x01(\tR\n" +
|
||||
"masqString\x12o\n" +
|
||||
"\x13masq_string_headers\x18\n" +
|
||||
" \x03(\v2?.xray.transport.internet.hysteria.Config.MasqStringHeadersEntryR\x11masqStringHeaders\x125\n" +
|
||||
"\x17masq_string_status_code\x18\v \x01(\x05R\x14masqStringStatusCode\x1aD\n" +
|
||||
"\x13masq_string_headers\x18\v \x03(\v2?.xray.transport.internet.hysteria.Config.MasqStringHeadersEntryR\x11masqStringHeaders\x125\n" +
|
||||
"\x17masq_string_status_code\x18\f \x01(\x05R\x14masqStringStatusCode\x1aD\n" +
|
||||
"\x16MasqStringHeadersEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x01\x10\x02B\x82\x01\n" +
|
||||
|
||||
@@ -11,12 +11,17 @@ message Config {
|
||||
string auth = 2;
|
||||
|
||||
int64 udp_idle_timeout = 3;
|
||||
|
||||
string masq_type = 4;
|
||||
|
||||
string masq_file = 5;
|
||||
|
||||
string masq_url = 6;
|
||||
bool masq_url_rewrite_host = 7;
|
||||
bool masq_url_insecure = 8;
|
||||
string masq_string = 9;
|
||||
map<string, string> masq_string_headers = 10;
|
||||
int32 masq_string_status_code = 11;
|
||||
bool masq_url_x_forwarded = 8;
|
||||
bool masq_url_insecure = 9;
|
||||
|
||||
string masq_string = 10;
|
||||
map<string, string> masq_string_headers = 11;
|
||||
int32 masq_string_status_code = 12;
|
||||
}
|
||||
@@ -33,6 +33,8 @@ type BrutalSender struct {
|
||||
pktInfoSlots [pktInfoSlotCount]pktInfo
|
||||
ackRate float64
|
||||
|
||||
disableLossCompensation bool
|
||||
|
||||
debug bool
|
||||
lastAckPrintTimestamp int64
|
||||
}
|
||||
@@ -43,13 +45,14 @@ type pktInfo struct {
|
||||
LossCount uint64
|
||||
}
|
||||
|
||||
func NewBrutalSender(bps uint64) *BrutalSender {
|
||||
func NewBrutalSender(bps uint64, disableLossCompensation bool) *BrutalSender {
|
||||
debug, _ := strconv.ParseBool(os.Getenv(debugEnv))
|
||||
bs := &BrutalSender{
|
||||
bps: congestion.ByteCount(bps),
|
||||
maxDatagramSize: congestion.InitialPacketSize,
|
||||
ackRate: 1,
|
||||
debug: debug,
|
||||
bps: congestion.ByteCount(bps),
|
||||
maxDatagramSize: congestion.InitialPacketSize,
|
||||
ackRate: 1,
|
||||
disableLossCompensation: disableLossCompensation,
|
||||
debug: debug,
|
||||
}
|
||||
bs.pacer = common.NewPacer(func() congestion.ByteCount {
|
||||
return congestion.ByteCount(float64(bs.bps) / bs.ackRate)
|
||||
@@ -127,6 +130,10 @@ func (b *BrutalSender) SetMaxDatagramSize(size congestion.ByteCount) {
|
||||
}
|
||||
|
||||
func (b *BrutalSender) updateAckRate(currentTimestamp int64) {
|
||||
if b.disableLossCompensation {
|
||||
b.ackRate = 1
|
||||
return
|
||||
}
|
||||
minTimestamp := currentTimestamp - pktInfoSlotCount
|
||||
var ackCount, lossCount uint64
|
||||
for _, info := range b.pktInfoSlots {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package brutal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/quic-go/congestion"
|
||||
"github.com/apernet/quic-go/monotime"
|
||||
)
|
||||
|
||||
// feedAckRate drives a single sampling slot with the given number of acked and
|
||||
// lost packets and returns the resulting ackRate.
|
||||
func feedAckRate(disableLossCompensation bool, ackCount, lossCount int) float64 {
|
||||
b := NewBrutalSender(1000000, disableLossCompensation)
|
||||
acked := make([]congestion.AckedPacketInfo, ackCount)
|
||||
lost := make([]congestion.LostPacketInfo, lossCount)
|
||||
// eventTime lands in a fixed slot; a single event carries enough samples.
|
||||
b.OnCongestionEventEx(0, monotime.Time(5*time.Second), acked, lost)
|
||||
return b.ackRate
|
||||
}
|
||||
|
||||
func TestBrutalLossCompensation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ack, loss int
|
||||
want float64 // expected ackRate when compensation is ENABLED
|
||||
}{
|
||||
{"no loss", 100, 0, 1.0},
|
||||
{"20% loss", 80, 20, 0.8},
|
||||
{"50% loss clamps to floor", 50, 50, minAckRate}, // 0.5 clamped up to 0.8
|
||||
{"few samples stays 1", 10, 5, 1.0}, // below minSampleCount
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Compensation enabled (default behavior): ackRate reacts to loss.
|
||||
if got := feedAckRate(false, tt.ack, tt.loss); got != tt.want {
|
||||
t.Errorf("compensation on: ackRate = %v, want %v", got, tt.want)
|
||||
}
|
||||
// Compensation disabled: ackRate must stay pinned at 1 regardless.
|
||||
if got := feedAckRate(true, tt.ack, tt.loss); got != 1.0 {
|
||||
t.Errorf("compensation off: ackRate = %v, want 1.0", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/apernet/quic-go"
|
||||
"github.com/apernet/quic-go/congestion"
|
||||
"github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr"
|
||||
"github.com/xtls/xray-core/transport/internet/hysteria/congestion/brutal"
|
||||
)
|
||||
@@ -36,13 +37,29 @@ func NormalizeBBRProfile(profile string) (string, error) {
|
||||
func UseBBR(conn *quic.Conn, profile bbr.Profile) {
|
||||
conn.SetCongestionControl(bbr.NewBbrSender(
|
||||
bbr.DefaultClock{},
|
||||
bbr.GetInitialPacketSize(conn.RemoteAddr()),
|
||||
seedPacketSize(conn.InitialPacketSize(), bbr.GetInitialPacketSize(conn.RemoteAddr())),
|
||||
profile,
|
||||
))
|
||||
}
|
||||
|
||||
func UseBrutal(conn *quic.Conn, tx uint64) {
|
||||
conn.SetCongestionControl(brutal.NewBrutalSender(tx))
|
||||
// seedPacketSize picks the datagram size to seed a replacement congestion
|
||||
// controller with, given the size QUIC itself starts at and the guess derived
|
||||
// from the remote address.
|
||||
//
|
||||
// The seed must not exceed what QUIC actually starts at. If it does, the first
|
||||
// path MTU probe can land between the two: QUIC sees an increase and reports
|
||||
// it, but the controller sees a decrease, which it cannot represent. Taking the
|
||||
// smaller of the two keeps the address-based guess as a floor for connections
|
||||
// whose path we can't reason about, while never seeding above QUIC.
|
||||
func seedPacketSize(quicSize, byAddr congestion.ByteCount) congestion.ByteCount {
|
||||
if quicSize <= 0 {
|
||||
return byAddr
|
||||
}
|
||||
return min(quicSize, byAddr)
|
||||
}
|
||||
|
||||
func UseBrutal(conn *quic.Conn, tx uint64, disableLossCompensation bool) {
|
||||
conn.SetCongestionControl(brutal.NewBrutalSender(tx, disableLossCompensation))
|
||||
}
|
||||
|
||||
func UseConfigured(conn *quic.Conn, congestionType, bbrProfile string) {
|
||||
|
||||
@@ -90,6 +90,7 @@ func (c *client) dial(ctx context.Context) error {
|
||||
MaxIdleTimeout: time.Duration(quicParams.MaxIdleTimeout) * time.Second,
|
||||
KeepAlivePeriod: time.Duration(quicParams.KeepAlivePeriod) * time.Second,
|
||||
DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery || (runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin"),
|
||||
ChromeParrot: !quicParams.DisableChromeParrot,
|
||||
EnableDatagrams: true,
|
||||
MaxDatagramFrameSize: MaxDatagramFrameSize,
|
||||
OmitMaxDatagramFrameSize: time.Now().After(time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)),
|
||||
@@ -172,7 +173,12 @@ func (c *client) dial(ctx context.Context) error {
|
||||
pktConn = newConn
|
||||
}
|
||||
|
||||
tr := &quic.Transport{Conn: pktConn}
|
||||
tr := &quic.Transport{Conn: pktConn, DisableGSO: quicParams.DisableGSO}
|
||||
|
||||
if !quicParams.DisableChromeParrot {
|
||||
tr.ConnectionIDGenerator = quic.ZeroLengthConnectionIDGenerator{}
|
||||
c.tlsConfig.GetCertificate = nil
|
||||
}
|
||||
|
||||
var conn *quic.Conn
|
||||
rt := &http3.Transport{
|
||||
@@ -219,6 +225,7 @@ func (c *client) dial(ctx context.Context) error {
|
||||
|
||||
// udp, _ := strconv.ParseBool(resp.Header.Get(ResponseHeaderUDPEnabled))
|
||||
down, _ := strconv.ParseUint(resp.Header.Get(CommonHeaderCCRX), 10, 64)
|
||||
errors.LogDebug(context.Background(), "ECHAccepted ", conn.ConnectionState().TLS.ECHAccepted)
|
||||
|
||||
switch quicParams.Congestion {
|
||||
case "reno":
|
||||
@@ -228,10 +235,10 @@ func (c *client) dial(ctx context.Context) error {
|
||||
if quicParams.BrutalUp == 0 || down == 0 {
|
||||
congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile))
|
||||
} else {
|
||||
congestion.UseBrutal(conn, min(quicParams.BrutalUp, down))
|
||||
congestion.UseBrutal(conn, min(quicParams.BrutalUp, down), quicParams.BrutalDisableLossCompensation)
|
||||
}
|
||||
case "force-brutal":
|
||||
congestion.UseBrutal(conn, quicParams.BrutalUp)
|
||||
congestion.UseBrutal(conn, quicParams.BrutalUp, quicParams.BrutalDisableLossCompensation)
|
||||
default:
|
||||
panic(quicParams.Congestion)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package hysteria
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
gotls "crypto/tls"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
@@ -78,10 +79,10 @@ func (h *httpHandler) AuthHTTP(w http.ResponseWriter, r *http.Request) bool {
|
||||
if quicParams.BrutalUp == 0 || down == 0 {
|
||||
congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile))
|
||||
} else {
|
||||
congestion.UseBrutal(conn, min(quicParams.BrutalUp, down))
|
||||
congestion.UseBrutal(conn, min(quicParams.BrutalUp, down), quicParams.BrutalDisableLossCompensation)
|
||||
}
|
||||
case "force-brutal":
|
||||
congestion.UseBrutal(conn, quicParams.BrutalUp)
|
||||
congestion.UseBrutal(conn, quicParams.BrutalUp, quicParams.BrutalDisableLossCompensation)
|
||||
default:
|
||||
panic(quicParams.Congestion)
|
||||
}
|
||||
@@ -165,8 +166,9 @@ func (l *Listener) handleClient(conn *quic.Conn) {
|
||||
Handler: handler,
|
||||
StreamDispatcher: handler.StreamDispatcher,
|
||||
}
|
||||
_ = h3s.ServeQUICConn(conn)
|
||||
err := h3s.ServeQUICConn(conn)
|
||||
_ = conn.CloseWithError(closeErrCodeOK, "")
|
||||
errors.LogDebug(context.Background(), conn.RemoteAddr(), " ServeQUICConn exited with ", err)
|
||||
}
|
||||
|
||||
func (l *Listener) keepAccepting() {
|
||||
@@ -219,21 +221,43 @@ func Listen(ctx context.Context, address net.Address, port net.Port, streamSetti
|
||||
return nil, err
|
||||
}
|
||||
transport := http.DefaultTransport.(*http.Transport)
|
||||
if config.MasqUrlInsecure {
|
||||
transport = transport.Clone()
|
||||
transport.TLSClientConfig = &gotls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
switch u.Scheme {
|
||||
case "http", "https":
|
||||
if config.MasqUrlInsecure {
|
||||
transport = transport.Clone()
|
||||
if transport.TLSClientConfig == nil {
|
||||
transport.TLSClientConfig = &gotls.Config{}
|
||||
}
|
||||
transport.TLSClientConfig.InsecureSkipVerify = true
|
||||
}
|
||||
case "", "unix":
|
||||
u = &url.URL{Scheme: "http", Host: "localhost"}
|
||||
path := u.Path
|
||||
dialer := &net.Dialer{Timeout: 30 * time.Second}
|
||||
transport = transport.Clone()
|
||||
transport.Proxy = nil
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, "unix", path)
|
||||
}
|
||||
transport.MaxIdleConns = masqueradeProxyMaxIdleConnections
|
||||
transport.MaxIdleConnsPerHost = masqueradeProxyMaxIdleConnsPerHost
|
||||
default:
|
||||
return nil, errors.New("unknown scheme")
|
||||
}
|
||||
masqHandler = &httputil.ReverseProxy{
|
||||
Rewrite: func(pr *httputil.ProxyRequest) {
|
||||
pr.SetURL(u)
|
||||
Rewrite: func(r *httputil.ProxyRequest) {
|
||||
r.SetURL(u)
|
||||
if !config.MasqUrlRewriteHost {
|
||||
pr.Out.Host = pr.In.Host
|
||||
r.Out.Host = r.In.Host
|
||||
}
|
||||
if config.MasqUrlXForwarded {
|
||||
r.SetXForwarded()
|
||||
}
|
||||
},
|
||||
Transport: transport,
|
||||
Transport: transport,
|
||||
BufferPool: newMasqueradeProxyBufferPool(),
|
||||
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
errors.LogErrorInner(context.Background(), err, "HTTP reverse proxy error")
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
},
|
||||
}
|
||||
@@ -307,7 +331,13 @@ func Listen(ctx context.Context, address net.Address, port net.Port, streamSetti
|
||||
pktConn = newConn
|
||||
}
|
||||
|
||||
tr := &quic.Transport{Conn: pktConn}
|
||||
var k *quic.StatelessResetKey
|
||||
if !quicParams.DisableStatelessReset {
|
||||
k = &quic.StatelessResetKey{}
|
||||
common.Must2(rand.Read((*k)[:]))
|
||||
}
|
||||
|
||||
tr := &quic.Transport{Conn: pktConn, DisableGSO: quicParams.DisableGSO, StatelessResetKey: k}
|
||||
|
||||
listener, err := tr.Listen(tlsConfig.GetTLSConfig(tls.WithNextProto("h3")), quicConfig)
|
||||
if err != nil {
|
||||
@@ -336,3 +366,34 @@ func Listen(ctx context.Context, address net.Address, port net.Port, streamSetti
|
||||
func init() {
|
||||
common.Must(internet.RegisterTransportListener(protocolName, Listen))
|
||||
}
|
||||
|
||||
const (
|
||||
masqueradeProxyBufferSize = 32 * 1024
|
||||
masqueradeProxyMaxIdleConnections = 100
|
||||
masqueradeProxyMaxIdleConnsPerHost = 32
|
||||
)
|
||||
|
||||
type masqueradeProxyBufferPool struct {
|
||||
pool sync.Pool
|
||||
}
|
||||
|
||||
func newMasqueradeProxyBufferPool() *masqueradeProxyBufferPool {
|
||||
return &masqueradeProxyBufferPool{
|
||||
pool: sync.Pool{
|
||||
New: func() any {
|
||||
return make([]byte, masqueradeProxyBufferSize)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *masqueradeProxyBufferPool) Get() []byte {
|
||||
return p.pool.Get().([]byte)
|
||||
}
|
||||
|
||||
func (p *masqueradeProxyBufferPool) Put(buf []byte) {
|
||||
if cap(buf) < masqueradeProxyBufferSize {
|
||||
return
|
||||
}
|
||||
p.pool.Put(buf[:masqueradeProxyBufferSize])
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// +build js netbsd openbsd solaris
|
||||
//go:build js || netbsd || solaris
|
||||
// +build js netbsd solaris
|
||||
|
||||
package internet
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/apernet/quic-go/http3"
|
||||
"github.com/xtls/xray-core/common"
|
||||
@@ -32,7 +33,7 @@ type DialerClient interface {
|
||||
type DefaultDialerClient struct {
|
||||
transportConfig *Config
|
||||
client *http.Client
|
||||
closed bool
|
||||
closed atomic.Bool
|
||||
httpVersion string
|
||||
// pool of net.Conn, created using dialUploadConn
|
||||
uploadRawPool *sync.Pool
|
||||
@@ -40,7 +41,7 @@ type DefaultDialerClient struct {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -72,7 +73,7 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessio
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
if !uploadOnly { // stream-down is enough
|
||||
c.closed = true
|
||||
c.closed.Store(true)
|
||||
errors.LogInfoInner(ctx, err, "failed to "+method+" "+url)
|
||||
}
|
||||
gotConn.Close()
|
||||
@@ -108,7 +109,7 @@ func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, sessio
|
||||
if c.httpVersion != "1.1" {
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
c.closed = true
|
||||
c.closed.Store(true)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -148,7 +149,7 @@ func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, sessio
|
||||
if h1UploadConn.UnreadedResponsesCount > 0 {
|
||||
resp, err := http.ReadResponse(h1UploadConn.RespBufReader, req)
|
||||
if err != nil {
|
||||
c.closed = true
|
||||
c.closed.Store(true)
|
||||
return fmt.Errorf("error while reading response: %s", err.Error())
|
||||
}
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package splithttp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"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 {
|
||||
dataPlacement := c.GetNormalizedUplinkDataPlacement()
|
||||
|
||||
data := make([]byte, payload.Len())
|
||||
payload.Copy(data)
|
||||
buf.ReleaseMulti(payload)
|
||||
|
||||
if dataPlacement == PlacementBody || dataPlacement == PlacementAuto {
|
||||
request.Header = c.GetRequestHeader()
|
||||
request.Body = io.NopCloser(&buf.MultiBufferContainer{MultiBuffer: payload})
|
||||
request.ContentLength = int64(payload.Len())
|
||||
request.Body = io.NopCloser(bytes.NewReader(data))
|
||||
request.ContentLength = int64(len(data))
|
||||
request.GetBody = func() (io.ReadCloser, error) {
|
||||
return io.NopCloser(bytes.NewReader(data)), nil
|
||||
}
|
||||
} else {
|
||||
data := make([]byte, payload.Len())
|
||||
payload.Copy(data)
|
||||
buf.ReleaseMulti(payload)
|
||||
switch dataPlacement {
|
||||
case PlacementHeader:
|
||||
request.Header = c.GetRequestHeaderWithPayload(data)
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package splithttp_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +175,7 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea
|
||||
KeepAlivePeriod: time.Duration(quicParams.KeepAlivePeriod) * time.Second,
|
||||
MaxIncomingStreams: quicParams.MaxIncomingStreams,
|
||||
DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery || (runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin"),
|
||||
ChromeParrot: !quicParams.DisableChromeParrot,
|
||||
}
|
||||
if quicParams.MaxIdleTimeout == 0 {
|
||||
quicConfig.MaxIdleTimeout = net.ConnIdleTimeout
|
||||
@@ -255,18 +256,25 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea
|
||||
pktConn = newConn
|
||||
}
|
||||
|
||||
conn, err := quic.DialEarly(ctx, pktConn, udpAddr, tlsCfg, cfg)
|
||||
tr := &quic.Transport{Conn: pktConn, DisableGSO: quicParams.DisableGSO}
|
||||
|
||||
if !quicParams.DisableChromeParrot {
|
||||
tr.ConnectionIDGenerator = quic.ZeroLengthConnectionIDGenerator{}
|
||||
tlsCfg.GetCertificate = nil
|
||||
}
|
||||
|
||||
conn, err := tr.DialEarly(ctx, udpAddr, tlsCfg, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
context.AfterFunc(conn.Context(), func() { pktConn.Close() })
|
||||
context.AfterFunc(conn.Context(), func() { tr.Close(); pktConn.Close() })
|
||||
|
||||
switch quicParams.Congestion {
|
||||
case "reno":
|
||||
case "", "bbr":
|
||||
congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile))
|
||||
case "force-brutal":
|
||||
congestion.UseBrutal(conn, quicParams.BrutalUp)
|
||||
congestion.UseBrutal(conn, quicParams.BrutalUp, quicParams.BrutalDisableLossCompensation)
|
||||
default:
|
||||
panic(quicParams.Congestion)
|
||||
}
|
||||
@@ -595,11 +603,12 @@ func (w uploadWriter) Write(b []byte) (int, error) {
|
||||
|
||||
var writed int
|
||||
for _, buff := range buffer.MultiBuffer {
|
||||
n := int(buff.Len())
|
||||
err := w.WriteMultiBuffer(buf.MultiBuffer{buff})
|
||||
if err != nil {
|
||||
return writed, err
|
||||
}
|
||||
writed += int(buff.Len())
|
||||
writed += n
|
||||
}
|
||||
return writed, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package splithttp
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
gotls "crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
@@ -506,7 +507,15 @@ func ListenXH(ctx context.Context, address net.Address, port net.Port, streamSet
|
||||
DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery || (runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin"),
|
||||
}
|
||||
|
||||
l.h3listener, err = quic.ListenEarly(Conn, tlsConfig, quicConfig)
|
||||
var k *quic.StatelessResetKey
|
||||
if !quicParams.DisableStatelessReset {
|
||||
k = &quic.StatelessResetKey{}
|
||||
common.Must2(rand.Read((*k)[:]))
|
||||
}
|
||||
|
||||
tr := &quic.Transport{Conn: Conn, DisableGSO: quicParams.DisableGSO, StatelessResetKey: k}
|
||||
|
||||
l.h3listener, err = tr.ListenEarly(tlsConfig, quicConfig)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to listen QUIC for XHTTP/3 on ", address, ":", port).Base(err)
|
||||
}
|
||||
@@ -525,6 +534,8 @@ func ListenXH(ctx context.Context, address net.Address, port net.Port, streamSet
|
||||
if err := l.h3server.ServeListener(l.h3listener); err != nil {
|
||||
errors.LogErrorInner(ctx, err, "failed to serve HTTP/3 for XHTTP/3")
|
||||
}
|
||||
_ = tr.Close()
|
||||
_ = Conn.Close()
|
||||
}()
|
||||
} else { // tcp
|
||||
l.listener, err = internet.ListenSystem(ctx, &net.TCPAddr{
|
||||
@@ -622,7 +633,7 @@ func (l *QListener) Accept(ctx context.Context) (*quic.Conn, error) {
|
||||
case "", "bbr":
|
||||
congestion.UseBBR(conn, bbr.Profile(l.quicParams.BbrProfile))
|
||||
case "force-brutal":
|
||||
congestion.UseBrutal(conn, l.quicParams.BrutalUp)
|
||||
congestion.UseBrutal(conn, l.quicParams.BrutalUp, l.quicParams.BrutalDisableLossCompensation)
|
||||
default:
|
||||
panic(l.quicParams.Congestion)
|
||||
}
|
||||
|
||||
@@ -50,18 +50,24 @@ func (d *DefaultSystemDialer) Dial(ctx context.Context, src net.Address, dest ne
|
||||
errors.LogDebug(ctx, "dialing to "+dest.String())
|
||||
|
||||
if dest.Network == net.Network_UDP {
|
||||
srcAddr := resolveSrcAddr(net.Network_UDP, src)
|
||||
if srcAddr == nil {
|
||||
srcAddr = &net.UDPAddr{
|
||||
IP: []byte{0, 0, 0, 0},
|
||||
Port: 0,
|
||||
}
|
||||
}
|
||||
var lc net.ListenConfig
|
||||
destAddr, err := net.ResolveUDPAddr("udp", dest.NetAddr())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
srcAddr := resolveSrcAddr(net.Network_UDP, src)
|
||||
if srcAddr == nil {
|
||||
// some OS don't support mapped IPv4 dual stack
|
||||
// and need to select 0.0.0.0 or [::] manually based on the destination
|
||||
wildcard := net.AnyIP.IP()
|
||||
if destAddr.IP.To4() == nil {
|
||||
wildcard = net.AnyIPv6.IP()
|
||||
}
|
||||
srcAddr = &net.UDPAddr{
|
||||
IP: wildcard,
|
||||
Port: 0,
|
||||
}
|
||||
}
|
||||
var lc net.ListenConfig
|
||||
lc.Control = func(network, address string, c syscall.RawConn) error {
|
||||
for _, ctl := range Controllers {
|
||||
if err := ctl(network, address, c); err != nil {
|
||||
|
||||
@@ -42,9 +42,6 @@ func ListenTCP(ctx context.Context, address net.Address, port net.Port, streamSe
|
||||
var listener net.Listener
|
||||
var err error
|
||||
if port == net.Port(0) { // unix
|
||||
if !address.Family().IsDomain() {
|
||||
return nil, errors.New("invalid unix listen: ", address).AtError()
|
||||
}
|
||||
listener, err = internet.ListenSystem(ctx, &net.UnixAddr{
|
||||
Name: address.Domain(),
|
||||
Net: "unix",
|
||||
|
||||
@@ -65,6 +65,9 @@ func ListenTCP(ctx context.Context, address net.Address, port net.Port, settings
|
||||
if address.Family().IsDomain() {
|
||||
return nil, errors.New("domain address is not allowed for listening: ", address.Domain())
|
||||
}
|
||||
if port == 0 {
|
||||
return nil, errors.New("port 0 is not allowed for listening on TCP")
|
||||
}
|
||||
|
||||
protocol := settings.ProtocolName
|
||||
listenFunc := transportListenerCache[protocol]
|
||||
|
||||
@@ -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
|
||||
// +build !linux,!freebsd,!darwin
|
||||
//go:build !linux && !freebsd && !darwin && !openbsd
|
||||
// +build !linux,!freebsd,!darwin,!openbsd
|
||||
|
||||
package udp
|
||||
|
||||
|
||||
Reference in New Issue
Block a user