Compare commits

...

8 Commits

Author SHA1 Message Date
世界
f63091d14d Bump version 2026-02-15 21:05:34 +08:00
世界
1c4a01ee90 Fix matching multi predefined 2026-02-15 19:20:31 +08:00
世界
4d7f99310c Fix matching rule-set invert 2026-02-15 19:20:11 +08:00
世界
6fc511f56e wireguard: Fix missing fallback for gso 2026-02-15 19:20:03 +08:00
世界
d18d2b352a Bump version 2026-02-09 13:57:18 +08:00
世界
534128bba9 tuic: Fix udp context 2026-02-09 13:55:09 +08:00
世界
736a7368c6 Fix naive padding 2026-02-09 13:53:32 +08:00
世界
e7a9c90213 Fix DNS cache lock goroutine leak
The cache deduplication in Client.Exchange uses a channel-based lock
per DNS question. Waiting goroutines blocked on <-cond without context
awareness, causing them to accumulate indefinitely when the owning
goroutine's transport call stalls. Add select on ctx.Done() so waiters
respect context cancellation and timeouts.
2026-02-06 22:28:30 +08:00
10 changed files with 184 additions and 10 deletions

View File

@@ -144,7 +144,11 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
if c.cache != nil {
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
if loaded {
<-cond
select {
case <-cond:
case <-ctx.Done():
return nil, ctx.Err()
}
} else {
defer func() {
c.cacheLock.Delete(question)
@@ -154,7 +158,11 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
} else if c.transportCache != nil {
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
if loaded {
<-cond
select {
case <-cond:
case <-ctx.Done():
return nil, ctx.Err()
}
} else {
defer func() {
c.transportCacheLock.Delete(question)

View File

@@ -378,9 +378,11 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
case *R.RuleActionReject:
return nil, &R.RejectedError{Cause: action.Error(ctx)}
case *R.RuleActionPredefined:
responseAddrs = nil
if action.Rcode != mDNS.RcodeSuccess {
err = RcodeError(action.Rcode)
} else {
err = nil
for _, answer := range action.Answer {
switch record := answer.(type) {
case *mDNS.A:

View File

@@ -2,6 +2,14 @@
icon: material/alert-decagram
---
#### 1.12.22
* Fixes and improvements
#### 1.12.21
* Fixes and improvements
#### 1.12.20
* Fixes and improvements

2
go.mod
View File

@@ -29,7 +29,7 @@ require (
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3
github.com/sagernet/sing v0.7.18
github.com/sagernet/sing-mux v0.3.4
github.com/sagernet/sing-quic v0.5.2
github.com/sagernet/sing-quic v0.5.3
github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11

4
go.sum
View File

@@ -169,8 +169,8 @@ github.com/sagernet/sing v0.7.18 h1:iZHkaru1/MoHugx3G+9S3WG4owMewKO/KvieE2Pzk4E=
github.com/sagernet/sing v0.7.18/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
github.com/sagernet/sing-quic v0.5.2 h1:I3vlfRImhr0uLwRS3b3ib70RMG9FcXtOKKUDz3eKRWc=
github.com/sagernet/sing-quic v0.5.2/go.mod h1:evP1e++ZG8TJHVV5HudXV4vWeYzGfCdF4HwSJZcdqkI=
github.com/sagernet/sing-quic v0.5.3 h1:K937DKJN98xqyztijRkLJqbBfyV4rEZcYxFyP3EBikU=
github.com/sagernet/sing-quic v0.5.3/go.mod h1:evP1e++ZG8TJHVV5HudXV4vWeYzGfCdF4HwSJZcdqkI=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=

View File

@@ -95,6 +95,7 @@ func (p *paddingConn) writeWithPadding(writer io.Writer, data []byte) (n int, er
binary.BigEndian.PutUint16(header, uint16(len(data)))
header[2] = byte(paddingSize)
common.Must1(buffer.Write(data))
buffer.Extend(paddingSize)
_, err = writer.Write(buffer.Bytes())
if err == nil {
n = len(data)

View File

@@ -107,9 +107,7 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
}
for _, item := range r.items {
if _, isRuleSet := item.(*RuleSetItem); !isRuleSet {
metadata.DidMatch = true
}
metadata.DidMatch = true
if !item.Match(metadata) {
return r.invert
}

View File

@@ -0,0 +1,157 @@
package rule
import (
"context"
"testing"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common/x/list"
"github.com/stretchr/testify/require"
"go4.org/netipx"
)
type fakeRuleSet struct {
matched bool
}
func (f *fakeRuleSet) Name() string {
return "fake-rule-set"
}
func (f *fakeRuleSet) StartContext(context.Context, *adapter.HTTPStartContext) error {
return nil
}
func (f *fakeRuleSet) PostStart() error {
return nil
}
func (f *fakeRuleSet) Metadata() adapter.RuleSetMetadata {
return adapter.RuleSetMetadata{}
}
func (f *fakeRuleSet) ExtractIPSet() []*netipx.IPSet {
return nil
}
func (f *fakeRuleSet) IncRef() {}
func (f *fakeRuleSet) DecRef() {}
func (f *fakeRuleSet) Cleanup() {}
func (f *fakeRuleSet) RegisterCallback(adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
return nil
}
func (f *fakeRuleSet) UnregisterCallback(*list.Element[adapter.RuleSetUpdateCallback]) {}
func (f *fakeRuleSet) Close() error {
return nil
}
func (f *fakeRuleSet) Match(*adapter.InboundContext) bool {
return f.matched
}
func (f *fakeRuleSet) String() string {
return "fake-rule-set"
}
type fakeRuleItem struct {
matched bool
}
func (f *fakeRuleItem) Match(*adapter.InboundContext) bool {
return f.matched
}
func (f *fakeRuleItem) String() string {
return "fake-rule-item"
}
func newRuleSetOnlyRule(ruleSetMatched bool, invert bool) *DefaultRule {
ruleSetItem := &RuleSetItem{
setList: []adapter.RuleSet{&fakeRuleSet{matched: ruleSetMatched}},
}
return &DefaultRule{
abstractDefaultRule: abstractDefaultRule{
items: []RuleItem{ruleSetItem},
allItems: []RuleItem{ruleSetItem},
invert: invert,
},
}
}
func newSingleItemRule(matched bool) *DefaultRule {
item := &fakeRuleItem{matched: matched}
return &DefaultRule{
abstractDefaultRule: abstractDefaultRule{
items: []RuleItem{item},
allItems: []RuleItem{item},
},
}
}
func TestAbstractDefaultRule_RuleSetOnly_InvertFalse(t *testing.T) {
t.Parallel()
require.True(t, newRuleSetOnlyRule(true, false).Match(&adapter.InboundContext{}))
require.False(t, newRuleSetOnlyRule(false, false).Match(&adapter.InboundContext{}))
}
func TestAbstractDefaultRule_RuleSetOnly_InvertTrue(t *testing.T) {
t.Parallel()
require.False(t, newRuleSetOnlyRule(true, true).Match(&adapter.InboundContext{}))
require.True(t, newRuleSetOnlyRule(false, true).Match(&adapter.InboundContext{}))
}
func TestAbstractLogicalRule_And_WithRuleSetInvert(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
aMatched bool
ruleSetBMatch bool
expected bool
}{
{
name: "A true B true",
aMatched: true,
ruleSetBMatch: true,
expected: false,
},
{
name: "A true B false",
aMatched: true,
ruleSetBMatch: false,
expected: true,
},
{
name: "A false B true",
aMatched: false,
ruleSetBMatch: true,
expected: false,
},
{
name: "A false B false",
aMatched: false,
ruleSetBMatch: false,
expected: false,
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
logicalRule := &abstractLogicalRule{
mode: C.LogicalTypeAnd,
rules: []adapter.HeadlessRule{
newSingleItemRule(testCase.aMatched),
newRuleSetOnlyRule(testCase.ruleSetBMatch, true),
},
}
require.Equal(t, testCase.expected, logicalRule.Match(&adapter.InboundContext{}))
})
}
}

View File

@@ -88,7 +88,7 @@ func (w *systemDevice) Start() error {
w.options.Logger.Info("started at ", w.options.Name)
w.device = tunInterface
batchTUN, isBatchTUN := tunInterface.(tun.LinuxTUN)
if isBatchTUN {
if isBatchTUN && batchTUN.BatchSize() > 1 {
w.batchDevice = batchTUN
}
w.events <- wgTun.EventUp