From 2e4a6de4e78f81a02913ec0a5d2a20e4ab6a1257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 27 Mar 2025 20:30:57 +0800 Subject: [PATCH 01/16] release: Fix read tag --- cmd/internal/read_tag/main.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cmd/internal/read_tag/main.go b/cmd/internal/read_tag/main.go index 0f426332..c4da1de5 100644 --- a/cmd/internal/read_tag/main.go +++ b/cmd/internal/read_tag/main.go @@ -27,11 +27,8 @@ func main() { ) if flagRunNightly { var version badversion.Version - version, err = build_shared.ReadTagVersionRev() + version, err = build_shared.ReadTagVersion() if err == nil { - if version.PreReleaseIdentifier == "" { - version.Patch++ - } versionStr = version.String() } } else { From 9774a659b0328095c601057e9999502b40602af3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 29 Mar 2025 17:41:22 +0800 Subject: [PATCH 02/16] Fix DoQ / truncate DNS message --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 52167f1b..b3e970d3 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/sagernet/quic-go v0.49.0-beta.1 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 github.com/sagernet/sing v0.6.5 - github.com/sagernet/sing-dns v0.4.0 + github.com/sagernet/sing-dns v0.4.1 github.com/sagernet/sing-mux v0.3.1 github.com/sagernet/sing-quic v0.4.0 github.com/sagernet/sing-shadowsocks v0.2.7 diff --git a/go.sum b/go.sum index ca9f34dd..9e22fefc 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,8 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4Wk github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing v0.6.5 h1:TBKTK6Ms0/MNTZm+cTC2hhKunE42XrNIdsxcYtWqeUU= github.com/sagernet/sing v0.6.5/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= -github.com/sagernet/sing-dns v0.4.0 h1:+mNoOuR3nljjouCH+qMg4zHI1+R9T2ReblGFkZPEndc= -github.com/sagernet/sing-dns v0.4.0/go.mod h1:dweQs54ng2YGzoJfz+F9dGuDNdP5pJ3PLeggnK5VWc8= +github.com/sagernet/sing-dns v0.4.1 h1:nozS7iqpxZ7aV73oHbkD/8haOvf3XXDCgT//8NdYirk= +github.com/sagernet/sing-dns v0.4.1/go.mod h1:dweQs54ng2YGzoJfz+F9dGuDNdP5pJ3PLeggnK5VWc8= github.com/sagernet/sing-mux v0.3.1 h1:kvCc8HyGAskDHDQ0yQvoTi/7J4cZPB/VJMsAM3MmdQI= github.com/sagernet/sing-mux v0.3.1/go.mod h1:Mkdz8LnDstthz0HWuA/5foncnDIdcNN5KZ6AdJX+x78= github.com/sagernet/sing-quic v0.4.0 h1:E4geazHk/UrJTXMlT+CBCKmn8V86RhtNeczWtfeoEFc= From 47fc3ebda4b370bd7ff9fdfc6b461bf5bb6ffbd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 29 Mar 2025 19:51:21 +0800 Subject: [PATCH 03/16] Add duplicate tag check --- option/options.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/option/options.go b/option/options.go index 94c97719..85ff81eb 100644 --- a/option/options.go +++ b/option/options.go @@ -4,6 +4,7 @@ import ( "bytes" "context" + E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" ) @@ -30,7 +31,7 @@ func (o *Options) UnmarshalJSONContext(ctx context.Context, content []byte) erro return err } o.RawMessage = content - return nil + return checkOptions(o) } type LogOptions struct { @@ -42,3 +43,52 @@ type LogOptions struct { } type StubOptions struct{} + +func checkOptions(options *Options) error { + err := checkInbounds(options.Inbounds) + if err != nil { + return err + } + err = checkOutbounds(options.Outbounds, options.Endpoints) + if err != nil { + return err + } + return nil +} + +func checkInbounds(inbounds []Inbound) error { + seen := make(map[string]bool) + for _, inbound := range inbounds { + if inbound.Tag == "" { + continue + } + if seen[inbound.Tag] { + return E.New("duplicate inbound tag: ", inbound.Tag) + } + seen[inbound.Tag] = true + } + return nil +} + +func checkOutbounds(outbounds []Outbound, endpoints []Endpoint) error { + seen := make(map[string]bool) + for _, outbound := range outbounds { + if outbound.Tag == "" { + continue + } + if seen[outbound.Tag] { + return E.New("duplicate outbound/endpoint tag: ", outbound.Tag) + } + seen[outbound.Tag] = true + } + for _, endpoint := range endpoints { + if endpoint.Tag == "" { + continue + } + if seen[endpoint.Tag] { + return E.New("duplicate outbound/endpoint tag: ", endpoint.Tag) + } + seen[endpoint.Tag] = true + } + return nil +} From f4c29840c35041810d745de0c4bee5c47ba98204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 31 Mar 2025 20:44:46 +0800 Subject: [PATCH 04/16] Fix DNS sniffer --- common/sniff/dns.go | 4 ---- common/sniff/dns_test.go | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 common/sniff/dns_test.go diff --git a/common/sniff/dns.go b/common/sniff/dns.go index 96670eca..4eb1990c 100644 --- a/common/sniff/dns.go +++ b/common/sniff/dns.go @@ -11,7 +11,6 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" - M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/task" mDNS "github.com/miekg/dns" @@ -47,9 +46,6 @@ func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, pack if err != nil { return err } - if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) { - return os.ErrInvalid - } metadata.Protocol = C.ProtocolDNS return nil } diff --git a/common/sniff/dns_test.go b/common/sniff/dns_test.go new file mode 100644 index 00000000..eaf4dd1a --- /dev/null +++ b/common/sniff/dns_test.go @@ -0,0 +1,23 @@ +package sniff_test + +import ( + "context" + "encoding/hex" + "testing" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/sniff" + C "github.com/sagernet/sing-box/constant" + + "github.com/stretchr/testify/require" +) + +func TestSniffDNS(t *testing.T) { + t.Parallel() + query, err := hex.DecodeString("740701000001000000000000012a06676f6f676c6503636f6d0000010001") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.DomainNameQuery(context.TODO(), &metadata, query) + require.NoError(t, err) + require.Equal(t, C.ProtocolDNS, metadata.Protocol) +} From 5eeef6b28e81e4374c83378c8fe21d1553209354 Mon Sep 17 00:00:00 2001 From: xchacha20-poly1305 Date: Thu, 3 Apr 2025 20:01:08 +0800 Subject: [PATCH 05/16] Fix multiple trackers --- adapter/router.go | 2 +- box.go | 4 ++-- route/route.go | 8 ++++---- route/router.go | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/adapter/router.go b/adapter/router.go index a637e506..687943cb 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -38,7 +38,7 @@ type Router interface { ClearDNSCache() Rules() []Rule - SetTracker(tracker ConnectionTracker) + AppendTracker(tracker ConnectionTracker) ResetNetwork() } diff --git a/box.go b/box.go index 8eb8f2f3..b9f04c87 100644 --- a/box.go +++ b/box.go @@ -257,7 +257,7 @@ func New(options Options) (*Box, error) { if err != nil { return nil, E.Cause(err, "create clash-server") } - router.SetTracker(clashServer) + router.AppendTracker(clashServer) service.MustRegister[adapter.ClashServer](ctx, clashServer) services = append(services, clashServer) } @@ -267,7 +267,7 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "create v2ray-server") } if v2rayServer.StatsService() != nil { - router.SetTracker(v2rayServer.StatsService()) + router.AppendTracker(v2rayServer.StatsService()) services = append(services, v2rayServer) service.MustRegister[adapter.V2RayServer](ctx, v2rayServer) } diff --git a/route/route.go b/route/route.go index 834d3425..6ab4cc97 100644 --- a/route/route.go +++ b/route/route.go @@ -140,8 +140,8 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad for _, buffer := range buffers { conn = bufio.NewCachedConn(conn, buffer) } - if r.tracker != nil { - conn = r.tracker.RoutedConnection(ctx, conn, metadata, selectedRule, selectedOutbound) + for _, tracker := range r.trackers { + conn = tracker.RoutedConnection(ctx, conn, metadata, selectedRule, selectedOutbound) } if outboundHandler, isHandler := selectedOutbound.(adapter.ConnectionHandlerEx); isHandler { outboundHandler.NewConnectionEx(ctx, conn, metadata, onClose) @@ -258,8 +258,8 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m conn = bufio.NewCachedPacketConn(conn, buffer.Buffer, buffer.Destination) N.PutPacketBuffer(buffer) } - if r.tracker != nil { - conn = r.tracker.RoutedPacketConnection(ctx, conn, metadata, selectedRule, selectedOutbound) + for _, tracker := range r.trackers { + conn = tracker.RoutedPacketConnection(ctx, conn, metadata, selectedRule, selectedOutbound) } if metadata.FakeIP { conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination) diff --git a/route/router.go b/route/router.go index 68f5dc35..b74af8b9 100644 --- a/route/router.go +++ b/route/router.go @@ -64,7 +64,7 @@ type Router struct { fakeIPStore adapter.FakeIPStore processSearcher process.Searcher pauseManager pause.Manager - tracker adapter.ConnectionTracker + trackers []adapter.ConnectionTracker platformInterface platform.Interface needWIFIState bool started bool @@ -511,8 +511,8 @@ func (r *Router) Rules() []adapter.Rule { return r.rules } -func (r *Router) SetTracker(tracker adapter.ConnectionTracker) { - r.tracker = tracker +func (r *Router) AppendTracker(tracker adapter.ConnectionTracker) { + r.trackers = append(r.trackers, tracker) } func (r *Router) ResetNetwork() { From 3adc10a7976eabb4db8a1aee885ae41ad93b8f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 4 Apr 2025 21:54:01 +0800 Subject: [PATCH 06/16] Fix hysteria2 close --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index b3e970d3..d21192ce 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/cretz/bine v0.2.0 github.com/go-chi/chi/v5 v5.2.1 github.com/go-chi/render v1.0.3 - github.com/gofrs/uuid/v5 v5.3.0 + github.com/gofrs/uuid/v5 v5.3.2 github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 github.com/libdns/alidns v1.0.3 github.com/libdns/cloudflare v0.1.1 @@ -29,7 +29,7 @@ require ( github.com/sagernet/sing v0.6.5 github.com/sagernet/sing-dns v0.4.1 github.com/sagernet/sing-mux v0.3.1 - github.com/sagernet/sing-quic v0.4.0 + github.com/sagernet/sing-quic v0.4.1 github.com/sagernet/sing-shadowsocks v0.2.7 github.com/sagernet/sing-shadowsocks2 v0.2.0 github.com/sagernet/sing-shadowtls v0.2.0 diff --git a/go.sum b/go.sum index 9e22fefc..5d0ac955 100644 --- a/go.sum +++ b/go.sum @@ -31,8 +31,8 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk= -github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= +github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0= +github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= @@ -125,8 +125,8 @@ github.com/sagernet/sing-dns v0.4.1 h1:nozS7iqpxZ7aV73oHbkD/8haOvf3XXDCgT//8NdYi github.com/sagernet/sing-dns v0.4.1/go.mod h1:dweQs54ng2YGzoJfz+F9dGuDNdP5pJ3PLeggnK5VWc8= github.com/sagernet/sing-mux v0.3.1 h1:kvCc8HyGAskDHDQ0yQvoTi/7J4cZPB/VJMsAM3MmdQI= github.com/sagernet/sing-mux v0.3.1/go.mod h1:Mkdz8LnDstthz0HWuA/5foncnDIdcNN5KZ6AdJX+x78= -github.com/sagernet/sing-quic v0.4.0 h1:E4geazHk/UrJTXMlT+CBCKmn8V86RhtNeczWtfeoEFc= -github.com/sagernet/sing-quic v0.4.0/go.mod h1:c+CytOEyeN20KCTFIP8YQUkNDVFLSzjrEPqP7Hlnxys= +github.com/sagernet/sing-quic v0.4.1 h1:pxlMa4efZu/M07RgGagNNDDyl6ZUwpmNUjRTpgHOWK4= +github.com/sagernet/sing-quic v0.4.1/go.mod h1:tqPa0/Wqa19MkkSlKVZZX5sHxtiDR9BROcn4ufcbVdY= github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8= github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE= github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg= From af17eaa537524a330ecc497a16c70ec213b76ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 4 Apr 2025 16:21:50 +0800 Subject: [PATCH 07/16] Improve sniffer --- common/sniff/bittorrent.go | 5 +++-- common/sniff/dns.go | 16 ++++------------ common/sniff/http.go | 8 +++++++- common/sniff/quic.go | 4 +--- common/sniff/quic_test.go | 6 +++--- common/sniff/rdp.go | 19 ++++++++++--------- common/sniff/sniff.go | 20 +++++++++++++------- common/sniff/ssh.go | 18 +++++++++++------- common/sniff/tls.go | 8 +++++++- go.mod | 2 +- go.sum | 4 ++-- route/route.go | 5 +++-- 12 files changed, 65 insertions(+), 50 deletions(-) diff --git a/common/sniff/bittorrent.go b/common/sniff/bittorrent.go index 9e123c41..9fa7d8b8 100644 --- a/common/sniff/bittorrent.go +++ b/common/sniff/bittorrent.go @@ -9,6 +9,7 @@ import ( "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" ) const ( @@ -23,7 +24,7 @@ func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.R var first byte err := binary.Read(reader, binary.BigEndian, &first) if err != nil { - return err + return E.Cause1(ErrNeedMoreData, err) } if first != 19 { @@ -33,7 +34,7 @@ func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.R var protocol [19]byte _, err = reader.Read(protocol[:]) if err != nil { - return err + return E.Cause1(ErrNeedMoreData, err) } if string(protocol[:]) != "BitTorrent protocol" { return os.ErrInvalid diff --git a/common/sniff/dns.go b/common/sniff/dns.go index 4eb1990c..2e22f3d7 100644 --- a/common/sniff/dns.go +++ b/common/sniff/dns.go @@ -5,13 +5,11 @@ import ( "encoding/binary" "io" "os" - "time" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/task" + E "github.com/sagernet/sing/common/exceptions" mDNS "github.com/miekg/dns" ) @@ -20,22 +18,16 @@ func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundCon var length uint16 err := binary.Read(reader, binary.BigEndian, &length) if err != nil { - return os.ErrInvalid + return E.Cause1(ErrNeedMoreData, err) } if length == 0 { return os.ErrInvalid } buffer := buf.NewSize(int(length)) defer buffer.Release() - readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100) - var readTask task.Group - readTask.Append0(func(ctx context.Context) error { - return common.Error(buffer.ReadFullFrom(reader, buffer.FreeLen())) - }) - err = readTask.Run(readCtx) - cancel() + _, err = buffer.ReadFullFrom(reader, buffer.FreeLen()) if err != nil { - return err + return E.Cause1(ErrNeedMoreData, err) } return DomainNameQuery(readCtx, metadata, buffer.Bytes()) } diff --git a/common/sniff/http.go b/common/sniff/http.go index 0e6ab406..012f2c99 100644 --- a/common/sniff/http.go +++ b/common/sniff/http.go @@ -3,10 +3,12 @@ package sniff import ( std_bufio "bufio" "context" + "errors" "io" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/protocol/http" ) @@ -14,7 +16,11 @@ import ( func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { request, err := http.ReadRequest(std_bufio.NewReader(reader)) if err != nil { - return err + if errors.Is(err, io.ErrUnexpectedEOF) { + return E.Cause1(ErrNeedMoreData, err) + } else { + return err + } } metadata.Protocol = C.ProtocolHTTP metadata.Domain = M.ParseSocksaddr(request.Host).AddrString() diff --git a/common/sniff/quic.go b/common/sniff/quic.go index adec7008..4c2e667c 100644 --- a/common/sniff/quic.go +++ b/common/sniff/quic.go @@ -20,8 +20,6 @@ import ( "golang.org/x/crypto/hkdf" ) -var ErrClientHelloFragmented = E.New("need more packet for chromium QUIC connection") - func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { reader := bytes.NewReader(packet) typeByte, err := reader.ReadByte() @@ -308,7 +306,7 @@ find: metadata.Protocol = C.ProtocolQUIC metadata.Client = C.ClientChromium metadata.SniffContext = fragments - return ErrClientHelloFragmented + return E.Cause1(ErrNeedMoreData, err) } metadata.Domain = fingerprint.ServerName for metadata.Client == "" { diff --git a/common/sniff/quic_test.go b/common/sniff/quic_test.go index 1fbf6096..1149f68e 100644 --- a/common/sniff/quic_test.go +++ b/common/sniff/quic_test.go @@ -20,11 +20,11 @@ func TestSniffQUICChromeNew(t *testing.T) { err = sniff.QUICClientHello(context.Background(), &metadata, pkt) require.Equal(t, metadata.Protocol, C.ProtocolQUIC) require.Equal(t, metadata.Client, C.ClientChromium) - require.ErrorIs(t, err, sniff.ErrClientHelloFragmented) + require.ErrorIs(t, err, sniff.ErrNeedMoreData) pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894") require.NoError(t, err) err = sniff.QUICClientHello(context.Background(), &metadata, pkt) - require.ErrorIs(t, err, sniff.ErrClientHelloFragmented) + require.ErrorIs(t, err, sniff.ErrNeedMoreData) pkt, err = hex.DecodeString("c20000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489e2ff30c43a5f63beb2e4501ce7754085bcbe838003a0b4bccb53863c0766df7eac073c2bdc170772b157997945acdc2ab2e84750cc9aa0ffa0fdc023da7fc565a14f87f7c563dbc9183dd226aab79957d263f66e64b85a1b15a24516bd2c7c04eea4fa0a34ef9849c21585db2e4adb7c05e265c4f38d8ffe4cbed0f3b0e68f3693bf1f726c3fb135b8e32a5d22931d7c55fc2ff4b9a354933ab14544df3cdaf3e3217dfb8d7feb3465dc34df6320ea486f12e5b2d609aaa5f4515c20c86fc440f8087be0ee3d339835746ae2573c2afdee6bb6ef7e9eb541feae9209391b2902cfb0bdaccd9da8d290714638b7da588d4a656ca6eabba78b7363922d6037cf060b161a42019d4feb4156459103cffdeefd0e63114af2b0e0c39e70ebc7fecb8dd1ebb8d60b2137f509bb7dcef5f1d3e06ab1d391466652d57440a410fb4f58a6ce1fb62feb453241f64e110709f59a3d9ebdac94f811337d0e4a80fd6b56b2a70cd6eebbf98e1661291da6bf5beb8b8afc376dfd20eb76afe709e8e8f28e0ef82105954e346546ad25973df43f4acddbec0ffd9b215f62abebebf71305b5ea993560316f69430bf5afe50420340622f802b5830f3bcebffff04980c75a59d28902879e5d51a4fb21062a4ae13c42297075b21d54ee04303879c1157e7470c1451673c98a2f3921f2f3e8f6acfe85b01caaca66b59e5ebffbfe68e5e9ab17e9a1b857eb409df91cb76767fc1814fd3c522a9b117edd0b02526e469cb4afb291a4dcc74c79b47ec6e7ce558c597129366f83ec306b11d2598c705fd4ee9ee99df6b7039bef13b08fc6f26853ad213829d24f895747d45a47414f931c583fb6c3e4f6c27d0c2b81a5f3cee390ec6314e1fec637e8d28b675e97caafdfbf8c25d34a635083a7553d219dd80dbb39087d74c6ad6192ca6f48a3ff8d47db41b2a492c63fcd780012780931dae0a325f9dcbd772d09a700f132c4bc1d9809b25b9751b694eb72a8ba4db7208d2b1bab63e1845208e4f841ea30218a559db98751589716b6d059ca673378f5fe7c7d8a1c82e14a561c47313bbcc278412ba86ffb2b87ec308eab9df696f5b4b54f8e361731bf232820a02a35fda7e5d4bf01b8f005ad299a055116e7b23c181f15a66442cf6032ca477bccc55b79d424eb4f245847bd81a581dc369dd20b1a4892733bde3c38e492c0039f69f2b947a4dc251a49ee7ccc0f36b3b75a555fa1d126db75f94dab60f52f6b15a877a0c380b59f82d35c570bc5f8051e9ef87db51f52383d47b50829b7f9e947ccc67aa280566aa48b4a85c1c7eca6f542789d8abcc050f1aa3cc221b6859656a21454aa21c7bfb9d12115f61c3ed46263ade68a8d3679fa62a659a5da7817406bd16618fccf33ed208ada1b03584e8b485d3cb6ed80a0774e60b6cd55aff64169ea998cf8235997049515abac58e0169ca07fb1c8c4c8b2803ba9d27b44c045d0a1cac86e5e188195c68001f53eb44851b6d821fc01ccbb41e27f38e6ddd66540c2d62ed6e0d551e22c0f26b60078c74a6302a1ed3d9e8fc0861257a63f6ac4e759fd54bff088becd28e30944a6c15db4fc8ae6244346869add946d9d92c430d737e042fa18b28a8ed64d1e8987ad9061cdc1335f") require.NoError(t, err) err = sniff.QUICClientHello(context.Background(), &metadata, pkt) @@ -40,7 +40,7 @@ func TestSniffQUICChromium(t *testing.T) { err = sniff.QUICClientHello(context.Background(), &metadata, pkt) require.Equal(t, metadata.Protocol, C.ProtocolQUIC) require.Equal(t, metadata.Client, C.ClientChromium) - require.ErrorIs(t, err, sniff.ErrClientHelloFragmented) + require.ErrorIs(t, err, sniff.ErrNeedMoreData) pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28") require.NoError(t, err) err = sniff.QUICClientHello(context.Background(), &metadata, pkt) diff --git a/common/sniff/rdp.go b/common/sniff/rdp.go index 391ebd26..37551fef 100644 --- a/common/sniff/rdp.go +++ b/common/sniff/rdp.go @@ -8,6 +8,7 @@ import ( "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/rw" ) @@ -15,7 +16,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) var tpktVersion uint8 err := binary.Read(reader, binary.BigEndian, &tpktVersion) if err != nil { - return err + return E.Cause1(ErrNeedMoreData, err) } if tpktVersion != 0x03 { return os.ErrInvalid @@ -24,7 +25,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) var tpktReserved uint8 err = binary.Read(reader, binary.BigEndian, &tpktReserved) if err != nil { - return err + return E.Cause1(ErrNeedMoreData, err) } if tpktReserved != 0x00 { return os.ErrInvalid @@ -33,7 +34,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) var tpktLength uint16 err = binary.Read(reader, binary.BigEndian, &tpktLength) if err != nil { - return err + return E.Cause1(ErrNeedMoreData, err) } if tpktLength != 19 { @@ -43,7 +44,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) var cotpLength uint8 err = binary.Read(reader, binary.BigEndian, &cotpLength) if err != nil { - return err + return E.Cause1(ErrNeedMoreData, err) } if cotpLength != 14 { @@ -53,7 +54,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) var cotpTpduType uint8 err = binary.Read(reader, binary.BigEndian, &cotpTpduType) if err != nil { - return err + return E.Cause1(ErrNeedMoreData, err) } if cotpTpduType != 0xE0 { return os.ErrInvalid @@ -61,13 +62,13 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) err = rw.SkipN(reader, 5) if err != nil { - return err + return E.Cause1(ErrNeedMoreData, err) } var rdpType uint8 err = binary.Read(reader, binary.BigEndian, &rdpType) if err != nil { - return err + return E.Cause1(ErrNeedMoreData, err) } if rdpType != 0x01 { return os.ErrInvalid @@ -75,12 +76,12 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) var rdpFlags uint8 err = binary.Read(reader, binary.BigEndian, &rdpFlags) if err != nil { - return err + return E.Cause1(ErrNeedMoreData, err) } var rdpLength uint8 err = binary.Read(reader, binary.BigEndian, &rdpLength) if err != nil { - return err + return E.Cause1(ErrNeedMoreData, err) } if rdpLength != 8 { return os.ErrInvalid diff --git a/common/sniff/sniff.go b/common/sniff/sniff.go index ecb0488b..59e81aaa 100644 --- a/common/sniff/sniff.go +++ b/common/sniff/sniff.go @@ -3,6 +3,7 @@ package sniff import ( "bytes" "context" + "errors" "io" "net" "time" @@ -19,6 +20,8 @@ type ( PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error ) +var ErrNeedMoreData = E.New("need more data") + func Skip(metadata *adapter.InboundContext) bool { // skip server first protocols switch metadata.Destination.Port { @@ -40,7 +43,7 @@ func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net. timeout = C.ReadPayloadTimeout } deadline := time.Now().Add(timeout) - var errors []error + var sniffError error for i := 0; ; i++ { err := conn.SetReadDeadline(deadline) if err != nil { @@ -54,7 +57,7 @@ func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net. } return E.Cause(err, "read payload") } - errors = nil + sniffError = nil for _, sniffer := range sniffers { reader := io.MultiReader(common.Map(append(buffers, buffer), func(it *buf.Buffer) io.Reader { return bytes.NewReader(it.Bytes()) @@ -63,20 +66,23 @@ func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net. if err == nil { return nil } - errors = append(errors, err) + sniffError = E.Errors(sniffError, err) + } + if !errors.Is(err, ErrNeedMoreData) { + break } } - return E.Errors(errors...) + return sniffError } func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error { - var errors []error + var sniffError []error for _, sniffer := range sniffers { err := sniffer(ctx, metadata, packet) if err == nil { return nil } - errors = append(errors, err) + sniffError = append(sniffError, err) } - return E.Errors(errors...) + return E.Errors(sniffError...) } diff --git a/common/sniff/ssh.go b/common/sniff/ssh.go index 194d0bda..d373d292 100644 --- a/common/sniff/ssh.go +++ b/common/sniff/ssh.go @@ -5,22 +5,26 @@ import ( "context" "io" "os" - "strings" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" ) func SSH(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { - scanner := bufio.NewScanner(reader) - if !scanner.Scan() { + const sshPrefix = "SSH-2.0-" + bReader := bufio.NewReader(reader) + prefix, err := bReader.Peek(len(sshPrefix)) + if err != nil { + return E.Cause1(ErrNeedMoreData, err) + } else if string(prefix) != sshPrefix { return os.ErrInvalid } - fistLine := scanner.Text() - if !strings.HasPrefix(fistLine, "SSH-2.0-") { - return os.ErrInvalid + fistLine, _, err := bReader.ReadLine() + if err != nil { + return err } metadata.Protocol = C.ProtocolSSH - metadata.Client = fistLine[8:] + metadata.Client = string(fistLine)[8:] return nil } diff --git a/common/sniff/tls.go b/common/sniff/tls.go index 6fe430e2..613086e8 100644 --- a/common/sniff/tls.go +++ b/common/sniff/tls.go @@ -3,11 +3,13 @@ package sniff import ( "context" "crypto/tls" + "errors" "io" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" ) func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error { @@ -23,5 +25,9 @@ func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reade metadata.Domain = clientHello.ServerName return nil } - return err + if errors.Is(err, io.ErrUnexpectedEOF) { + return E.Cause1(ErrNeedMoreData, err) + } else { + return err + } } diff --git a/go.mod b/go.mod index d21192ce..c42ecf7c 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff github.com/sagernet/quic-go v0.49.0-beta.1 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 - github.com/sagernet/sing v0.6.5 + github.com/sagernet/sing v0.6.6-0.20250406082302-d3673bff4af8 github.com/sagernet/sing-dns v0.4.1 github.com/sagernet/sing-mux v0.3.1 github.com/sagernet/sing-quic v0.4.1 diff --git a/go.sum b/go.sum index 5d0ac955..921cfe3d 100644 --- a/go.sum +++ b/go.sum @@ -119,8 +119,8 @@ github.com/sagernet/quic-go v0.49.0-beta.1/go.mod h1:uesWD1Ihrldq1M3XtjuEvIUqi8W github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= -github.com/sagernet/sing v0.6.5 h1:TBKTK6Ms0/MNTZm+cTC2hhKunE42XrNIdsxcYtWqeUU= -github.com/sagernet/sing v0.6.5/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.6.6-0.20250406082302-d3673bff4af8 h1:1jHChanwnGF5DJZ5pR/RkVf69VyjQxfDVfOMJx7bPyI= +github.com/sagernet/sing v0.6.6-0.20250406082302-d3673bff4af8/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-dns v0.4.1 h1:nozS7iqpxZ7aV73oHbkD/8haOvf3XXDCgT//8NdYirk= github.com/sagernet/sing-dns v0.4.1/go.mod h1:dweQs54ng2YGzoJfz+F9dGuDNdP5pJ3PLeggnK5VWc8= github.com/sagernet/sing-mux v0.3.1 h1:kvCc8HyGAskDHDQ0yQvoTi/7J4cZPB/VJMsAM3MmdQI= diff --git a/route/route.go b/route/route.go index 6ab4cc97..dde91db8 100644 --- a/route/route.go +++ b/route/route.go @@ -549,7 +549,7 @@ func (r *Router) actionSniff( sniffBuffer.Release() } } else if inputPacketConn != nil { - if metadata.PacketSniffError != nil && !errors.Is(metadata.PacketSniffError, sniff.ErrClientHelloFragmented) { + if metadata.PacketSniffError != nil && !errors.Is(metadata.PacketSniffError, sniff.ErrNeedMoreData) { r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.PacketSniffError) return } @@ -618,7 +618,8 @@ func (r *Router) actionSniff( } packetBuffers = append(packetBuffers, packetBuffer) metadata.PacketSniffError = err - if errors.Is(err, sniff.ErrClientHelloFragmented) { + if errors.Is(err, sniff.ErrNeedMoreData) { + // TODO: replace with generic message when there are more multi-packet protocols r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello") continue } From 24af0766ac2f4dc36acf90c4298ca31ab6cd7af8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 6 Apr 2025 16:19:46 +0800 Subject: [PATCH 08/16] Fix uTP sniffer --- common/sniff/bittorrent.go | 4 +++- common/sniff/bittorrent_test.go | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/common/sniff/bittorrent.go b/common/sniff/bittorrent.go index 9fa7d8b8..39c19598 100644 --- a/common/sniff/bittorrent.go +++ b/common/sniff/bittorrent.go @@ -68,7 +68,9 @@ func UTP(_ context.Context, metadata *adapter.InboundContext, packet []byte) err if err != nil { return err } - + if extension > 0x04 { + return os.ErrInvalid + } var length byte err = binary.Read(reader, binary.BigEndian, &length) if err != nil { diff --git a/common/sniff/bittorrent_test.go b/common/sniff/bittorrent_test.go index 65f095bd..f4762e32 100644 --- a/common/sniff/bittorrent_test.go +++ b/common/sniff/bittorrent_test.go @@ -71,3 +71,19 @@ func TestSniffUDPTracker(t *testing.T) { require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol) } } + +func TestSniffNotUTP(t *testing.T) { + t.Parallel() + + packets := []string{ + "0102736470696e674958d580121500000000000079aaed6717a39c27b07c0c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + } + for _, pkt := range packets { + pkt, err := hex.DecodeString(pkt) + require.NoError(t, err) + + var metadata adapter.InboundContext + err = sniff.UTP(context.TODO(), &metadata, pkt) + require.Error(t, err) + } +} From 97d41ffde8339eee50fcdb6245690f179f5c2297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 6 Apr 2025 16:35:18 +0800 Subject: [PATCH 09/16] Improve pause management --- experimental/libbox/service.go | 2 +- experimental/libbox/service_pause.go | 30 +++++++++-------- protocol/group/urltest.go | 49 +++++++++++++--------------- protocol/wireguard/endpoint.go | 1 - protocol/wireguard/outbound.go | 1 - route/route.go | 7 ---- route/rule/rule_set_remote.go | 23 +++++++------ transport/wireguard/endpoint.go | 14 ++++---- 8 files changed, 60 insertions(+), 67 deletions(-) diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 8d42d26e..16c04a1f 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -40,7 +40,7 @@ type BoxService struct { clashServer adapter.ClashServer pauseManager pause.Manager - servicePauseFields + iOSPauseFields } func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) { diff --git a/experimental/libbox/service_pause.go b/experimental/libbox/service_pause.go index 0fa9541f..791684ec 100644 --- a/experimental/libbox/service_pause.go +++ b/experimental/libbox/service_pause.go @@ -1,31 +1,33 @@ package libbox import ( - "sync" "time" + + C "github.com/sagernet/sing-box/constant" ) -type servicePauseFields struct { - pauseAccess sync.Mutex - pauseTimer *time.Timer +type iOSPauseFields struct { + endPauseTimer *time.Timer } func (s *BoxService) Pause() { - s.pauseAccess.Lock() - defer s.pauseAccess.Unlock() - if s.pauseTimer != nil { - s.pauseTimer.Stop() + s.pauseManager.DevicePause() + if !C.IsIos { + s.instance.Router().ResetNetwork() + } else { + if s.endPauseTimer == nil { + s.endPauseTimer = time.AfterFunc(time.Minute, s.pauseManager.DeviceWake) + } else { + s.endPauseTimer.Reset(time.Minute) + } } - s.pauseTimer = time.AfterFunc(3*time.Second, s.ResetNetwork) } func (s *BoxService) Wake() { - s.pauseAccess.Lock() - defer s.pauseAccess.Unlock() - if s.pauseTimer != nil { - s.pauseTimer.Stop() + if !C.IsIos { + s.pauseManager.DeviceWake() + s.instance.Router().ResetNetwork() } - s.pauseTimer = time.AfterFunc(3*time.Minute, s.ResetNetwork) } func (s *BoxService) ResetNetwork() { diff --git a/protocol/group/urltest.go b/protocol/group/urltest.go index 564c2373..e52ec906 100644 --- a/protocol/group/urltest.go +++ b/protocol/group/urltest.go @@ -19,6 +19,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" ) @@ -27,10 +28,7 @@ func RegisterURLTest(registry *outbound.Registry) { outbound.Register[option.URLTestOutboundOptions](registry, C.TypeURLTest, NewURLTest) } -var ( - _ adapter.OutboundGroup = (*URLTest)(nil) - _ adapter.InterfaceUpdateListener = (*URLTest)(nil) -) +var _ adapter.OutboundGroup = (*URLTest)(nil) type URLTest struct { outbound.Adapter @@ -172,15 +170,12 @@ func (s *URLTest) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, s.connection.NewPacketConnection(ctx, s, conn, metadata, onClose) } -func (s *URLTest) InterfaceUpdated() { - go s.group.CheckOutbounds(true) - return -} - type URLTestGroup struct { ctx context.Context router adapter.Router - outboundManager adapter.OutboundManager + outbound adapter.OutboundManager + pause pause.Manager + pauseCallback *list.Element[pause.Callback] logger log.Logger outbounds []adapter.Outbound link string @@ -189,17 +184,15 @@ type URLTestGroup struct { idleTimeout time.Duration history *urltest.HistoryStorage checking atomic.Bool - pauseManager pause.Manager selectedOutboundTCP adapter.Outbound selectedOutboundUDP adapter.Outbound interruptGroup *interrupt.Group interruptExternalConnections bool - - access sync.Mutex - ticker *time.Ticker - close chan struct{} - started bool - lastActive atomic.TypedValue[time.Time] + access sync.Mutex + ticker *time.Ticker + close chan struct{} + started bool + lastActive atomic.TypedValue[time.Time] } func NewURLTestGroup(ctx context.Context, outboundManager adapter.OutboundManager, logger log.Logger, outbounds []adapter.Outbound, link string, interval time.Duration, tolerance uint16, idleTimeout time.Duration, interruptExternalConnections bool) (*URLTestGroup, error) { @@ -224,7 +217,7 @@ func NewURLTestGroup(ctx context.Context, outboundManager adapter.OutboundManage } return &URLTestGroup{ ctx: ctx, - outboundManager: outboundManager, + outbound: outboundManager, logger: logger, outbounds: outbounds, link: link, @@ -233,13 +226,15 @@ func NewURLTestGroup(ctx context.Context, outboundManager adapter.OutboundManage idleTimeout: idleTimeout, history: history, close: make(chan struct{}), - pauseManager: service.FromContext[pause.Manager](ctx), + pause: service.FromContext[pause.Manager](ctx), interruptGroup: interrupt.NewGroup(), interruptExternalConnections: interruptExternalConnections, }, nil } func (g *URLTestGroup) PostStart() { + g.access.Lock() + defer g.access.Unlock() g.started = true g.lastActive.Store(time.Now()) go g.CheckOutbounds(false) @@ -249,24 +244,25 @@ func (g *URLTestGroup) Touch() { if !g.started { return } + g.access.Lock() + defer g.access.Unlock() if g.ticker != nil { g.lastActive.Store(time.Now()) return } - g.access.Lock() - defer g.access.Unlock() - if g.ticker != nil { - return - } g.ticker = time.NewTicker(g.interval) go g.loopCheck() + g.pauseCallback = pause.RegisterTicker(g.pause, g.ticker, g.interval, nil) } func (g *URLTestGroup) Close() error { + g.access.Lock() + defer g.access.Unlock() if g.ticker == nil { return nil } g.ticker.Stop() + g.pause.UnregisterCallback(g.pauseCallback) close(g.close) return nil } @@ -330,10 +326,11 @@ func (g *URLTestGroup) loopCheck() { g.access.Lock() g.ticker.Stop() g.ticker = nil + g.pause.UnregisterCallback(g.pauseCallback) + g.pauseCallback = nil g.access.Unlock() return } - g.pauseManager.WaitActive() g.CheckOutbounds(false) } } @@ -366,7 +363,7 @@ func (g *URLTestGroup) urlTest(ctx context.Context, force bool) (map[string]uint continue } checked[realTag] = true - p, loaded := g.outboundManager.Outbound(realTag) + p, loaded := g.outbound.Outbound(realTag) if !loaded { continue } diff --git a/protocol/wireguard/endpoint.go b/protocol/wireguard/endpoint.go index 21d72bd9..c9751c36 100644 --- a/protocol/wireguard/endpoint.go +++ b/protocol/wireguard/endpoint.go @@ -120,7 +120,6 @@ func (w *Endpoint) Close() error { func (w *Endpoint) InterfaceUpdated() { w.endpoint.BindUpdate() - return } func (w *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr) error { diff --git a/protocol/wireguard/outbound.go b/protocol/wireguard/outbound.go index 3e299705..7a516598 100644 --- a/protocol/wireguard/outbound.go +++ b/protocol/wireguard/outbound.go @@ -126,7 +126,6 @@ func (o *Outbound) Close() error { func (o *Outbound) InterfaceUpdated() { o.endpoint.BindUpdate() - return } func (o *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { diff --git a/route/route.go b/route/route.go index dde91db8..d6611b4f 100644 --- a/route/route.go +++ b/route/route.go @@ -60,10 +60,6 @@ func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata } func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { - if r.pauseManager.IsDevicePaused() { - return E.New("reject connection to ", metadata.Destination, " while device paused") - } - //nolint:staticcheck if metadata.InboundDetour != "" { if metadata.LastInbound == metadata.InboundDetour { @@ -186,9 +182,6 @@ func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, } func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { - if r.pauseManager.IsDevicePaused() { - return E.New("reject packet connection to ", metadata.Destination, " while device paused") - } //nolint:staticcheck if metadata.InboundDetour != "" { if metadata.LastInbound == metadata.InboundDetour { diff --git a/route/rule/rule_set_remote.go b/route/rule/rule_set_remote.go index 9e0c1729..c29d6616 100644 --- a/route/rule/rule_set_remote.go +++ b/route/rule/rule_set_remote.go @@ -103,7 +103,7 @@ func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext *adapter. } } if s.lastUpdated.IsZero() { - err := s.fetchOnce(ctx, startContext) + err := s.fetch(ctx, startContext) if err != nil { return E.Cause(err, "initial rule-set: ", s.options.Tag) } @@ -198,7 +198,7 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error { func (s *RemoteRuleSet) loopUpdate() { if time.Since(s.lastUpdated) > s.updateInterval { - err := s.fetchOnce(s.ctx, nil) + err := s.fetch(s.ctx, nil) if err != nil { s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) } else if s.refs.Load() == 0 { @@ -211,18 +211,21 @@ func (s *RemoteRuleSet) loopUpdate() { case <-s.ctx.Done(): return case <-s.updateTicker.C: - s.pauseManager.WaitActive() - err := s.fetchOnce(s.ctx, nil) - if err != nil { - s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) - } else if s.refs.Load() == 0 { - s.rules = nil - } + s.updateOnce() } } } -func (s *RemoteRuleSet) fetchOnce(ctx context.Context, startContext *adapter.HTTPStartContext) error { +func (s *RemoteRuleSet) updateOnce() { + err := s.fetch(s.ctx, nil) + if err != nil { + s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) + } else if s.refs.Load() == 0 { + s.rules = nil + } +} + +func (s *RemoteRuleSet) fetch(ctx context.Context, startContext *adapter.HTTPStartContext) error { s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL) var httpClient *http.Client if startContext != nil { diff --git a/transport/wireguard/endpoint.go b/transport/wireguard/endpoint.go index 69ce9170..17a58a6c 100644 --- a/transport/wireguard/endpoint.go +++ b/transport/wireguard/endpoint.go @@ -30,7 +30,7 @@ type Endpoint struct { allowedAddress []netip.Prefix tunDevice Device device *device.Device - pauseManager pause.Manager + pause pause.Manager pauseCallback *list.Element[pause.Callback] } @@ -187,9 +187,9 @@ func (e *Endpoint) Start(resolve bool) error { return E.Cause(err, "setup wireguard: \n", ipcConf) } e.device = wgDevice - e.pauseManager = service.FromContext[pause.Manager](e.options.Context) - if e.pauseManager != nil { - e.pauseCallback = e.pauseManager.RegisterCallback(e.onPauseUpdated) + e.pause = service.FromContext[pause.Manager](e.options.Context) + if e.pause != nil { + e.pauseCallback = e.pause.RegisterCallback(e.onPauseUpdated) } return nil } @@ -217,16 +217,16 @@ func (e *Endpoint) Close() error { e.device.Close() } if e.pauseCallback != nil { - e.pauseManager.UnregisterCallback(e.pauseCallback) + e.pause.UnregisterCallback(e.pauseCallback) } return nil } func (e *Endpoint) onPauseUpdated(event int) { switch event { - case pause.EventDevicePaused: + case pause.EventDevicePaused, pause.EventNetworkPause: e.device.Down() - case pause.EventDeviceWake: + case pause.EventDeviceWake, pause.EventNetworkWake: e.device.Up() } } From 991e7557898e8fc297cc541f0b8ed50487117bf6 Mon Sep 17 00:00:00 2001 From: Mahdi <80265960+Mahdi-zarei@users.noreply.github.com> Date: Thu, 3 Apr 2025 11:40:43 +0330 Subject: [PATCH 10/16] Fix conn copy --- route/conn.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/route/conn.go b/route/conn.go index 17218387..319e463c 100644 --- a/route/conn.go +++ b/route/conn.go @@ -246,7 +246,7 @@ func (m *ConnectionManager) connectionCopy(ctx context.Context, source net.Conn, return } } - _, err := bufio.CopyWithCounters(destination, sourceReader, source, readCounters, writeCounters) + _, err := bufio.CopyWithCounters(destinationWriter, sourceReader, source, readCounters, writeCounters) if err != nil { common.Close(source, destination) } else if duplexDst, isDuplex := destination.(N.WriteCloser); isDuplex { From a15b5a2463d9e52945ff71908c2daa97e84a7011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 6 Apr 2025 19:51:24 +0800 Subject: [PATCH 11/16] Fix `no_drop` not work --- route/rule/rule_action.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index 8989ff3c..d4c6625d 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -279,6 +279,9 @@ func (r *RuleActionReject) Error(ctx context.Context) error { default: panic(F.ToString("unknown reject method: ", r.Method)) } + if r.NoDrop { + return returnErr + } r.dropAccess.Lock() defer r.dropAccess.Unlock() timeNow := time.Now() From 594ee480a24da63652666ba6a5bddccb19405b98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 6 Apr 2025 20:21:55 +0800 Subject: [PATCH 12/16] option: Fix listable --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c42ecf7c..3c1ab644 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff github.com/sagernet/quic-go v0.49.0-beta.1 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 - github.com/sagernet/sing v0.6.6-0.20250406082302-d3673bff4af8 + github.com/sagernet/sing v0.6.6-0.20250406121928-926a5a1e8bb7 github.com/sagernet/sing-dns v0.4.1 github.com/sagernet/sing-mux v0.3.1 github.com/sagernet/sing-quic v0.4.1 diff --git a/go.sum b/go.sum index 921cfe3d..7aa82f74 100644 --- a/go.sum +++ b/go.sum @@ -119,8 +119,8 @@ github.com/sagernet/quic-go v0.49.0-beta.1/go.mod h1:uesWD1Ihrldq1M3XtjuEvIUqi8W github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= -github.com/sagernet/sing v0.6.6-0.20250406082302-d3673bff4af8 h1:1jHChanwnGF5DJZ5pR/RkVf69VyjQxfDVfOMJx7bPyI= -github.com/sagernet/sing v0.6.6-0.20250406082302-d3673bff4af8/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.6.6-0.20250406121928-926a5a1e8bb7 h1:ZJauxLmH12Gzv3nucfjsSBQw9UA8t7Sxu8pYHBSP2TU= +github.com/sagernet/sing v0.6.6-0.20250406121928-926a5a1e8bb7/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-dns v0.4.1 h1:nozS7iqpxZ7aV73oHbkD/8haOvf3XXDCgT//8NdYirk= github.com/sagernet/sing-dns v0.4.1/go.mod h1:dweQs54ng2YGzoJfz+F9dGuDNdP5pJ3PLeggnK5VWc8= github.com/sagernet/sing-mux v0.3.1 h1:kvCc8HyGAskDHDQ0yQvoTi/7J4cZPB/VJMsAM3MmdQI= From ae9bc7acf175cdeb9e7be9508e0538673df70e12 Mon Sep 17 00:00:00 2001 From: testing <58134720+testing765@users.noreply.github.com> Date: Sun, 6 Apr 2025 15:35:59 +0300 Subject: [PATCH 13/16] documentation: Fix typo Signed-off-by: testing <58134720+testing765@users.noreply.github.com> --- docs/configuration/route/sniff.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration/route/sniff.md b/docs/configuration/route/sniff.md index 40de038c..3880f790 100644 --- a/docs/configuration/route/sniff.md +++ b/docs/configuration/route/sniff.md @@ -29,7 +29,7 @@ If enabled in the inbound, the protocol and domain name (if present) of by the c | QUIC Client | Type | |:------------------------:|:----------:| -| Chromium/Cronet | `chrimium` | +| Chromium/Cronet | `chromium` | | Safari/Apple Network API | `safari` | | Firefox / uquic firefox | `firefox` | -| quic-go / uquic chrome | `quic-go` | \ No newline at end of file +| quic-go / uquic chrome | `quic-go` | From 9668ea69b87e04c204e094593200d9a1129deb5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 7 Apr 2025 15:05:15 +0800 Subject: [PATCH 14/16] Fix windows process searcher --- common/process/searcher_windows.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/common/process/searcher_windows.go b/common/process/searcher_windows.go index 5b3d59b5..6ea7c709 100644 --- a/common/process/searcher_windows.go +++ b/common/process/searcher_windows.go @@ -124,14 +124,6 @@ func (s *searcher) Search(b []byte, ip netip.Addr, port uint16) (uint32, error) for i := 0; i < n; i++ { row := b[4+itemSize*i : 4+itemSize*(i+1)] - if s.tcpState >= 0 { - tcpState := readNativeUint32(row[s.tcpState : s.tcpState+4]) - // MIB_TCP_STATE_ESTAB, only check established connections for TCP - if tcpState != 5 { - continue - } - } - // according to MSDN, only the lower 16 bits of dwLocalPort are used and the port number is in network endian. // this field can be illustrated as follows depends on different machine endianess: // little endian: [ MSB LSB 0 0 ] interpret as native uint32 is ((LSB<<8)|MSB) @@ -144,7 +136,7 @@ func (s *searcher) Search(b []byte, ip netip.Addr, port uint16) (uint32, error) srcIP, _ := netip.AddrFromSlice(row[s.ip : s.ip+s.ipSize]) // windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto - if ip != srcIP && (!srcIP.IsUnspecified() || s.tcpState != -1) { + if ip != srcIP && (!srcIP.IsUnspecified()) { continue } From 5adaf1ac75df426651a9079ca3f69dff5da7f6ef Mon Sep 17 00:00:00 2001 From: Fei1Yang Date: Tue, 8 Apr 2025 14:20:31 +0800 Subject: [PATCH 15/16] Mark config file as noreplace for rpm --- .fpm | 1 + .goreleaser.fury.yaml | 2 +- .goreleaser.yaml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.fpm b/.fpm index 718244b2..82e786bc 100644 --- a/.fpm +++ b/.fpm @@ -6,6 +6,7 @@ --url "https://sing-box.sagernet.org/" --maintainer "nekohasekai " --deb-field "Bug: https://github.com/SagerNet/sing-box/issues" +--config-files /etc/sing-box/config.json release/config/config.json=/etc/sing-box/config.json diff --git a/.goreleaser.fury.yaml b/.goreleaser.fury.yaml index fbd1ae42..3212027a 100644 --- a/.goreleaser.fury.yaml +++ b/.goreleaser.fury.yaml @@ -48,7 +48,7 @@ nfpms: contents: - src: release/config/config.json dst: /etc/sing-box/config.json - type: config + type: "config|noreplace" - src: release/config/sing-box.service dst: /usr/lib/systemd/system/sing-box.service diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 3ada7377..87a2f458 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -130,7 +130,7 @@ nfpms: contents: - src: release/config/config.json dst: /etc/sing-box/config.json - type: config + type: "config|noreplace" - src: release/config/sing-box.service dst: /usr/lib/systemd/system/sing-box.service From 10874d2dc4eba1dcff67cd53cf0619ec25bd582a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 2 Apr 2025 16:46:30 +0800 Subject: [PATCH 16/16] Bump version --- clients/android | 2 +- docs/changelog.md | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/clients/android b/clients/android index 5659088b..8354b78e 160000 --- a/clients/android +++ b/clients/android @@ -1 +1 @@ -Subproject commit 5659088bb3fe18b7095e4b9f868c181e27739617 +Subproject commit 8354b78e5d002d636827cfeed6ed5df8ea057452 diff --git a/docs/changelog.md b/docs/changelog.md index 62933f5e..4e563a81 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,12 @@ icon: material/alert-decagram --- +### 1.11.7 + +* Fixes and improvements + +_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._ + ### 1.11.6 * Fixes and improvements