diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 38fff2be..a540f36a 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -65,7 +65,7 @@ jobs: echo "LATEST=$LATEST" >>${GITHUB_ENV} - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up QEMU uses: docker/setup-qemu-action@v4 diff --git a/.github/workflows/release-win7.yml b/.github/workflows/release-win7.yml index d6bce607..73553e10 100644 --- a/.github/workflows/release-win7.yml +++ b/.github/workflows/release-win7.yml @@ -83,7 +83,7 @@ jobs: CGO_ENABLED: 0 steps: - name: Checkout codebase - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Show workflow information run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6ff09d52..b6bf7cd5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -170,7 +170,7 @@ jobs: CGO_ENABLED: 0 steps: - name: Checkout codebase - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up NDK if: matrix.goos == 'android' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ccc2fab6..62460115 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - name: Checkout codebase - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Check Proto Version Header run: | head -n 4 core/config.pb.go > ref.txt @@ -59,7 +59,7 @@ jobs: contents: read steps: - name: Checkout codebase - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Go uses: actions/setup-go@v6 with: @@ -83,7 +83,7 @@ jobs: os: [windows-latest, ubuntu-latest, macos-latest] steps: - name: Checkout codebase - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Go uses: actions/setup-go@v6 with: diff --git a/app/dns/dnscommon.go b/app/dns/dnscommon.go index d16316b8..5ffae51b 100644 --- a/app/dns/dnscommon.go +++ b/app/dns/dnscommon.go @@ -198,9 +198,14 @@ func parseResponse(payload []byte) (*IPRecord, error) { ipRecord := &IPRecord{ ReqID: h.ID, RCode: h.RCode, - Expire: now.Add(time.Second * dns_feature.DefaultTTL), RawHeader: &h, } + defer func() { + // set to default TTL if no valid TTL is found + if ipRecord.Expire.IsZero() { + ipRecord.Expire = now.Add(time.Second * dns_feature.DefaultTTL) + } + }() L: for { @@ -217,7 +222,7 @@ L: ttl = 1 } expire := now.Add(time.Duration(ttl) * time.Second) - if ipRecord.Expire.After(expire) { + if ipRecord.Expire.IsZero() || ipRecord.Expire.After(expire) { ipRecord.Expire = expire } diff --git a/common/geodata/domain_matcher.go b/common/geodata/domain_matcher.go index 2c231d98..a4d3564e 100644 --- a/common/geodata/domain_matcher.go +++ b/common/geodata/domain_matcher.go @@ -220,7 +220,7 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) { case Domain_Regex: return strmatcher.Regex.New(d.Value) case Domain_Domain: - return strmatcher.Domain.New(d.Value) + return strmatcher.Domain.New(strings.ToLower(d.Value)) case Domain_Full: return strmatcher.Full.New(strings.ToLower(d.Value)) default: diff --git a/common/geodata/domain_registry.go b/common/geodata/domain_registry.go index 7b6c6527..1bcc333a 100644 --- a/common/geodata/domain_registry.go +++ b/common/geodata/domain_registry.go @@ -6,12 +6,14 @@ import ( "sync/atomic" "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/utils" + "github.com/xtls/xray-core/common/uuid" ) type DomainRegistry struct { mu sync.Mutex factory DomainMatcherFactory - matchers []*DynamicDomainMatcher + matchers *utils.WeakCacheMap[uuid.UUID, DynamicDomainMatcher] } func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher, error) { @@ -24,7 +26,7 @@ func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher, } d := NewDynamicDomainMatcher(rules, m) - r.matchers = append(r.matchers, d) + r.matchers.Store(uuid.New(), d) return d, nil } @@ -32,15 +34,20 @@ func (r *DomainRegistry) Reload() error { r.mu.Lock() defer r.mu.Unlock() - errors.LogInfo(context.Background(), "reloading GeoSite data for ", len(r.matchers), " domain matcher(s)") + var matchers []*DynamicDomainMatcher + r.matchers.Range(func(_ uuid.UUID, matcher *DynamicDomainMatcher) bool { + matchers = append(matchers, matcher) + return true + }) + errors.LogInfo(context.Background(), "reloading GeoSite data for ", len(matchers), " domain matcher(s)") factory := newDomainMatcherFactory() type reloadEntry struct { dynamic *DynamicDomainMatcher matcher DomainMatcher } - reloaded := make([]reloadEntry, len(r.matchers)) - for i, d := range r.matchers { + reloaded := make([]reloadEntry, len(matchers)) + for i, d := range matchers { m, err := factory.BuildMatcher(d.rules) if err != nil { errors.LogErrorInner(context.Background(), err, "failed to reload GeoSite data for domain matcher ", i) @@ -52,13 +59,14 @@ func (r *DomainRegistry) Reload() error { entry.dynamic.Reload(entry.matcher) } r.factory = factory - errors.LogInfo(context.Background(), "reloaded GeoSite data for ", len(r.matchers), " domain matcher(s)") + errors.LogInfo(context.Background(), "reloaded GeoSite data for ", len(matchers), " domain matcher(s)") return nil } func newDomainRegistry() *DomainRegistry { return &DomainRegistry{ - factory: newDomainMatcherFactory(), + factory: newDomainMatcherFactory(), + matchers: utils.NewWeakCacheMap[uuid.UUID, DynamicDomainMatcher](), } } diff --git a/common/geodata/ip_registry.go b/common/geodata/ip_registry.go index 21fbdf9e..a02adf6d 100644 --- a/common/geodata/ip_registry.go +++ b/common/geodata/ip_registry.go @@ -7,25 +7,27 @@ import ( "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/utils" + "github.com/xtls/xray-core/common/uuid" ) type IPRegistry struct { - mu sync.Mutex - ipsetFactory *IPSetFactory - matchers []*DynamicIPMatcher + mu sync.Mutex + factory *IPSetFactory + matchers *utils.WeakCacheMap[uuid.UUID, DynamicIPMatcher] } func (r *IPRegistry) BuildIPMatcher(rules []*IPRule) (IPMatcher, error) { r.mu.Lock() defer r.mu.Unlock() - m, err := buildOptimizedIPMatcher(r.ipsetFactory, rules) + m, err := buildOptimizedIPMatcher(r.factory, rules) if err != nil { return nil, err } d := NewDynamicIPMatcher(rules, m) - r.matchers = append(r.matchers, d) + r.matchers.Store(uuid.New(), d) return d, nil } @@ -33,15 +35,20 @@ func (r *IPRegistry) Reload() error { r.mu.Lock() defer r.mu.Unlock() - errors.LogInfo(context.Background(), "reloading GeoIP data for ", len(r.matchers), " IP matcher(s)") + var matchers []*DynamicIPMatcher + r.matchers.Range(func(_ uuid.UUID, matcher *DynamicIPMatcher) bool { + matchers = append(matchers, matcher) + return true + }) + errors.LogInfo(context.Background(), "reloading GeoIP data for ", len(matchers), " IP matcher(s)") factory := newIPSetFactory() type reloadEntry struct { dynamic *DynamicIPMatcher matcher IPMatcher } - reloaded := make([]reloadEntry, len(r.matchers)) - for i, d := range r.matchers { + reloaded := make([]reloadEntry, len(matchers)) + for i, d := range matchers { m, err := buildOptimizedIPMatcher(factory, d.rules) if err != nil { errors.LogErrorInner(context.Background(), err, "failed to reload GeoIP data for IP matcher ", i) @@ -52,14 +59,15 @@ func (r *IPRegistry) Reload() error { for _, entry := range reloaded { entry.dynamic.Reload(entry.matcher) } - r.ipsetFactory = factory - errors.LogInfo(context.Background(), "reloaded GeoIP data for ", len(r.matchers), " IP matcher(s)") + r.factory = factory + errors.LogInfo(context.Background(), "reloaded GeoIP data for ", len(matchers), " IP matcher(s)") return nil } func newIPRegistry() *IPRegistry { return &IPRegistry{ - ipsetFactory: newIPSetFactory(), + factory: newIPSetFactory(), + matchers: utils.NewWeakCacheMap[uuid.UUID, DynamicIPMatcher](), } } diff --git a/common/geodata/rule_parser.go b/common/geodata/rule_parser.go index c05fed31..16b80b76 100644 --- a/common/geodata/rule_parser.go +++ b/common/geodata/rule_parser.go @@ -138,7 +138,7 @@ func ParseDomainRule(r string, defaultType Domain_Type) (*DomainRule, error) { } prefix := 0 - for _, ext := range [...]string{"ext:", "ext-domain:"} { + for _, ext := range [...]string{"ext:", "ext-domain:", "ext-site:"} { if strings.HasPrefix(r, ext) { prefix = len(ext) break @@ -167,7 +167,7 @@ func ParseDomainRules(rules []string, defaultType Domain_Type) ([]*DomainRule, e } prefix := 0 - for _, ext := range [...]string{"ext:", "ext-domain:"} { + for _, ext := range [...]string{"ext:", "ext-domain:", "ext-site:"} { if strings.HasPrefix(r, ext) { prefix = len(ext) break diff --git a/common/utils/weak_cache.go b/common/utils/weak_cache.go index a9aaacca..48cc8ce0 100644 --- a/common/utils/weak_cache.go +++ b/common/utils/weak_cache.go @@ -1,6 +1,7 @@ package utils import ( + "maps" "runtime" "sync" "weak" @@ -43,3 +44,16 @@ func (c *WeakCacheMap[K, V]) Store(key K, value *V) { } }, struct{}{}) } + +func (c *WeakCacheMap[K, V]) Range(f func(K, *V) bool) { + c.mu.Lock() + snapshot := maps.Clone(c.m) + c.mu.Unlock() + for k, v := range snapshot { + if value := v.Value(); value != nil { + if !f(k, value) { + break + } + } + } +} diff --git a/core/core.go b/core/core.go index bb66da8e..78d7d238 100644 --- a/core/core.go +++ b/core/core.go @@ -20,7 +20,7 @@ import ( var ( Version_x byte = 26 Version_y byte = 6 - Version_z byte = 1 + Version_z byte = 22 ) var ( diff --git a/go.mod b/go.mod index e48eca14..88932cf0 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26 require ( github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 - github.com/cloudflare/circl v1.6.3 + github.com/cloudflare/circl v1.6.4 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/golang/mock v1.7.0-rc.1 github.com/google/go-cmp v0.7.0 @@ -12,7 +12,7 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 github.com/miekg/dns v1.1.72 github.com/pelletier/go-toml v1.9.5 - github.com/pion/stun/v3 v3.1.5 + github.com/pion/stun/v3 v3.1.6 github.com/pires/go-proxyproto v0.12.0 github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af github.com/robfig/cron/v3 v3.0.1 diff --git a/go.sum b/go.sum index 59fe6c90..5e282085 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkd github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA= github.com/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.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= -github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U= +github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -49,8 +49,8 @@ github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY= github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= -github.com/pion/stun/v3 v3.1.5 h1:Y1FHlhaI6+4UoC5i/zQf4F7JvdZtB24/05oyy/GF1x8= -github.com/pion/stun/v3 v3.1.5/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs= +github.com/pion/stun/v3 v3.1.6 h1:WnhsD0eHCiwCfKNkVx0VJJwr2Y3eV4Ueih3KJ+dfZy8= +github.com/pion/stun/v3 v3.1.6/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs= github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk= github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM= github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM= diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 9b08f8c3..feb593b4 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -1409,10 +1409,12 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) { } type FragmentMask struct { - Packets string `json:"packets"` - Length Int32Range `json:"length"` - Delay Int32Range `json:"delay"` - MaxSplit Int32Range `json:"maxSplit"` + Packets string `json:"packets"` + Length Int32Range `json:"length"` + Delay Int32Range `json:"delay"` + Lengths []Int32Range `json:"lengths"` + Delays []Int32Range `json:"delays"` + MaxSplit Int32Range `json:"maxSplit"` } func (c *FragmentMask) Build() (proto.Message, error) { @@ -1437,14 +1439,29 @@ func (c *FragmentMask) Build() (proto.Message, error) { } } - config.LengthMin = int64(c.Length.From) - config.LengthMax = int64(c.Length.To) - if config.LengthMin == 0 { - return nil, errors.New("LengthMin can't be 0") + if len(c.Lengths) > 0 { + for _, r := range c.Lengths { + config.LengthsMin = append(config.LengthsMin, int64(r.From)) + config.LengthsMax = append(config.LengthsMax, int64(r.To)) + } + } else { + config.LengthsMin = append(config.LengthsMin, int64(c.Length.From)) + config.LengthsMax = append(config.LengthsMax, int64(c.Length.To)) } - config.DelayMin = int64(c.Delay.From) - config.DelayMax = int64(c.Delay.To) + if config.LengthsMin[len(config.LengthsMin)-1] == 0 { + return nil, errors.New("last lengths entry min can't be 0") + } + + if len(c.Delays) > 0 { + for _, r := range c.Delays { + config.DelaysMin = append(config.DelaysMin, int64(r.From)) + config.DelaysMax = append(config.DelaysMax, int64(r.To)) + } + } else { + config.DelaysMin = append(config.DelaysMin, int64(c.Delay.From)) + config.DelaysMax = append(config.DelaysMax, int64(c.Delay.To)) + } config.MaxSplitMin = int64(c.MaxSplit.From) config.MaxSplitMax = int64(c.MaxSplit.To) diff --git a/proxy/tun/handler.go b/proxy/tun/handler.go index 174b9e0a..5bf8f231 100644 --- a/proxy/tun/handler.go +++ b/proxy/tun/handler.go @@ -155,11 +155,22 @@ func (t *Handler) Start() error { // HandleConnection pass the connection coming from the ip stack to the routing dispatcher func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) { + // when handling is done with any outcome, always signal back to the incoming connection + // to close, send completion packets back to the network, and cleanup + defer conn.Close() + ctx, cancel := context.WithCancel(t.ctx) defer cancel() ctx = c.ContextWithID(ctx, session.NewID()) - source := net.DestinationFromAddr(conn.RemoteAddr()) + // if the connection is already closed, conn.RemoteAddr() will be nil + // due to gvisor weird behavior + remote := conn.RemoteAddr() + if remote == nil { + errors.LogInfo(t.ctx, "dropped quickly closed connection") + return + } + source := net.DestinationFromAddr(remote) if t.uplinkCounter != nil || t.downlinkCounter != nil { conn = &stat.CounterConnection{ Connection: conn, @@ -167,9 +178,6 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) { WriteCounter: t.downlinkCounter, } } - // when handling is done with any outcome, always signal back to the incoming connection - // to close, send completion packets back to the network, and cleanup - defer conn.Close() inbound := session.Inbound{ Name: "tun", diff --git a/proxy/tun/tun_linux.go b/proxy/tun/tun_linux.go index ef3f3505..3e5a30bf 100644 --- a/proxy/tun/tun_linux.go +++ b/proxy/tun/tun_linux.go @@ -4,8 +4,11 @@ package tun import ( "net" + "strconv" "github.com/vishvananda/netlink" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/platform" "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/tcpip/link/fdbased" "gvisor.dev/gvisor/pkg/tcpip/stack" @@ -18,6 +21,7 @@ type LinuxTun struct { tunFd int tunLink netlink.Link options *Config + ownsTun bool } // LinuxTun implements Tun @@ -25,12 +29,24 @@ var _ Tun = (*LinuxTun)(nil) // NewTun builds new tun interface handler (linux specific) func NewTun(options *Config) (Tun, error) { - tunFd, err := open(options.Name) + tunFd, tunLink, fdProvided, err := openFromEnv(options.Name) + if err != nil { + return nil, err + } + if fdProvided { + return &LinuxTun{ + tunFd: tunFd, + tunLink: tunLink, + options: options, + }, nil + } + + tunFd, err = open(options.Name) if err != nil { return nil, err } - tunLink, err := setup(options.Name, int(options.MTU)) + tunLink, err = setup(options.Name, int(options.MTU)) if err != nil { _ = unix.Close(tunFd) return nil, err @@ -40,11 +56,59 @@ func NewTun(options *Config) (Tun, error) { tunFd: tunFd, tunLink: tunLink, options: options, + ownsTun: true, } return linuxTun, nil } +func openFromEnv(expectedName string) (int, netlink.Link, bool, error) { + fdStr := platform.NewEnvFlag(platform.TunFdKey).GetValue(func() string { return "" }) + if fdStr == "" { + return -1, nil, false, nil + } + + fd, err := strconv.Atoi(fdStr) + if err != nil { + return -1, nil, true, errors.New("invalid ", platform.TunFdKey).Base(err) + } + if fd < 3 { + return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": file descriptor must be >= 3") + } + + ifr, err := unix.NewIfreq("") + if err != nil { + return -1, nil, true, err + } + if err = unix.IoctlIfreq(fd, unix.TUNGETIFF, ifr); err != nil { + return -1, nil, true, err + } + + flags := ifr.Uint16() + if flags&unix.IFF_TUN == 0 { + return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": file descriptor is not a TUN device") + } + if flags&unix.IFF_NO_PI == 0 { + return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": TUN device must use IFF_NO_PI") + } + + actualName := ifr.Name() + if expectedName != "" && actualName != expectedName { + return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": TUN device name ", actualName, " does not match configured name ", expectedName) + } + + tunLink, err := netlink.LinkByName(actualName) + if err != nil { + return -1, nil, true, err + } + + if err = unix.SetNonblock(fd, true); err != nil { + return -1, nil, true, err + } + + return fd, tunLink, true, nil +} + // open the file that implements tun interface in the OS func open(name string) (int, error) { fd, err := unix.Open("/dev/net/tun", unix.O_RDWR, 0) @@ -93,6 +157,10 @@ func setup(name string, MTU int) (netlink.Link, error) { // Start is called by handler to bring tun interface to life func (t *LinuxTun) Start() error { + if !t.ownsTun { + return nil + } + err := netlink.LinkSetUp(t.tunLink) if err != nil { return err @@ -103,7 +171,9 @@ func (t *LinuxTun) Start() error { // Close is called to shut down the tun interface func (t *LinuxTun) Close() error { - _ = netlink.LinkSetDown(t.tunLink) + if t.ownsTun { + _ = netlink.LinkSetDown(t.tunLink) + } _ = unix.Close(t.tunFd) return nil diff --git a/transport/internet/finalmask/fragment/config.pb.go b/transport/internet/finalmask/fragment/config.pb.go index c8660f5e..3659c7ac 100644 --- a/transport/internet/finalmask/fragment/config.pb.go +++ b/transport/internet/finalmask/fragment/config.pb.go @@ -25,12 +25,12 @@ type Config struct { state protoimpl.MessageState `protogen:"open.v1"` PacketsFrom int64 `protobuf:"varint,1,opt,name=packets_from,json=packetsFrom,proto3" json:"packets_from,omitempty"` PacketsTo int64 `protobuf:"varint,2,opt,name=packets_to,json=packetsTo,proto3" json:"packets_to,omitempty"` - LengthMin int64 `protobuf:"varint,3,opt,name=length_min,json=lengthMin,proto3" json:"length_min,omitempty"` - LengthMax int64 `protobuf:"varint,4,opt,name=length_max,json=lengthMax,proto3" json:"length_max,omitempty"` - DelayMin int64 `protobuf:"varint,5,opt,name=delay_min,json=delayMin,proto3" json:"delay_min,omitempty"` - DelayMax int64 `protobuf:"varint,6,opt,name=delay_max,json=delayMax,proto3" json:"delay_max,omitempty"` MaxSplitMin int64 `protobuf:"varint,7,opt,name=max_split_min,json=maxSplitMin,proto3" json:"max_split_min,omitempty"` MaxSplitMax int64 `protobuf:"varint,8,opt,name=max_split_max,json=maxSplitMax,proto3" json:"max_split_max,omitempty"` + LengthsMin []int64 `protobuf:"varint,9,rep,packed,name=lengths_min,json=lengthsMin,proto3" json:"lengths_min,omitempty"` + LengthsMax []int64 `protobuf:"varint,10,rep,packed,name=lengths_max,json=lengthsMax,proto3" json:"lengths_max,omitempty"` + DelaysMin []int64 `protobuf:"varint,11,rep,packed,name=delays_min,json=delaysMin,proto3" json:"delays_min,omitempty"` + DelaysMax []int64 `protobuf:"varint,12,rep,packed,name=delays_max,json=delaysMax,proto3" json:"delays_max,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -79,34 +79,6 @@ func (x *Config) GetPacketsTo() int64 { return 0 } -func (x *Config) GetLengthMin() int64 { - if x != nil { - return x.LengthMin - } - return 0 -} - -func (x *Config) GetLengthMax() int64 { - if x != nil { - return x.LengthMax - } - return 0 -} - -func (x *Config) GetDelayMin() int64 { - if x != nil { - return x.DelayMin - } - return 0 -} - -func (x *Config) GetDelayMax() int64 { - if x != nil { - return x.DelayMax - } - return 0 -} - func (x *Config) GetMaxSplitMin() int64 { if x != nil { return x.MaxSplitMin @@ -121,23 +93,54 @@ func (x *Config) GetMaxSplitMax() int64 { return 0 } +func (x *Config) GetLengthsMin() []int64 { + if x != nil { + return x.LengthsMin + } + return nil +} + +func (x *Config) GetLengthsMax() []int64 { + if x != nil { + return x.LengthsMax + } + return nil +} + +func (x *Config) GetDelaysMin() []int64 { + if x != nil { + return x.DelaysMin + } + return nil +} + +func (x *Config) GetDelaysMax() []int64 { + if x != nil { + return x.DelaysMax + } + return nil +} + var File_transport_internet_finalmask_fragment_config_proto protoreflect.FileDescriptor const file_transport_internet_finalmask_fragment_config_proto_rawDesc = "" + "\n" + - "2transport/internet/finalmask/fragment/config.proto\x12*xray.transport.internet.finalmask.fragment\"\x8a\x02\n" + + "2transport/internet/finalmask/fragment/config.proto\x12*xray.transport.internet.finalmask.fragment\"\x92\x02\n" + "\x06Config\x12!\n" + "\fpackets_from\x18\x01 \x01(\x03R\vpacketsFrom\x12\x1d\n" + "\n" + - "packets_to\x18\x02 \x01(\x03R\tpacketsTo\x12\x1d\n" + - "\n" + - "length_min\x18\x03 \x01(\x03R\tlengthMin\x12\x1d\n" + - "\n" + - "length_max\x18\x04 \x01(\x03R\tlengthMax\x12\x1b\n" + - "\tdelay_min\x18\x05 \x01(\x03R\bdelayMin\x12\x1b\n" + - "\tdelay_max\x18\x06 \x01(\x03R\bdelayMax\x12\"\n" + + "packets_to\x18\x02 \x01(\x03R\tpacketsTo\x12\"\n" + "\rmax_split_min\x18\a \x01(\x03R\vmaxSplitMin\x12\"\n" + - "\rmax_split_max\x18\b \x01(\x03R\vmaxSplitMaxB\xa0\x01\n" + + "\rmax_split_max\x18\b \x01(\x03R\vmaxSplitMax\x12\x1f\n" + + "\vlengths_min\x18\t \x03(\x03R\n" + + "lengthsMin\x12\x1f\n" + + "\vlengths_max\x18\n" + + " \x03(\x03R\n" + + "lengthsMax\x12\x1d\n" + + "\n" + + "delays_min\x18\v \x03(\x03R\tdelaysMin\x12\x1d\n" + + "\n" + + "delays_max\x18\f \x03(\x03R\tdelaysMaxB\xa0\x01\n" + ".com.xray.transport.internet.finalmask.fragmentP\x01Z?github.com/xtls/xray-core/transport/internet/finalmask/fragment\xaa\x02*Xray.Transport.Internet.Finalmask.Fragmentb\x06proto3" var ( diff --git a/transport/internet/finalmask/fragment/config.proto b/transport/internet/finalmask/fragment/config.proto index 62aaf186..c83a962a 100644 --- a/transport/internet/finalmask/fragment/config.proto +++ b/transport/internet/finalmask/fragment/config.proto @@ -9,10 +9,10 @@ option java_multiple_files = true; message Config { int64 packets_from = 1; int64 packets_to = 2; - int64 length_min = 3; - int64 length_max = 4; - int64 delay_min = 5; - int64 delay_max = 6; int64 max_split_min = 7; int64 max_split_max = 8; + repeated int64 lengths_min = 9; + repeated int64 lengths_max = 10; + repeated int64 delays_min = 11; + repeated int64 delays_max = 12; } \ No newline at end of file diff --git a/transport/internet/finalmask/fragment/conn.go b/transport/internet/finalmask/fragment/conn.go index 91822ace..3c65a8bd 100644 --- a/transport/internet/finalmask/fragment/conn.go +++ b/transport/internet/finalmask/fragment/conn.go @@ -43,6 +43,29 @@ func (c *fragmentConn) Splice() bool { return true } +// lengthForSegment returns the length range (min, max) for the given segment index (0-based). +// Clamps to the last entry when the index exceeds the list length. +func (c *fragmentConn) lengthForSegment(segIdx int) (int64, int64) { + if segIdx >= len(c.config.LengthsMin) { + segIdx = len(c.config.LengthsMin) - 1 + } + return c.config.LengthsMin[segIdx], c.config.LengthsMax[segIdx] +} + +// delayForSegment returns the delay range (min, max) for the given segment index (0-based). +// Clamps to the last entry when the index exceeds the list length. +func (c *fragmentConn) delayForSegment(segIdx int) (int64, int64) { + if segIdx >= len(c.config.DelaysMin) { + segIdx = len(c.config.DelaysMin) - 1 + } + return c.config.DelaysMin[segIdx], c.config.DelaysMax[segIdx] +} + +// mergeTlsHelloSegments returns true only when delays has exactly one zero entry. +func (c *fragmentConn) mergeTlsHelloSegments() bool { + return len(c.config.DelaysMax) == 1 && c.config.DelaysMax[0] == 0 +} + func (c *fragmentConn) Write(p []byte) (n int, err error) { c.count++ @@ -57,12 +80,13 @@ func (c *fragmentConn) Write(p []byte) (n int, err error) { data := p[5:recordLen] buff := make([]byte, 2048) var hello []byte + mergeHello := c.mergeTlsHelloSegments() maxSplit := crypto.RandBetween(c.config.MaxSplitMin, c.config.MaxSplitMax) var splitNum int64 for from := 0; ; { - to := from + int(crypto.RandBetween(c.config.LengthMin, c.config.LengthMax)) - splitNum++ - if to > len(data) || (maxSplit > 0 && splitNum >= maxSplit) { + lengthMin, lengthMax := c.lengthForSegment(int(splitNum)) + to := from + int(crypto.RandBetween(lengthMin, lengthMax)) + if to > len(data) || (maxSplit > 0 && splitNum+1 >= maxSplit) { to = len(data) } l := to - from @@ -74,15 +98,19 @@ func (c *fragmentConn) Write(p []byte) (n int, err error) { from = to buff[3] = byte(l >> 8) buff[4] = byte(l) - if c.config.DelayMax == 0 { + if mergeHello { hello = append(hello, buff[:5+l]...) } else { + delayMin, delayMax := c.delayForSegment(int(splitNum)) _, err := c.Conn.Write(buff[:5+l]) - time.Sleep(time.Duration(crypto.RandBetween(c.config.DelayMin, c.config.DelayMax)) * time.Millisecond) + if delayMax > 0 { + time.Sleep(time.Duration(crypto.RandBetween(delayMin, delayMax)) * time.Millisecond) + } if err != nil { return 0, err } } + splitNum++ if from == len(data) { if len(hello) > 0 { _, err := c.Conn.Write(hello) @@ -107,9 +135,9 @@ func (c *fragmentConn) Write(p []byte) (n int, err error) { maxSplit := crypto.RandBetween(c.config.MaxSplitMin, c.config.MaxSplitMax) var splitNum int64 for from := 0; ; { - to := from + int(crypto.RandBetween(c.config.LengthMin, c.config.LengthMax)) - splitNum++ - if to > len(p) || (maxSplit > 0 && splitNum >= maxSplit) { + lengthMin, lengthMax := c.lengthForSegment(int(splitNum)) + to := from + int(crypto.RandBetween(lengthMin, lengthMax)) + if to > len(p) || (maxSplit > 0 && splitNum+1 >= maxSplit) { to = len(p) } n, err := c.Conn.Write(p[from:to]) @@ -117,7 +145,11 @@ func (c *fragmentConn) Write(p []byte) (n int, err error) { if err != nil { return from, err } - time.Sleep(time.Duration(crypto.RandBetween(c.config.DelayMin, c.config.DelayMax)) * time.Millisecond) + delayMin, delayMax := c.delayForSegment(int(splitNum)) + if delayMax > 0 { + time.Sleep(time.Duration(crypto.RandBetween(delayMin, delayMax)) * time.Millisecond) + } + splitNum++ if from >= len(p) { return from, nil } diff --git a/transport/internet/splithttp/hub.go b/transport/internet/splithttp/hub.go index a28866cf..8416591a 100644 --- a/transport/internet/splithttp/hub.go +++ b/transport/internet/splithttp/hub.go @@ -146,6 +146,7 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req writer.WriteHeader(http.StatusBadRequest) return } + obfsPaddingAccepted := h.config.XPaddingObfsMode && paddingValue != "" sessionId, seqStr := h.config.ExtractMetaFromRequest(request, h.path) @@ -215,8 +216,8 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req writer.Header().Set("Cache-Control", "no-store") writer.WriteHeader(http.StatusOK) scStreamUpServerSecs := h.config.GetNormalizedScStreamUpServerSecs() - referrer := request.Header.Get("Referer") - if referrer != "" && scStreamUpServerSecs.To > 0 { + hasLegacyRefererCompatMarker := request.Header.Get("Referer") != "" + if (hasLegacyRefererCompatMarker || obfsPaddingAccepted) && scStreamUpServerSecs.To > 0 { go func() { for { _, err := httpSC.Write(bytes.Repeat([]byte{'X'}, int(h.config.GetNormalizedXPaddingBytes().rand())))