From a3c2fa6a025de3f728797bed959c99660304cb23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 26 Aug 2024 14:01:32 +0800 Subject: [PATCH 01/31] wireguard: Fix events chan leak --- outbound/wireguard.go | 1 - transport/wireguard/device_stack.go | 8 +-- transport/wireguard/device_system.go | 91 ++++++++++++++-------------- 3 files changed, 49 insertions(+), 51 deletions(-) diff --git a/outbound/wireguard.go b/outbound/wireguard.go index 7805e165a1..d34023653a 100644 --- a/outbound/wireguard.go +++ b/outbound/wireguard.go @@ -180,7 +180,6 @@ func (w *WireGuard) Close() error { if w.pauseCallback != nil { w.pauseManager.UnregisterCallback(w.pauseCallback) } - w.tunDevice.Close() return nil } diff --git a/transport/wireguard/device_stack.go b/transport/wireguard/device_stack.go index 7f57b7c73a..d5770419e2 100644 --- a/transport/wireguard/device_stack.go +++ b/transport/wireguard/device_stack.go @@ -230,17 +230,13 @@ func (w *StackDevice) Events() <-chan wgTun.Event { } func (w *StackDevice) Close() error { - select { - case <-w.done: - return os.ErrClosed - default: - } + close(w.done) + close(w.events) w.stack.Close() for _, endpoint := range w.stack.CleanupEndpoints() { endpoint.Abort() } w.stack.Wait() - close(w.done) return nil } diff --git a/transport/wireguard/device_system.go b/transport/wireguard/device_system.go index 49acc5b90e..2c16c53dfc 100644 --- a/transport/wireguard/device_system.go +++ b/transport/wireguard/device_system.go @@ -6,6 +6,7 @@ import ( "net" "net/netip" "os" + "sync" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" @@ -21,14 +22,16 @@ import ( var _ Device = (*SystemDevice)(nil) type SystemDevice struct { - dialer N.Dialer - device tun.Tun - batchDevice tun.LinuxTUN - name string - mtu int - events chan wgTun.Event - addr4 netip.Addr - addr6 netip.Addr + dialer N.Dialer + device tun.Tun + batchDevice tun.LinuxTUN + name string + mtu uint32 + inet4Addresses []netip.Prefix + inet6Addresses []netip.Prefix + gso bool + events chan wgTun.Event + closeOnce sync.Once } func NewSystemDevice(router adapter.Router, interfaceName string, localPrefixes []netip.Prefix, mtu uint32, gso bool) (*SystemDevice, error) { @@ -44,43 +47,17 @@ func NewSystemDevice(router adapter.Router, interfaceName string, localPrefixes if interfaceName == "" { interfaceName = tun.CalculateInterfaceName("wg") } - tunInterface, err := tun.New(tun.Options{ - Name: interfaceName, - Inet4Address: inet4Addresses, - Inet6Address: inet6Addresses, - MTU: mtu, - GSO: gso, - }) - if err != nil { - return nil, err - } - var inet4Address netip.Addr - var inet6Address netip.Addr - if len(inet4Addresses) > 0 { - inet4Address = inet4Addresses[0].Addr() - } - if len(inet6Addresses) > 0 { - inet6Address = inet6Addresses[0].Addr() - } - var batchDevice tun.LinuxTUN - if gso { - batchTUN, isBatchTUN := tunInterface.(tun.LinuxTUN) - if !isBatchTUN { - return nil, E.New("GSO is not supported on current platform") - } - batchDevice = batchTUN - } + return &SystemDevice{ dialer: common.Must1(dialer.NewDefault(router, option.DialerOptions{ BindInterface: interfaceName, })), - device: tunInterface, - batchDevice: batchDevice, - name: interfaceName, - mtu: int(mtu), - events: make(chan wgTun.Event), - addr4: inet4Address, - addr6: inet6Address, + name: interfaceName, + mtu: mtu, + inet4Addresses: inet4Addresses, + inet6Addresses: inet6Addresses, + gso: gso, + events: make(chan wgTun.Event), }, nil } @@ -93,14 +70,39 @@ func (w *SystemDevice) ListenPacket(ctx context.Context, destination M.Socksaddr } func (w *SystemDevice) Inet4Address() netip.Addr { - return w.addr4 + if len(w.inet4Addresses) == 0 { + return netip.Addr{} + } + return w.inet4Addresses[0].Addr() } func (w *SystemDevice) Inet6Address() netip.Addr { - return w.addr6 + if len(w.inet6Addresses) == 0 { + return netip.Addr{} + } + return w.inet6Addresses[0].Addr() } func (w *SystemDevice) Start() error { + tunInterface, err := tun.New(tun.Options{ + Name: w.name, + Inet4Address: w.inet4Addresses, + Inet6Address: w.inet6Addresses, + MTU: w.mtu, + GSO: w.gso, + }) + if err != nil { + return err + } + w.device = tunInterface + if w.gso { + batchTUN, isBatchTUN := tunInterface.(tun.LinuxTUN) + if !isBatchTUN { + tunInterface.Close() + return E.New("GSO is not supported on current platform") + } + w.batchDevice = batchTUN + } w.events <- wgTun.EventUp return nil } @@ -143,7 +145,7 @@ func (w *SystemDevice) Flush() error { } func (w *SystemDevice) MTU() (int, error) { - return w.mtu, nil + return int(w.mtu), nil } func (w *SystemDevice) Name() (string, error) { @@ -155,6 +157,7 @@ func (w *SystemDevice) Events() <-chan wgTun.Event { } func (w *SystemDevice) Close() error { + close(w.events) return w.device.Close() } From 5a184b8dd9130cbd5c954e6fe760620823c423c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 20 Aug 2024 21:33:31 +0800 Subject: [PATCH 02/31] Update workflow to go1.23 --- .github/workflows/debug.yml | 23 +++++++++++++++++++++-- .github/workflows/docker.yml | 1 + .github/workflows/lint.yml | 2 +- .github/workflows/linux.yml | 2 +- Dockerfile | 4 ++-- 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/workflows/debug.yml b/.github/workflows/debug.yml index 003caf5564..0b114b3d8b 100644 --- a/.github/workflows/debug.yml +++ b/.github/workflows/debug.yml @@ -28,8 +28,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ^1.22 - continue-on-error: true + go-version: ^1.23 - name: Run Test run: | go test -v ./... @@ -93,6 +92,26 @@ jobs: key: go121-${{ hashFiles('**/go.sum') }} - name: Run Test run: make ci_build + build_go122: + name: Debug build (Go 1.22) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + with: + fetch-depth: 0 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ~1.22 + - name: Cache go module + uses: actions/cache@v4 + with: + path: | + ~/go/pkg/mod + key: go122-${{ hashFiles('**/go.sum') }} + - name: Run Test + run: make ci_build cross: strategy: matrix: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8e75954d22..9eb21d3281 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -4,6 +4,7 @@ on: release: types: - released + - prereleased workflow_dispatch: inputs: tag: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3eb31561a3..08fb6c54d6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -28,7 +28,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ^1.22 + go-version: ^1.23 - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 1e6aeff363..bd2f1023f2 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -16,7 +16,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ^1.22 + go-version: ^1.23 - name: Extract signing key run: |- mkdir -p $HOME/.gnupg diff --git a/Dockerfile b/Dockerfile index db890fd439..af121d40b1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS builder +FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder LABEL maintainer="nekohasekai " COPY . /go/src/github.com/sagernet/sing-box WORKDIR /go/src/github.com/sagernet/sing-box @@ -15,7 +15,7 @@ RUN set -ex \ && go build -v -trimpath -tags \ "with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \ -o /go/bin/sing-box \ - -ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \ + -ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid= -checklinkname=0" \ ./cmd/sing-box FROM --platform=$TARGETPLATFORM alpine AS dist LABEL maintainer="nekohasekai " From ecdaac170489e52f08ba4ded03054304160f59af Mon Sep 17 00:00:00 2001 From: iosmanthus Date: Mon, 27 May 2024 19:24:41 +0800 Subject: [PATCH 03/31] Introduce bittorrent related protocol sniffers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Introduce bittorrent related protocol sniffers including, sniffers of 1. BitTorrent Protocol (TCP) 2. uTorrent Transport Protocol (UDP) Signed-off-by: iosmanthus Co-authored-by: 世界 --- common/sniff/bittorrent.go | 101 +++++++++++++++++++++++++++ common/sniff/bittorrent_test.go | 70 +++++++++++++++++++ constant/protocol.go | 11 +-- docs/configuration/route/sniff.md | 15 ++-- docs/configuration/route/sniff.zh.md | 15 ++-- route/router.go | 21 +++++- 6 files changed, 212 insertions(+), 21 deletions(-) create mode 100644 common/sniff/bittorrent.go create mode 100644 common/sniff/bittorrent_test.go diff --git a/common/sniff/bittorrent.go b/common/sniff/bittorrent.go new file mode 100644 index 0000000000..debc35ca4f --- /dev/null +++ b/common/sniff/bittorrent.go @@ -0,0 +1,101 @@ +package sniff + +import ( + "bytes" + "context" + "encoding/binary" + "io" + "os" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" +) + +const ( + trackerConnectFlag = 0 + trackerProtocolID = 0x41727101980 + trackerConnectMinSize = 16 +) + +// BitTorrent detects if the stream is a BitTorrent connection. +// For the BitTorrent protocol specification, see https://www.bittorrent.org/beps/bep_0003.html +func BitTorrent(_ context.Context, reader io.Reader) (*adapter.InboundContext, error) { + var first byte + err := binary.Read(reader, binary.BigEndian, &first) + if err != nil { + return nil, err + } + + if first != 19 { + return nil, os.ErrInvalid + } + + var protocol [19]byte + _, err = reader.Read(protocol[:]) + if err != nil { + return nil, err + } + if string(protocol[:]) != "BitTorrent protocol" { + return nil, os.ErrInvalid + } + + return &adapter.InboundContext{ + Protocol: C.ProtocolBitTorrent, + }, nil +} + +// UTP detects if the packet is a uTP connection packet. +// For the uTP protocol specification, see +// 1. https://www.bittorrent.org/beps/bep_0029.html +// 2. https://github.com/bittorrent/libutp/blob/2b364cbb0650bdab64a5de2abb4518f9f228ec44/utp_internal.cpp#L112 +func UTP(_ context.Context, packet []byte) (*adapter.InboundContext, error) { + // A valid uTP packet must be at least 20 bytes long. + if len(packet) < 20 { + return nil, os.ErrInvalid + } + + version := packet[0] & 0x0F + ty := packet[0] >> 4 + if version != 1 || ty > 4 { + return nil, os.ErrInvalid + } + + // Validate the extensions + extension := packet[1] + reader := bytes.NewReader(packet[20:]) + for extension != 0 { + err := binary.Read(reader, binary.BigEndian, &extension) + if err != nil { + return nil, err + } + + var length byte + err = binary.Read(reader, binary.BigEndian, &length) + if err != nil { + return nil, err + } + _, err = reader.Seek(int64(length), io.SeekCurrent) + if err != nil { + return nil, err + } + } + + return &adapter.InboundContext{ + Protocol: C.ProtocolBitTorrent, + }, nil +} + +// UDPTracker detects if the packet is a UDP Tracker Protocol packet. +// For the UDP Tracker Protocol specification, see https://www.bittorrent.org/beps/bep_0015.html +func UDPTracker(_ context.Context, packet []byte) (*adapter.InboundContext, error) { + if len(packet) < trackerConnectMinSize { + return nil, os.ErrInvalid + } + if binary.BigEndian.Uint64(packet[:8]) != trackerProtocolID { + return nil, os.ErrInvalid + } + if binary.BigEndian.Uint32(packet[8:12]) != trackerConnectFlag { + return nil, os.ErrInvalid + } + return &adapter.InboundContext{Protocol: C.ProtocolBitTorrent}, nil +} diff --git a/common/sniff/bittorrent_test.go b/common/sniff/bittorrent_test.go new file mode 100644 index 0000000000..6b3ab64e95 --- /dev/null +++ b/common/sniff/bittorrent_test.go @@ -0,0 +1,70 @@ +package sniff_test + +import ( + "bytes" + "context" + "encoding/hex" + "testing" + + "github.com/sagernet/sing-box/common/sniff" + C "github.com/sagernet/sing-box/constant" + + "github.com/stretchr/testify/require" +) + +func TestSniffBittorrent(t *testing.T) { + t.Parallel() + + packets := []string{ + "13426974546f7272656e742070726f746f636f6c0000000000100000e21ea9569b69bab33c97851d0298bdfa89bc90922d5554313631302dea812fcd6a3563e3be40c1d1", + "13426974546f7272656e742070726f746f636f6c00000000001000052aa4f5a7e209e54b32803d43670971c4c8caaa052d5452333030302d653369733079647675763638", + "13426974546f7272656e742070726f746f636f6c00000000001000052aa4f5a7e209e54b32803d43670971c4c8caaa052d5452343035302d6f7a316c6e79377931716130", + } + + for _, pkt := range packets { + pkt, err := hex.DecodeString(pkt) + require.NoError(t, err) + metadata, err := sniff.BitTorrent(context.TODO(), bytes.NewReader(pkt)) + require.NoError(t, err) + require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol) + } +} + +func TestSniffUTP(t *testing.T) { + t.Parallel() + + packets := []string{ + "010041a282d7ee7b583afb160004000006d8318da776968f92d666f7963f32dae23ba0d2c810d8b8209cc4939f54fde9eeaa521c2c20c9ba7f43f4fb0375f28de06643b5e3ca4685ab7ac76adca99783be72ef05ed59ef4234f5712b75b4c7c0d7bee8fe2ca20ad626ba5bb0ffcc16bf06790896f888048cf72716419a07db1a3dca4550fbcea75b53e97235168a221cf3e553dfbb723961bd719fab038d86e0ecb74747f5a2cd669de1c4b9ad375f3a492d09d98cdfad745435625401315bbba98d35d32086299801377b93495a63a9efddb8d05f5b37a5c5b1c0a25e917f12007bb5e05013ada8aff544fab8cadf61d80ddb0b60f12741e44515a109d144fd53ef845acb4b5ccf0d6fc302d7003d76df3fc3423bb0237301c9e88f900c2d392a8e0fdb36d143cf7527a93fd0a2638b746e72f6699fffcd4fd15348fce780d4caa04382fd9faf1ca0ae377ca805da7536662b84f5ee18dd3ae38fcb095a7543e55f9069ae92c8cf54ae44e97b558d35e2545c66601ed2149cbc32bd6df199a2be7cf0da8b2ff137e0d23e776bc87248425013876d3a3cc31a83b424b752bd0346437f24b532978005d8f5b1b0be1a37a2489c32a18a9ad3118e3f9d30eb299bffae18e1f0677c2a5c185e62519093fe6bc2b7339299ea50a587989f726ca6443a75dd5bb936f6367c6355d80fae53ff529d740b2e5576e3eefdf1fdbfc69c3c8d8ac750512635de63e054bee1d3b689bc1b2bc3d2601e42a00b5c89066d173d4ae7ffedfd2274e5cf6d868fbe640aedb69b8246142f00b32d459974287537ddd5373460dcbc92f5cfdd7a3ed6020822ae922d947893752ca1983d0d32977374c384ac8f5ab566859019b7351526b9f13e932037a55bb052d9deb3b3c23317e0784fdc51a64f2159bfea3b069cf5caf02ee2c3c1a6b6b427bb16165713e8802d95b5c8ed77953690e994bd38c9ae113fedaf6ee7fc2b96c032ceafc2a530ad0422e84546b9c6ad8ef6ea02fa508abddd1805c38a7b42e9b7c971b1b636865ebec06ed754bb404cd6b4e6cc8cb77bd4a0c43410d5cd5ef8fe853a66d49b3b9e06cb141236cdbfdd5761601dc54d1250b86c660e0f898fe62526fdd9acf0eab60a3bbbb2151970461f28f10b31689594bea646c4b03ee197d63bdef4e5a7c22716b3bb9494a83b78ecd81b338b80ac6c09c43485b1b09ba41c74343832c78f0520c1d659ac9eb1502094141e82fb9e5e620970ebc0655514c43c294a7714cbf9a499d277daf089f556398a01589a77494bec8bfb60a108f3813b55368672b88c1af40f6b3c8b513f7c70c3e0efce85228b8b9ec67ba0393f9f7305024d8e2da6a26cf85613d14f249170ce1000089df4c9c260df7f8292aa2ecb5d5bac97656d59aa248caedea2d198e51ce87baece338716d114b458de02d65c9ff808ca5b5b73723b4d1e962d9ac2d98176544dc9984cf8554d07820ef3dd0861cfe57b478328046380de589adad94ee44743ffac73bb7361feca5d56f07cf8ce75080e261282ae30350d7882679b15cab9e7e53ddf93310b33f7390ae5d318bb53f387e6af5d0ef4f947fc9cb8e7e38b52c7f8d772ece6156b38d88796ea19df02c53723b44df7c76315a0de9462f27287e682d2b4cda1a68fe00d7e48c51ee981be44e1ca940fb5190c12655edb4a83c3a4f33e48a015692df4f0b3d61656e362aca657b5ae8c12db5a0db3db1e45135ee918b66918f40e53c4f83e9da0cddfe63f736ae751ab3837a30ae3220d8e8e311487093a7b90c7e7e40dd54ca750e19452f9193aa892aa6a6229ab493dadae988b1724f7898ee69c36d3eb7364c4adbeca811cfe2065873e78c2b6dfdf1595f7a7831c07e03cda82e4f86f76438dfb2b07c13638ce7b509cfa71b88b5102b39a203b423202088e1c2103319cb32c13c1e546ff8612fa194c95a7808ab767c265a1bd5fa0efed5c8ec1701876a00ec8", + "01001ecb68176f215d04326300100000dbcf30292d14b54e9ee2d115ee5b8ebc7fad3e882d4fcdd0c14c6b917c11cb4c6a9f410b52a33ae97c2ac77c7a2b122b8955e09af3c5c595f1b2e79ca57cfe44c44e069610773b9bc9ba223d7f6b383e3adddd03fb88a8476028e30979c2ef321ffc97c5c132bcf9ac5b410bbb5ec6cefca3c7209202a14c5ae922b6b157b0a80249d13ffe5b996af0bc8e54ba576d148372494303e7ead0602b05b9c8fc97d48508a028a04d63a1fd28b0edfcd5c51715f63188b53eefede98a76912dca98518551a8856567307a56a702cbfcc115ea0c755b418bc2c7b57721239b82f09fb24328a4b0ce0f109bcb2a64e04b8aadb1f8487585425acdf8fc4ec8ea93cfcec5ac098bb29d42ddef6e46b03f34a5de28316726699b7cb5195c33e5c48abe87d591d63f9991c84c30819d186d6e0e95fd83c8dff07aa669c4430989bcaccfeacb9bcadbdb4d8f1964dbeb9687745656edd30b21c66cc0a1d742a78717d134a19a7f02d285a4973b1a198c00cfdff4676608dc4f3e817e3463c3b4e2c80d3e8d4fbac541a58a2fb7ad6939f607f8144eff6c8b0adc28ee5609ea158987519892fb", + "21001ecb6817f2805d044fd700100000dbd03029", + "410277ef0b1fb1f60000000000040000c233000000080000000000000000", + } + + for _, pkt := range packets { + pkt, err := hex.DecodeString(pkt) + require.NoError(t, err) + + metadata, err := sniff.UTP(context.TODO(), pkt) + require.NoError(t, err) + require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol) + } +} + +func TestSniffUDPTracker(t *testing.T) { + t.Parallel() + + connectPackets := []string{ + "00000417271019800000000078e90560", + "00000417271019800000000022c5d64d", + "000004172710198000000000b3863541", + } + + for _, pkt := range connectPackets { + pkt, err := hex.DecodeString(pkt) + require.NoError(t, err) + + metadata, err := sniff.UDPTracker(context.TODO(), pkt) + require.NoError(t, err) + require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol) + } +} diff --git a/constant/protocol.go b/constant/protocol.go index 810c79ec99..2b7a9e0f50 100644 --- a/constant/protocol.go +++ b/constant/protocol.go @@ -1,9 +1,10 @@ package constant const ( - ProtocolTLS = "tls" - ProtocolHTTP = "http" - ProtocolQUIC = "quic" - ProtocolDNS = "dns" - ProtocolSTUN = "stun" + ProtocolTLS = "tls" + ProtocolHTTP = "http" + ProtocolQUIC = "quic" + ProtocolDNS = "dns" + ProtocolSTUN = "stun" + ProtocolBitTorrent = "bittorrent" ) diff --git a/docs/configuration/route/sniff.md b/docs/configuration/route/sniff.md index 2ba2c25135..7a3de02bc6 100644 --- a/docs/configuration/route/sniff.md +++ b/docs/configuration/route/sniff.md @@ -2,10 +2,11 @@ If enabled in the inbound, the protocol and domain name (if present) of by the c #### Supported Protocols -| Network | Protocol | Domain Name | -|:-------:|:--------:|:-----------:| -| TCP | HTTP | Host | -| TCP | TLS | Server Name | -| UDP | QUIC | Server Name | -| UDP | STUN | / | -| TCP/UDP | DNS | / | \ No newline at end of file +| Network | Protocol | Domain Name | +|:-------:|:-----------:|:-----------:| +| TCP | HTTP | Host | +| TCP | TLS | Server Name | +| UDP | QUIC | Server Name | +| UDP | STUN | / | +| TCP/UDP | DNS | / | +| TCP/UDP | BitTorrent | / | \ No newline at end of file diff --git a/docs/configuration/route/sniff.zh.md b/docs/configuration/route/sniff.zh.md index c3cdcc4ee3..553c6ed76b 100644 --- a/docs/configuration/route/sniff.zh.md +++ b/docs/configuration/route/sniff.zh.md @@ -2,10 +2,11 @@ #### 支持的协议 -| 网络 | 协议 | 域名 | -|:-------:|:----:|:-----------:| -| TCP | HTTP | Host | -| TCP | TLS | Server Name | -| UDP | QUIC | Server Name | -| UDP | STUN | / | -| TCP/UDP | DNS | / | \ No newline at end of file +| 网络 | 协议 | 域名 | +|:-------:|:-----------:|:-----------:| +| TCP | HTTP | Host | +| TCP | TLS | Server Name | +| UDP | QUIC | Server Name | +| UDP | STUN | / | +| TCP/UDP | DNS | / | +| TCP/UDP | BitTorrent | / | \ No newline at end of file diff --git a/route/router.go b/route/router.go index 0c13b1986b..22788121e4 100644 --- a/route/router.go +++ b/route/router.go @@ -834,7 +834,16 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad if metadata.InboundOptions.SniffEnabled { buffer := buf.NewPacket() - sniffMetadata, err := sniff.PeekStream(ctx, conn, buffer, time.Duration(metadata.InboundOptions.SniffTimeout), sniff.StreamDomainNameQuery, sniff.TLSClientHello, sniff.HTTPHost) + sniffMetadata, err := sniff.PeekStream( + ctx, + conn, + buffer, + time.Duration(metadata.InboundOptions.SniffTimeout), + sniff.StreamDomainNameQuery, + sniff.TLSClientHello, + sniff.HTTPHost, + sniff.BitTorrent, + ) if sniffMetadata != nil { metadata.Protocol = sniffMetadata.Protocol metadata.Domain = sniffMetadata.Domain @@ -983,7 +992,15 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m metadata.Destination = destination } if metadata.InboundOptions.SniffEnabled { - sniffMetadata, _ := sniff.PeekPacket(ctx, buffer.Bytes(), sniff.DomainNameQuery, sniff.QUICClientHello, sniff.STUNMessage) + sniffMetadata, _ := sniff.PeekPacket( + ctx, + buffer.Bytes(), + sniff.DomainNameQuery, + sniff.QUICClientHello, + sniff.STUNMessage, + sniff.UTP, + sniff.UDPTracker, + ) if sniffMetadata != nil { metadata.Protocol = sniffMetadata.Protocol metadata.Domain = sniffMetadata.Domain From f4cd036e396e20f976e66c3a5a1b312887a08cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 7 Jul 2024 15:45:50 +0800 Subject: [PATCH 04/31] Introduce DTLS sniffer --- common/sniff/dtls.go | 31 ++++++++++++++++++++++++++++ common/sniff/dtls_test.go | 30 +++++++++++++++++++++++++++ constant/protocol.go | 1 + docs/configuration/route/sniff.md | 26 ++++++++++++++++------- docs/configuration/route/sniff.zh.md | 26 ++++++++++++++++------- route/router.go | 1 + 6 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 common/sniff/dtls.go create mode 100644 common/sniff/dtls_test.go diff --git a/common/sniff/dtls.go b/common/sniff/dtls.go new file mode 100644 index 0000000000..6469b09709 --- /dev/null +++ b/common/sniff/dtls.go @@ -0,0 +1,31 @@ +package sniff + +import ( + "context" + "os" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" +) + +func DTLSRecord(ctx context.Context, packet []byte) (*adapter.InboundContext, error) { + const fixedHeaderSize = 13 + if len(packet) < fixedHeaderSize { + return nil, os.ErrInvalid + } + contentType := packet[0] + switch contentType { + case 20, 21, 22, 23, 25: + default: + return nil, os.ErrInvalid + } + versionMajor := packet[1] + if versionMajor != 0xfe { + return nil, os.ErrInvalid + } + versionMinor := packet[2] + if versionMinor != 0xff && versionMinor != 0xfd { + return nil, os.ErrInvalid + } + return &adapter.InboundContext{Protocol: C.ProtocolDTLS}, nil +} diff --git a/common/sniff/dtls_test.go b/common/sniff/dtls_test.go new file mode 100644 index 0000000000..45f7712642 --- /dev/null +++ b/common/sniff/dtls_test.go @@ -0,0 +1,30 @@ +package sniff_test + +import ( + "context" + "encoding/hex" + "testing" + + "github.com/sagernet/sing-box/common/sniff" + C "github.com/sagernet/sing-box/constant" + + "github.com/stretchr/testify/require" +) + +func TestSniffDTLSClientHello(t *testing.T) { + t.Parallel() + packet, err := hex.DecodeString("16fefd0000000000000000007e010000720000000000000072fefd668a43523798e064bd806d0c87660de9c611a59bbdfc3892c4e072d94f2cafc40000000cc02bc02fc00ac014c02cc0300100003c000d0010000e0403050306030401050106010807ff01000100000a00080006001d00170018000b00020100000e000900060008000700010000170000") + require.NoError(t, err) + metadata, err := sniff.DTLSRecord(context.Background(), packet) + require.NoError(t, err) + require.Equal(t, metadata.Protocol, C.ProtocolDTLS) +} + +func TestSniffDTLSClientApplicationData(t *testing.T) { + t.Parallel() + packet, err := hex.DecodeString("17fefd000100000000000100440001000000000001a4f682b77ecadd10f3f3a2f78d90566212366ff8209fd77314f5a49352f9bb9bd12f4daba0b4736ae29e46b9714d3b424b3e6d0234736619b5aa0d3f") + require.NoError(t, err) + metadata, err := sniff.DTLSRecord(context.Background(), packet) + require.NoError(t, err) + require.Equal(t, metadata.Protocol, C.ProtocolDTLS) +} diff --git a/constant/protocol.go b/constant/protocol.go index 2b7a9e0f50..915e64ca19 100644 --- a/constant/protocol.go +++ b/constant/protocol.go @@ -7,4 +7,5 @@ const ( ProtocolDNS = "dns" ProtocolSTUN = "stun" ProtocolBitTorrent = "bittorrent" + ProtocolDTLS = "dtls" ) diff --git a/docs/configuration/route/sniff.md b/docs/configuration/route/sniff.md index 7a3de02bc6..cc9a5c130d 100644 --- a/docs/configuration/route/sniff.md +++ b/docs/configuration/route/sniff.md @@ -1,12 +1,22 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.10.0" + + :material-plus: BitTorrent support + :material-plus: DTLS support + If enabled in the inbound, the protocol and domain name (if present) of by the connection can be sniffed. #### Supported Protocols -| Network | Protocol | Domain Name | -|:-------:|:-----------:|:-----------:| -| TCP | HTTP | Host | -| TCP | TLS | Server Name | -| UDP | QUIC | Server Name | -| UDP | STUN | / | -| TCP/UDP | DNS | / | -| TCP/UDP | BitTorrent | / | \ No newline at end of file +| Network | Protocol | Domain Name | +|:-------:|:------------:|:-----------:| +| TCP | `http` | Host | +| TCP | `tls` | Server Name | +| UDP | `quic` | Server Name | +| UDP | `stun` | / | +| TCP/UDP | `dns` | / | +| TCP/UDP | `bittorrent` | / | +| UDP | `dtls` | / | diff --git a/docs/configuration/route/sniff.zh.md b/docs/configuration/route/sniff.zh.md index 553c6ed76b..523ed44b90 100644 --- a/docs/configuration/route/sniff.zh.md +++ b/docs/configuration/route/sniff.zh.md @@ -1,12 +1,22 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.10.0 中的更改" + + :material-plus: BitTorrent 支持 + :material-plus: DTLS 支持 + 如果在入站中启用,则可以嗅探连接的协议和域名(如果存在)。 #### 支持的协议 -| 网络 | 协议 | 域名 | -|:-------:|:-----------:|:-----------:| -| TCP | HTTP | Host | -| TCP | TLS | Server Name | -| UDP | QUIC | Server Name | -| UDP | STUN | / | -| TCP/UDP | DNS | / | -| TCP/UDP | BitTorrent | / | \ No newline at end of file +| 网络 | 协议 | 域名 | +|:-------:|:------------:|:-----------:| +| TCP | `http` | Host | +| TCP | `tls` | Server Name | +| UDP | `quic` | Server Name | +| UDP | `stun` | / | +| TCP/UDP | `dns` | / | +| TCP/UDP | `bittorrent` | / | +| UDP | `dtls` | / | diff --git a/route/router.go b/route/router.go index 22788121e4..8b51b8593c 100644 --- a/route/router.go +++ b/route/router.go @@ -1000,6 +1000,7 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m sniff.STUNMessage, sniff.UTP, sniff.UDPTracker, + sniff.DTLSRecord, ) if sniffMetadata != nil { metadata.Protocol = sniffMetadata.Protocol From 10d33ee8357658c3ce294e1482a7351f6490c5ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 7 Jun 2024 16:08:07 +0800 Subject: [PATCH 05/31] Drop support for go1.18 and go1.19 --- .github/workflows/debug.yml | 20 -------------------- Makefile | 9 ++------- debug_go119.go => debug.go | 2 -- debug_go118.go | 36 ------------------------------------ docs/deprecated.md | 6 ++++++ docs/deprecated.zh.md | 6 ++++++ 6 files changed, 14 insertions(+), 65 deletions(-) rename debug_go119.go => debug.go (97%) delete mode 100644 debug_go118.go diff --git a/.github/workflows/debug.yml b/.github/workflows/debug.yml index 0b114b3d8b..2bca96e050 100644 --- a/.github/workflows/debug.yml +++ b/.github/workflows/debug.yml @@ -32,26 +32,6 @@ jobs: - name: Run Test run: | go test -v ./... - build_go118: - name: Debug build (Go 1.18) - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 - with: - fetch-depth: 0 - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: ~1.18 - - name: Cache go module - uses: actions/cache@v4 - with: - path: | - ~/go/pkg/mod - key: go118-${{ hashFiles('**/go.sum') }} - - name: Run Test - run: make ci_build_go118 build_go120: name: Debug build (Go 1.20) runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index f6815470ae..78721f1d18 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,6 @@ NAME = sing-box COMMIT = $(shell git rev-parse --short HEAD) -TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api -TAGS_GO120 = with_quic,with_utls +TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls TAGS_GO121 = with_ech TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121) TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server @@ -20,13 +19,9 @@ PREFIX ?= $(shell go env GOPATH) build: go build $(MAIN_PARAMS) $(MAIN) -ci_build_go118: - go build $(PARAMS) $(MAIN) - go build $(PARAMS) -tags "$(TAGS_GO118)" $(MAIN) - ci_build_go120: go build $(PARAMS) $(MAIN) - go build $(PARAMS) -tags "$(TAGS_GO118),$(TAGS_GO120)" $(MAIN) + go build $(PARAMS) -tags "$(TAGS_GO120)" $(MAIN) ci_build: go build $(PARAMS) $(MAIN) diff --git a/debug_go119.go b/debug.go similarity index 97% rename from debug_go119.go rename to debug.go index cf522afb72..2fa962d642 100644 --- a/debug_go119.go +++ b/debug.go @@ -1,5 +1,3 @@ -//go:build go1.19 - package box import ( diff --git a/debug_go118.go b/debug_go118.go deleted file mode 100644 index bb132efb98..0000000000 --- a/debug_go118.go +++ /dev/null @@ -1,36 +0,0 @@ -//go:build !go1.19 - -package box - -import ( - "runtime/debug" - - "github.com/sagernet/sing-box/common/conntrack" - "github.com/sagernet/sing-box/option" -) - -func applyDebugOptions(options option.DebugOptions) { - applyDebugListenOption(options) - if options.GCPercent != nil { - debug.SetGCPercent(*options.GCPercent) - } - if options.MaxStack != nil { - debug.SetMaxStack(*options.MaxStack) - } - if options.MaxThreads != nil { - debug.SetMaxThreads(*options.MaxThreads) - } - if options.PanicOnFault != nil { - debug.SetPanicOnFault(*options.PanicOnFault) - } - if options.TraceBack != "" { - debug.SetTraceback(options.TraceBack) - } - if options.MemoryLimit != 0 { - // debug.SetMemoryLimit(int64(options.MemoryLimit)) - conntrack.MemoryLimit = uint64(options.MemoryLimit) - } - if options.OOMKiller != nil { - conntrack.KillerEnabled = *options.OOMKiller - } -} diff --git a/docs/deprecated.md b/docs/deprecated.md index 270aaaea72..439bf7e8f5 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -4,6 +4,12 @@ icon: material/delete-alert # Deprecated Feature List +## 1.10.0 + +#### Drop support for go1.18 and go1.19 + +Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile. + ## 1.8.0 #### Cache file and related features in Clash API diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index 69ec4bdd7f..76e7e7684c 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -4,6 +4,12 @@ icon: material/delete-alert # 废弃功能列表 +## 1.10.0 + +#### 移除对 go1.18 和 go1.19 的支持 + +由于维护困难,sing-box 1.10.0 要求至少 Go 1.20 才能编译。 + ## 1.8.0 #### Clash API 中的 Cache file 及相关功能 From e413f849b32c41895f75e2f6cdf50c10e0e3a711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 11 Jun 2024 21:16:33 +0800 Subject: [PATCH 06/31] platform: Prepare connections list --- box.go | 1 + constant/proxy.go | 8 + experimental/clashapi/connections.go | 5 +- experimental/clashapi/server.go | 48 +--- .../clashapi/trafficontrol/manager.go | 91 ++++-- .../clashapi/trafficontrol/tracker.go | 249 +++++++++------- experimental/libbox/command.go | 2 + experimental/libbox/command_client.go | 8 + .../libbox/command_close_connection.go | 53 ++++ experimental/libbox/command_connections.go | 268 ++++++++++++++++++ experimental/libbox/command_group.go | 60 ++-- experimental/libbox/command_server.go | 6 + experimental/libbox/command_status.go | 2 +- experimental/libbox/iterator.go | 4 + experimental/libbox/service.go | 54 ++-- experimental/libbox/setup.go | 6 + inbound/builder.go | 34 +-- log/format.go | 6 +- 18 files changed, 651 insertions(+), 254 deletions(-) create mode 100644 experimental/libbox/command_close_connection.go create mode 100644 experimental/libbox/command_connections.go diff --git a/box.go b/box.go index 70235fd344..3c514cfee2 100644 --- a/box.go +++ b/box.go @@ -111,6 +111,7 @@ func New(options Options) (*Box, error) { ctx, router, logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")), + tag, inboundOptions, options.PlatformInterface, ) diff --git a/constant/proxy.go b/constant/proxy.go index 1e9baee298..3197de6052 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -32,6 +32,12 @@ const ( func ProxyDisplayName(proxyType string) string { switch proxyType { + case TypeTun: + return "TUN" + case TypeRedirect: + return "Redirect" + case TypeTProxy: + return "TProxy" case TypeDirect: return "Direct" case TypeBlock: @@ -42,6 +48,8 @@ func ProxyDisplayName(proxyType string) string { return "SOCKS" case TypeHTTP: return "HTTP" + case TypeMixed: + return "Mixed" case TypeShadowsocks: return "Shadowsocks" case TypeVMess: diff --git a/experimental/clashapi/connections.go b/experimental/clashapi/connections.go index c9471207e8..999d589828 100644 --- a/experimental/clashapi/connections.go +++ b/experimental/clashapi/connections.go @@ -14,6 +14,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/render" + "github.com/gofrs/uuid/v5" ) func connectionRouter(router adapter.Router, trafficManager *trafficontrol.Manager) http.Handler { @@ -76,10 +77,10 @@ func getConnections(trafficManager *trafficontrol.Manager) func(w http.ResponseW func closeConnection(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") + id := uuid.FromStringOrNil(chi.URLParam(r, "id")) snapshot := trafficManager.Snapshot() for _, c := range snapshot.Connections { - if id == c.ID() { + if id == c.Metadata().ID { c.Close() break } diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 1eec8448af..a1152baaf7 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -19,7 +19,6 @@ import ( "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" - F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" @@ -218,58 +217,15 @@ func (s *Server) TrafficManager() *trafficontrol.Manager { } func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule) (net.Conn, adapter.Tracker) { - tracker := trafficontrol.NewTCPTracker(conn, s.trafficManager, castMetadata(metadata), s.router, matchedRule) + tracker := trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.router, matchedRule) return tracker, tracker } func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule) (N.PacketConn, adapter.Tracker) { - tracker := trafficontrol.NewUDPTracker(conn, s.trafficManager, castMetadata(metadata), s.router, matchedRule) + tracker := trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.router, matchedRule) return tracker, tracker } -func castMetadata(metadata adapter.InboundContext) trafficontrol.Metadata { - var inbound string - if metadata.Inbound != "" { - inbound = metadata.InboundType + "/" + metadata.Inbound - } else { - inbound = metadata.InboundType - } - var domain string - if metadata.Domain != "" { - domain = metadata.Domain - } else { - domain = metadata.Destination.Fqdn - } - var processPath string - if metadata.ProcessInfo != nil { - if metadata.ProcessInfo.ProcessPath != "" { - processPath = metadata.ProcessInfo.ProcessPath - } else if metadata.ProcessInfo.PackageName != "" { - processPath = metadata.ProcessInfo.PackageName - } - if processPath == "" { - if metadata.ProcessInfo.UserId != -1 { - processPath = F.ToString(metadata.ProcessInfo.UserId) - } - } else if metadata.ProcessInfo.User != "" { - processPath = F.ToString(processPath, " (", metadata.ProcessInfo.User, ")") - } else if metadata.ProcessInfo.UserId != -1 { - processPath = F.ToString(processPath, " (", metadata.ProcessInfo.UserId, ")") - } - } - return trafficontrol.Metadata{ - NetWork: metadata.Network, - Type: inbound, - SrcIP: metadata.Source.Addr, - DstIP: metadata.Destination.Addr, - SrcPort: F.ToString(metadata.Source.Port), - DstPort: F.ToString(metadata.Destination.Port), - Host: domain, - DNSMode: "normal", - ProcessPath: processPath, - } -} - func authentication(serverSecret string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { diff --git a/experimental/clashapi/trafficontrol/manager.go b/experimental/clashapi/trafficontrol/manager.go index eac7aee4d8..9b22f1e3d9 100644 --- a/experimental/clashapi/trafficontrol/manager.go +++ b/experimental/clashapi/trafficontrol/manager.go @@ -2,10 +2,17 @@ package trafficontrol import ( "runtime" + "sync" "time" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/clashapi/compatible" + "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/atomic" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/x/list" + + "github.com/gofrs/uuid/v5" ) type Manager struct { @@ -16,9 +23,11 @@ type Manager struct { uploadTotal atomic.Int64 downloadTotal atomic.Int64 - connections compatible.Map[string, tracker] - ticker *time.Ticker - done chan struct{} + connections compatible.Map[uuid.UUID, Tracker] + closedConnectionsAccess sync.Mutex + closedConnections list.List[TrackerMetadata] + ticker *time.Ticker + done chan struct{} // process *process.Process memory uint64 } @@ -33,12 +42,22 @@ func NewManager() *Manager { return manager } -func (m *Manager) Join(c tracker) { - m.connections.Store(c.ID(), c) +func (m *Manager) Join(c Tracker) { + m.connections.Store(c.Metadata().ID, c) } -func (m *Manager) Leave(c tracker) { - m.connections.Delete(c.ID()) +func (m *Manager) Leave(c Tracker) { + metadata := c.Metadata() + _, loaded := m.connections.LoadAndDelete(metadata.ID) + if loaded { + metadata.ClosedAt = time.Now() + m.closedConnectionsAccess.Lock() + defer m.closedConnectionsAccess.Unlock() + if m.closedConnections.Len() >= 1000 { + m.closedConnections.PopFront() + } + m.closedConnections.PushBack(metadata) + } } func (m *Manager) PushUploaded(size int64) { @@ -59,14 +78,39 @@ func (m *Manager) Total() (up int64, down int64) { return m.uploadTotal.Load(), m.downloadTotal.Load() } -func (m *Manager) Connections() int { +func (m *Manager) ConnectionsLen() int { return m.connections.Len() } +func (m *Manager) Connections() []TrackerMetadata { + var connections []TrackerMetadata + m.connections.Range(func(_ uuid.UUID, value Tracker) bool { + connections = append(connections, value.Metadata()) + return true + }) + return connections +} + +func (m *Manager) ClosedConnections() []TrackerMetadata { + m.closedConnectionsAccess.Lock() + defer m.closedConnectionsAccess.Unlock() + return m.closedConnections.Array() +} + +func (m *Manager) Connection(id uuid.UUID) Tracker { + connection, loaded := m.connections.Load(id) + if !loaded { + return nil + } + return connection +} + func (m *Manager) Snapshot() *Snapshot { - var connections []tracker - m.connections.Range(func(_ string, value tracker) bool { - connections = append(connections, value) + var connections []Tracker + m.connections.Range(func(_ uuid.UUID, value Tracker) bool { + if value.Metadata().OutboundType != C.TypeDNS { + connections = append(connections, value) + } return true }) @@ -75,10 +119,10 @@ func (m *Manager) Snapshot() *Snapshot { m.memory = memStats.StackInuse + memStats.HeapInuse + memStats.HeapIdle - memStats.HeapReleased return &Snapshot{ - UploadTotal: m.uploadTotal.Load(), - DownloadTotal: m.downloadTotal.Load(), - Connections: connections, - Memory: m.memory, + Upload: m.uploadTotal.Load(), + Download: m.downloadTotal.Load(), + Connections: connections, + Memory: m.memory, } } @@ -114,8 +158,17 @@ func (m *Manager) Close() error { } type Snapshot struct { - DownloadTotal int64 `json:"downloadTotal"` - UploadTotal int64 `json:"uploadTotal"` - Connections []tracker `json:"connections"` - Memory uint64 `json:"memory"` + Download int64 + Upload int64 + Connections []Tracker + Memory uint64 +} + +func (s *Snapshot) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]any{ + "downloadTotal": s.Download, + "uploadTotal": s.Upload, + "connections": common.Map(s.Connections, func(t Tracker) TrackerMetadata { return t.Metadata() }), + "memory": s.Memory, + }) } diff --git a/experimental/clashapi/trafficontrol/tracker.go b/experimental/clashapi/trafficontrol/tracker.go index 4e635d1257..73c28e69bd 100644 --- a/experimental/clashapi/trafficontrol/tracker.go +++ b/experimental/clashapi/trafficontrol/tracker.go @@ -2,97 +2,135 @@ package trafficontrol import ( "net" - "net/netip" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/atomic" "github.com/sagernet/sing/common/bufio" + F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" N "github.com/sagernet/sing/common/network" "github.com/gofrs/uuid/v5" ) -type Metadata struct { - NetWork string `json:"network"` - Type string `json:"type"` - SrcIP netip.Addr `json:"sourceIP"` - DstIP netip.Addr `json:"destinationIP"` - SrcPort string `json:"sourcePort"` - DstPort string `json:"destinationPort"` - Host string `json:"host"` - DNSMode string `json:"dnsMode"` - ProcessPath string `json:"processPath"` +type TrackerMetadata struct { + ID uuid.UUID + Metadata adapter.InboundContext + CreatedAt time.Time + ClosedAt time.Time + Upload *atomic.Int64 + Download *atomic.Int64 + Chain []string + Rule adapter.Rule + Outbound string + OutboundType string } -type tracker interface { - ID() string - Close() error - Leave() -} - -type trackerInfo struct { - UUID uuid.UUID `json:"id"` - Metadata Metadata `json:"metadata"` - UploadTotal *atomic.Int64 `json:"upload"` - DownloadTotal *atomic.Int64 `json:"download"` - Start time.Time `json:"start"` - Chain []string `json:"chains"` - Rule string `json:"rule"` - RulePayload string `json:"rulePayload"` -} - -func (t trackerInfo) MarshalJSON() ([]byte, error) { +func (t TrackerMetadata) MarshalJSON() ([]byte, error) { + var inbound string + if t.Metadata.Inbound != "" { + inbound = t.Metadata.InboundType + "/" + t.Metadata.Inbound + } else { + inbound = t.Metadata.InboundType + } + var domain string + if t.Metadata.Domain != "" { + domain = t.Metadata.Domain + } else { + domain = t.Metadata.Destination.Fqdn + } + var processPath string + if t.Metadata.ProcessInfo != nil { + if t.Metadata.ProcessInfo.ProcessPath != "" { + processPath = t.Metadata.ProcessInfo.ProcessPath + } else if t.Metadata.ProcessInfo.PackageName != "" { + processPath = t.Metadata.ProcessInfo.PackageName + } + if processPath == "" { + if t.Metadata.ProcessInfo.UserId != -1 { + processPath = F.ToString(t.Metadata.ProcessInfo.UserId) + } + } else if t.Metadata.ProcessInfo.User != "" { + processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.User, ")") + } else if t.Metadata.ProcessInfo.UserId != -1 { + processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.UserId, ")") + } + } + var rule string + if t.Rule != nil { + rule = F.ToString(t.Rule, " => ", t.Rule.Outbound()) + } else { + rule = "final" + } return json.Marshal(map[string]any{ - "id": t.UUID.String(), - "metadata": t.Metadata, - "upload": t.UploadTotal.Load(), - "download": t.DownloadTotal.Load(), - "start": t.Start, + "id": t.ID, + "metadata": map[string]any{ + "network": t.Metadata.Network, + "type": inbound, + "sourceIP": t.Metadata.Source.Addr, + "destinationIP": t.Metadata.Destination.Addr, + "sourcePort": F.ToString(t.Metadata.Source.Port), + "destinationPort": F.ToString(t.Metadata.Destination.Port), + "host": domain, + "dnsMode": "normal", + "processPath": processPath, + }, + "upload": t.Upload.Load(), + "download": t.Download.Load(), + "start": t.CreatedAt, "chains": t.Chain, - "rule": t.Rule, - "rulePayload": t.RulePayload, + "rule": rule, + "rulePayload": "", }) } -type tcpTracker struct { - N.ExtendedConn `json:"-"` - *trackerInfo - manager *Manager +type Tracker interface { + adapter.Tracker + Metadata() TrackerMetadata + Close() error +} + +type TCPConn struct { + N.ExtendedConn + metadata TrackerMetadata + manager *Manager } -func (tt *tcpTracker) ID() string { - return tt.UUID.String() +func (tt *TCPConn) Metadata() TrackerMetadata { + return tt.metadata } -func (tt *tcpTracker) Close() error { +func (tt *TCPConn) Close() error { tt.manager.Leave(tt) return tt.ExtendedConn.Close() } -func (tt *tcpTracker) Leave() { +func (tt *TCPConn) Leave() { tt.manager.Leave(tt) } -func (tt *tcpTracker) Upstream() any { +func (tt *TCPConn) Upstream() any { return tt.ExtendedConn } -func (tt *tcpTracker) ReaderReplaceable() bool { +func (tt *TCPConn) ReaderReplaceable() bool { return true } -func (tt *tcpTracker) WriterReplaceable() bool { +func (tt *TCPConn) WriterReplaceable() bool { return true } -func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router adapter.Router, rule adapter.Rule) *tcpTracker { - uuid, _ := uuid.NewV4() - - var chain []string - var next string +func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundContext, router adapter.Router, rule adapter.Rule) *TCPConn { + id, _ := uuid.NewV4() + var ( + chain []string + next string + outbound string + outboundType string + ) if rule == nil { if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { next = defaultOutbound.Tag() @@ -106,17 +144,17 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad if !loaded { break } + outbound = detour.Tag() + outboundType = detour.Type() group, isGroup := detour.(adapter.OutboundGroup) if !isGroup { break } next = group.Now() } - upload := new(atomic.Int64) download := new(atomic.Int64) - - t := &tcpTracker{ + tracker := &TCPConn{ ExtendedConn: bufio.NewCounterConn(conn, []N.CountFunc{func(n int64) { upload.Add(n) manager.PushUploaded(n) @@ -124,64 +162,62 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad download.Add(n) manager.PushDownloaded(n) }}), - manager: manager, - trackerInfo: &trackerInfo{ - UUID: uuid, - Start: time.Now(), - Metadata: metadata, - Chain: common.Reverse(chain), - Rule: "", - UploadTotal: upload, - DownloadTotal: download, + metadata: TrackerMetadata{ + ID: id, + Metadata: metadata, + CreatedAt: time.Now(), + Upload: upload, + Download: download, + Chain: common.Reverse(chain), + Rule: rule, + Outbound: outbound, + OutboundType: outboundType, }, + manager: manager, } - - if rule != nil { - t.trackerInfo.Rule = rule.String() + " => " + rule.Outbound() - } else { - t.trackerInfo.Rule = "final" - } - - manager.Join(t) - return t + manager.Join(tracker) + return tracker } -type udpTracker struct { +type UDPConn struct { N.PacketConn `json:"-"` - *trackerInfo - manager *Manager + metadata TrackerMetadata + manager *Manager } -func (ut *udpTracker) ID() string { - return ut.UUID.String() +func (ut *UDPConn) Metadata() TrackerMetadata { + return ut.metadata } -func (ut *udpTracker) Close() error { +func (ut *UDPConn) Close() error { ut.manager.Leave(ut) return ut.PacketConn.Close() } -func (ut *udpTracker) Leave() { +func (ut *UDPConn) Leave() { ut.manager.Leave(ut) } -func (ut *udpTracker) Upstream() any { +func (ut *UDPConn) Upstream() any { return ut.PacketConn } -func (ut *udpTracker) ReaderReplaceable() bool { +func (ut *UDPConn) ReaderReplaceable() bool { return true } -func (ut *udpTracker) WriterReplaceable() bool { +func (ut *UDPConn) WriterReplaceable() bool { return true } -func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, router adapter.Router, rule adapter.Rule) *udpTracker { - uuid, _ := uuid.NewV4() - - var chain []string - var next string +func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.InboundContext, router adapter.Router, rule adapter.Rule) *UDPConn { + id, _ := uuid.NewV4() + var ( + chain []string + next string + outbound string + outboundType string + ) if rule == nil { if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil { next = defaultOutbound.Tag() @@ -195,17 +231,17 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route if !loaded { break } + outbound = detour.Tag() + outboundType = detour.Type() group, isGroup := detour.(adapter.OutboundGroup) if !isGroup { break } next = group.Now() } - upload := new(atomic.Int64) download := new(atomic.Int64) - - ut := &udpTracker{ + trackerConn := &UDPConn{ PacketConn: bufio.NewCounterPacketConn(conn, []N.CountFunc{func(n int64) { upload.Add(n) manager.PushUploaded(n) @@ -213,24 +249,19 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route download.Add(n) manager.PushDownloaded(n) }}), - manager: manager, - trackerInfo: &trackerInfo{ - UUID: uuid, - Start: time.Now(), - Metadata: metadata, - Chain: common.Reverse(chain), - Rule: "", - UploadTotal: upload, - DownloadTotal: download, + metadata: TrackerMetadata{ + ID: id, + Metadata: metadata, + CreatedAt: time.Now(), + Upload: upload, + Download: download, + Chain: common.Reverse(chain), + Rule: rule, + Outbound: outbound, + OutboundType: outboundType, }, + manager: manager, } - - if rule != nil { - ut.trackerInfo.Rule = rule.String() + " => " + rule.Outbound() - } else { - ut.trackerInfo.Rule = "final" - } - - manager.Join(ut) - return ut + manager.Join(trackerConn) + return trackerConn } diff --git a/experimental/libbox/command.go b/experimental/libbox/command.go index 7915419d64..f9aca13ff9 100644 --- a/experimental/libbox/command.go +++ b/experimental/libbox/command.go @@ -14,4 +14,6 @@ const ( CommandSetClashMode CommandGetSystemProxyStatus CommandSetSystemProxyEnabled + CommandConnections + CommandCloseConnection ) diff --git a/experimental/libbox/command_client.go b/experimental/libbox/command_client.go index f3c9ad2a19..199dce0d18 100644 --- a/experimental/libbox/command_client.go +++ b/experimental/libbox/command_client.go @@ -31,6 +31,7 @@ type CommandClientHandler interface { WriteGroups(message OutboundGroupIterator) InitializeClashMode(modeList StringIterator, currentMode string) UpdateClashMode(newMode string) + WriteConnections(message *Connections) } func NewStandaloneCommandClient() *CommandClient { @@ -116,6 +117,13 @@ func (c *CommandClient) Connect() error { return nil } go c.handleModeConn(conn) + case CommandConnections: + err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval) + if err != nil { + return E.Cause(err, "write interval") + } + c.handler.Connected() + go c.handleConnectionsConn(conn) } return nil } diff --git a/experimental/libbox/command_close_connection.go b/experimental/libbox/command_close_connection.go new file mode 100644 index 0000000000..62f5dc8419 --- /dev/null +++ b/experimental/libbox/command_close_connection.go @@ -0,0 +1,53 @@ +package libbox + +import ( + "bufio" + "net" + + "github.com/sagernet/sing-box/experimental/clashapi" + "github.com/sagernet/sing/common/binary" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/gofrs/uuid/v5" +) + +func (c *CommandClient) CloseConnection(connId string) error { + conn, err := c.directConnect() + if err != nil { + return err + } + defer conn.Close() + writer := bufio.NewWriter(conn) + err = binary.WriteData(writer, binary.BigEndian, connId) + if err != nil { + return err + } + err = writer.Flush() + if err != nil { + return err + } + return readError(conn) +} + +func (s *CommandServer) handleCloseConnection(conn net.Conn) error { + reader := bufio.NewReader(conn) + var connId string + err := binary.ReadData(reader, binary.BigEndian, &connId) + if err != nil { + return E.Cause(err, "read connection id") + } + service := s.service + if service == nil { + return writeError(conn, E.New("service not ready")) + } + clashServer := service.instance.Router().ClashServer() + if clashServer == nil { + return writeError(conn, E.New("Clash API disabled")) + } + targetConn := clashServer.(*clashapi.Server).TrafficManager().Connection(uuid.FromStringOrNil(connId)) + if targetConn == nil { + return writeError(conn, E.New("connection already closed")) + } + targetConn.Close() + return writeError(conn, nil) +} diff --git a/experimental/libbox/command_connections.go b/experimental/libbox/command_connections.go new file mode 100644 index 0000000000..44d35bbedc --- /dev/null +++ b/experimental/libbox/command_connections.go @@ -0,0 +1,268 @@ +package libbox + +import ( + "bufio" + "net" + "slices" + "strings" + "time" + + "github.com/sagernet/sing-box/experimental/clashapi" + "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" + "github.com/sagernet/sing/common/binary" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + + "github.com/gofrs/uuid/v5" +) + +func (c *CommandClient) handleConnectionsConn(conn net.Conn) { + defer conn.Close() + reader := bufio.NewReader(conn) + var connections Connections + for { + err := binary.ReadData(reader, binary.BigEndian, &connections.connections) + if err != nil { + c.handler.Disconnected(err.Error()) + return + } + c.handler.WriteConnections(&connections) + } +} + +func (s *CommandServer) handleConnectionsConn(conn net.Conn) error { + var interval int64 + err := binary.Read(conn, binary.BigEndian, &interval) + if err != nil { + return E.Cause(err, "read interval") + } + ticker := time.NewTicker(time.Duration(interval)) + defer ticker.Stop() + ctx := connKeepAlive(conn) + var trafficManager *trafficontrol.Manager + for { + service := s.service + if service != nil { + clashServer := service.instance.Router().ClashServer() + if clashServer == nil { + return E.New("Clash API disabled") + } + trafficManager = clashServer.(*clashapi.Server).TrafficManager() + break + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + } + } + var ( + connections = make(map[uuid.UUID]*Connection) + outConnections []Connection + ) + writer := bufio.NewWriter(conn) + for { + outConnections = outConnections[:0] + for _, connection := range trafficManager.Connections() { + outConnections = append(outConnections, newConnection(connections, connection, false)) + } + for _, connection := range trafficManager.ClosedConnections() { + outConnections = append(outConnections, newConnection(connections, connection, true)) + } + err = binary.WriteData(writer, binary.BigEndian, outConnections) + if err != nil { + return err + } + err = writer.Flush() + if err != nil { + return err + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + } + } +} + +const ( + ConnectionStateAll = iota + ConnectionStateActive + ConnectionStateClosed +) + +type Connections struct { + connections []Connection + filteredConnections []Connection + outConnections *[]Connection +} + +func (c *Connections) FilterState(state int32) { + c.filteredConnections = c.filteredConnections[:0] + switch state { + case ConnectionStateAll: + c.filteredConnections = append(c.filteredConnections, c.connections...) + case ConnectionStateActive: + for _, connection := range c.connections { + if connection.ClosedAt == 0 { + c.filteredConnections = append(c.filteredConnections, connection) + } + } + case ConnectionStateClosed: + for _, connection := range c.connections { + if connection.ClosedAt != 0 { + c.filteredConnections = append(c.filteredConnections, connection) + } + } + } +} + +func (c *Connections) SortByDate() { + slices.SortStableFunc(c.filteredConnections, func(x, y Connection) int { + if x.CreatedAt < y.CreatedAt { + return 1 + } else if x.CreatedAt > y.CreatedAt { + return -1 + } else { + return strings.Compare(y.ID, x.ID) + } + }) +} + +func (c *Connections) SortByTraffic() { + slices.SortStableFunc(c.filteredConnections, func(x, y Connection) int { + xTraffic := x.Uplink + x.Downlink + yTraffic := y.Uplink + y.Downlink + if xTraffic < yTraffic { + return 1 + } else if xTraffic > yTraffic { + return -1 + } else { + return strings.Compare(y.ID, x.ID) + } + }) +} + +func (c *Connections) SortByTrafficTotal() { + slices.SortStableFunc(c.filteredConnections, func(x, y Connection) int { + xTraffic := x.UplinkTotal + x.DownlinkTotal + yTraffic := y.UplinkTotal + y.DownlinkTotal + if xTraffic < yTraffic { + return 1 + } else if xTraffic > yTraffic { + return -1 + } else { + return strings.Compare(y.ID, x.ID) + } + }) +} + +func (c *Connections) Iterator() ConnectionIterator { + return newPtrIterator(c.filteredConnections) +} + +type Connection struct { + ID string + Inbound string + InboundType string + IPVersion int32 + Network string + Source string + Destination string + Domain string + Protocol string + User string + FromOutbound string + CreatedAt int64 + ClosedAt int64 + Uplink int64 + Downlink int64 + UplinkTotal int64 + DownlinkTotal int64 + Rule string + Outbound string + OutboundType string + ChainList []string +} + +func (c *Connection) Chain() StringIterator { + return newIterator(c.ChainList) +} + +func (c *Connection) DisplayDestination() string { + destination := M.ParseSocksaddr(c.Destination) + if destination.IsIP() && c.Domain != "" { + destination = M.Socksaddr{ + Fqdn: c.Domain, + Port: destination.Port, + } + return destination.String() + } + return c.Destination +} + +type ConnectionIterator interface { + Next() *Connection + HasNext() bool +} + +func newConnection(connections map[uuid.UUID]*Connection, metadata trafficontrol.TrackerMetadata, isClosed bool) Connection { + if oldConnection, loaded := connections[metadata.ID]; loaded { + if isClosed { + if oldConnection.ClosedAt == 0 { + oldConnection.Uplink = 0 + oldConnection.Downlink = 0 + oldConnection.ClosedAt = metadata.ClosedAt.UnixMilli() + } + return *oldConnection + } + lastUplink := oldConnection.UplinkTotal + lastDownlink := oldConnection.DownlinkTotal + uplinkTotal := metadata.Upload.Load() + downlinkTotal := metadata.Download.Load() + oldConnection.Uplink = uplinkTotal - lastUplink + oldConnection.Downlink = downlinkTotal - lastDownlink + oldConnection.UplinkTotal = uplinkTotal + oldConnection.DownlinkTotal = downlinkTotal + return *oldConnection + } + var rule string + if metadata.Rule != nil { + rule = metadata.Rule.String() + } + uplinkTotal := metadata.Upload.Load() + downlinkTotal := metadata.Download.Load() + uplink := uplinkTotal + downlink := downlinkTotal + var closedAt int64 + if !metadata.ClosedAt.IsZero() { + closedAt = metadata.ClosedAt.UnixMilli() + uplink = 0 + downlink = 0 + } + connection := Connection{ + ID: metadata.ID.String(), + Inbound: metadata.Metadata.Inbound, + InboundType: metadata.Metadata.InboundType, + IPVersion: int32(metadata.Metadata.IPVersion), + Network: metadata.Metadata.Network, + Source: metadata.Metadata.Source.String(), + Destination: metadata.Metadata.Destination.String(), + Domain: metadata.Metadata.Domain, + Protocol: metadata.Metadata.Protocol, + User: metadata.Metadata.User, + FromOutbound: metadata.Metadata.Outbound, + CreatedAt: metadata.CreatedAt.UnixMilli(), + ClosedAt: closedAt, + Uplink: uplink, + Downlink: downlink, + UplinkTotal: uplinkTotal, + DownlinkTotal: downlinkTotal, + Rule: rule, + Outbound: metadata.Outbound, + OutboundType: metadata.OutboundType, + ChainList: metadata.Chain, + } + connections[metadata.ID] = &connection + return connection +} diff --git a/experimental/libbox/command_group.go b/experimental/libbox/command_group.go index 21fd39d29a..a5572ea1f5 100644 --- a/experimental/libbox/command_group.go +++ b/experimental/libbox/command_group.go @@ -14,36 +14,6 @@ import ( "github.com/sagernet/sing/service" ) -type OutboundGroup struct { - Tag string - Type string - Selectable bool - Selected string - IsExpand bool - items []*OutboundGroupItem -} - -func (g *OutboundGroup) GetItems() OutboundGroupItemIterator { - return newIterator(g.items) -} - -type OutboundGroupIterator interface { - Next() *OutboundGroup - HasNext() bool -} - -type OutboundGroupItem struct { - Tag string - Type string - URLTestTime int64 - URLTestDelay int32 -} - -type OutboundGroupItemIterator interface { - Next() *OutboundGroupItem - HasNext() bool -} - func (c *CommandClient) handleGroupConn(conn net.Conn) { defer conn.Close() @@ -92,6 +62,36 @@ func (s *CommandServer) handleGroupConn(conn net.Conn) error { } } +type OutboundGroup struct { + Tag string + Type string + Selectable bool + Selected string + IsExpand bool + items []*OutboundGroupItem +} + +func (g *OutboundGroup) GetItems() OutboundGroupItemIterator { + return newIterator(g.items) +} + +type OutboundGroupIterator interface { + Next() *OutboundGroup + HasNext() bool +} + +type OutboundGroupItem struct { + Tag string + Type string + URLTestTime int64 + URLTestDelay int32 +} + +type OutboundGroupItemIterator interface { + Next() *OutboundGroupItem + HasNext() bool +} + func readGroups(reader io.Reader) (OutboundGroupIterator, error) { var groupLength uint16 err := binary.Read(reader, binary.BigEndian, &groupLength) diff --git a/experimental/libbox/command_server.go b/experimental/libbox/command_server.go index da931ef552..8918756dae 100644 --- a/experimental/libbox/command_server.go +++ b/experimental/libbox/command_server.go @@ -33,6 +33,8 @@ type CommandServer struct { urlTestUpdate chan struct{} modeUpdate chan struct{} logReset chan struct{} + + closedConnections []Connection } type CommandServerHandler interface { @@ -176,6 +178,10 @@ func (s *CommandServer) handleConnection(conn net.Conn) error { return s.handleGetSystemProxyStatus(conn) case CommandSetSystemProxyEnabled: return s.handleSetSystemProxyEnabled(conn) + case CommandConnections: + return s.handleConnectionsConn(conn) + case CommandCloseConnection: + return s.handleCloseConnection(conn) default: return E.New("unknown command: ", command) } diff --git a/experimental/libbox/command_status.go b/experimental/libbox/command_status.go index 7f1eca8c03..4ab09d4b28 100644 --- a/experimental/libbox/command_status.go +++ b/experimental/libbox/command_status.go @@ -36,7 +36,7 @@ func (s *CommandServer) readStatus() StatusMessage { trafficManager := clashServer.(*clashapi.Server).TrafficManager() message.Uplink, message.Downlink = trafficManager.Now() message.UplinkTotal, message.DownlinkTotal = trafficManager.Total() - message.ConnectionsIn = int32(trafficManager.Connections()) + message.ConnectionsIn = int32(trafficManager.ConnectionsLen()) } } diff --git a/experimental/libbox/iterator.go b/experimental/libbox/iterator.go index db64a25978..530a7e43bd 100644 --- a/experimental/libbox/iterator.go +++ b/experimental/libbox/iterator.go @@ -17,6 +17,10 @@ func newIterator[T any](values []T) *iterator[T] { return &iterator[T]{values} } +func newPtrIterator[T any](values []T) *iterator[*T] { + return &iterator[*T]{common.Map(values, func(value T) *T { return &value })} +} + func (i *iterator[T]) Next() T { if len(i.values) == 0 { return common.DefaultValue[T]() diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 0a54d7abd8..c65090103c 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -149,33 +149,6 @@ func (w *platformInterfaceWrapper) OpenTun(options *tun.Options, platformOptions return tun.New(*options) } -func (w *platformInterfaceWrapper) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*process.Info, error) { - var uid int32 - if w.useProcFS { - uid = procfs.ResolveSocketByProcSearch(network, source, destination) - if uid == -1 { - return nil, E.New("procfs: not found") - } - } else { - var ipProtocol int32 - switch N.NetworkName(network) { - case N.NetworkTCP: - ipProtocol = syscall.IPPROTO_TCP - case N.NetworkUDP: - ipProtocol = syscall.IPPROTO_UDP - default: - return nil, E.New("unknown network: ", network) - } - var err error - uid, err = w.iif.FindConnectionOwner(ipProtocol, source.Addr().String(), int32(source.Port()), destination.Addr().String(), int32(destination.Port())) - if err != nil { - return nil, err - } - } - packageName, _ := w.iif.PackageNameByUid(uid) - return &process.Info{UserId: uid, PackageName: packageName}, nil -} - func (w *platformInterfaceWrapper) UsePlatformDefaultInterfaceMonitor() bool { return w.iif.UsePlatformDefaultInterfaceMonitor() } @@ -229,6 +202,33 @@ func (w *platformInterfaceWrapper) ReadWIFIState() adapter.WIFIState { return (adapter.WIFIState)(*wifiState) } +func (w *platformInterfaceWrapper) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*process.Info, error) { + var uid int32 + if w.useProcFS { + uid = procfs.ResolveSocketByProcSearch(network, source, destination) + if uid == -1 { + return nil, E.New("procfs: not found") + } + } else { + var ipProtocol int32 + switch N.NetworkName(network) { + case N.NetworkTCP: + ipProtocol = syscall.IPPROTO_TCP + case N.NetworkUDP: + ipProtocol = syscall.IPPROTO_UDP + default: + return nil, E.New("unknown network: ", network) + } + var err error + uid, err = w.iif.FindConnectionOwner(ipProtocol, source.Addr().String(), int32(source.Port()), destination.Addr().String(), int32(destination.Port())) + if err != nil { + return nil, err + } + } + packageName, _ := w.iif.PackageNameByUid(uid) + return &process.Info{UserId: uid, PackageName: packageName}, nil +} + func (w *platformInterfaceWrapper) DisableColors() bool { return runtime.GOOS != "android" } diff --git a/experimental/libbox/setup.go b/experimental/libbox/setup.go index ea468f391c..31611354bf 100644 --- a/experimental/libbox/setup.go +++ b/experimental/libbox/setup.go @@ -4,10 +4,12 @@ import ( "os" "os/user" "strconv" + "time" "github.com/sagernet/sing-box/common/humanize" C "github.com/sagernet/sing-box/constant" _ "github.com/sagernet/sing-box/include" + "github.com/sagernet/sing-box/log" ) var ( @@ -59,6 +61,10 @@ func FormatMemoryBytes(length int64) string { return humanize.MemoryBytes(uint64(length)) } +func FormatDuration(duration int64) string { + return log.FormatDuration(time.Duration(duration) * time.Millisecond) +} + func ProxyDisplayType(proxyType string) string { return C.ProxyDisplayName(proxyType) } diff --git a/inbound/builder.go b/inbound/builder.go index 513b016f79..ddfd361dbd 100644 --- a/inbound/builder.go +++ b/inbound/builder.go @@ -11,43 +11,43 @@ import ( E "github.com/sagernet/sing/common/exceptions" ) -func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.Inbound, platformInterface platform.Interface) (adapter.Inbound, error) { +func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Inbound, platformInterface platform.Interface) (adapter.Inbound, error) { if options.Type == "" { return nil, E.New("missing inbound type") } switch options.Type { case C.TypeTun: - return NewTun(ctx, router, logger, options.Tag, options.TunOptions, platformInterface) + return NewTun(ctx, router, logger, tag, options.TunOptions, platformInterface) case C.TypeRedirect: - return NewRedirect(ctx, router, logger, options.Tag, options.RedirectOptions), nil + return NewRedirect(ctx, router, logger, tag, options.RedirectOptions), nil case C.TypeTProxy: - return NewTProxy(ctx, router, logger, options.Tag, options.TProxyOptions), nil + return NewTProxy(ctx, router, logger, tag, options.TProxyOptions), nil case C.TypeDirect: - return NewDirect(ctx, router, logger, options.Tag, options.DirectOptions), nil + return NewDirect(ctx, router, logger, tag, options.DirectOptions), nil case C.TypeSOCKS: - return NewSocks(ctx, router, logger, options.Tag, options.SocksOptions), nil + return NewSocks(ctx, router, logger, tag, options.SocksOptions), nil case C.TypeHTTP: - return NewHTTP(ctx, router, logger, options.Tag, options.HTTPOptions) + return NewHTTP(ctx, router, logger, tag, options.HTTPOptions) case C.TypeMixed: - return NewMixed(ctx, router, logger, options.Tag, options.MixedOptions), nil + return NewMixed(ctx, router, logger, tag, options.MixedOptions), nil case C.TypeShadowsocks: - return NewShadowsocks(ctx, router, logger, options.Tag, options.ShadowsocksOptions) + return NewShadowsocks(ctx, router, logger, tag, options.ShadowsocksOptions) case C.TypeVMess: - return NewVMess(ctx, router, logger, options.Tag, options.VMessOptions) + return NewVMess(ctx, router, logger, tag, options.VMessOptions) case C.TypeTrojan: - return NewTrojan(ctx, router, logger, options.Tag, options.TrojanOptions) + return NewTrojan(ctx, router, logger, tag, options.TrojanOptions) case C.TypeNaive: - return NewNaive(ctx, router, logger, options.Tag, options.NaiveOptions) + return NewNaive(ctx, router, logger, tag, options.NaiveOptions) case C.TypeHysteria: - return NewHysteria(ctx, router, logger, options.Tag, options.HysteriaOptions) + return NewHysteria(ctx, router, logger, tag, options.HysteriaOptions) case C.TypeShadowTLS: - return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions) + return NewShadowTLS(ctx, router, logger, tag, options.ShadowTLSOptions) case C.TypeVLESS: - return NewVLESS(ctx, router, logger, options.Tag, options.VLESSOptions) + return NewVLESS(ctx, router, logger, tag, options.VLESSOptions) case C.TypeTUIC: - return NewTUIC(ctx, router, logger, options.Tag, options.TUICOptions) + return NewTUIC(ctx, router, logger, tag, options.TUICOptions) case C.TypeHysteria2: - return NewHysteria2(ctx, router, logger, options.Tag, options.Hysteria2Options) + return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options) default: return nil, E.New("unknown inbound type: ", options.Type) } diff --git a/log/format.go b/log/format.go index 6fb91d3157..6f4347b12a 100644 --- a/log/format.go +++ b/log/format.go @@ -43,7 +43,7 @@ func (f Formatter) Format(ctx context.Context, level Level, tag string, message id, hasId = IDFromContext(ctx) } if hasId { - activeDuration := formatDuration(time.Since(id.CreatedAt)) + activeDuration := FormatDuration(time.Since(id.CreatedAt)) if !f.DisableColors { var color aurora.Color color = aurora.Color(uint8(id.ID)) @@ -113,7 +113,7 @@ func (f Formatter) FormatWithSimple(ctx context.Context, level Level, tag string id, hasId = IDFromContext(ctx) } if hasId { - activeDuration := formatDuration(time.Since(id.CreatedAt)) + activeDuration := FormatDuration(time.Since(id.CreatedAt)) if !f.DisableColors { var color aurora.Color color = aurora.Color(uint8(id.ID)) @@ -163,7 +163,7 @@ func xd(value int, x int) string { return message } -func formatDuration(duration time.Duration) string { +func FormatDuration(duration time.Duration) string { if duration < time.Second { return F.ToString(duration.Milliseconds(), "ms") } else if duration < time.Minute { From cbadd7a0a0de842525eb036f58a5e5e254efc965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 18 Jun 2024 17:49:06 +0800 Subject: [PATCH 07/31] platform: Add log update interval --- experimental/libbox/command_client.go | 8 ++- experimental/libbox/command_log.go | 95 ++++++++++++++++----------- experimental/libbox/iterator.go | 15 +++-- 3 files changed, 71 insertions(+), 47 deletions(-) diff --git a/experimental/libbox/command_client.go b/experimental/libbox/command_client.go index 199dce0d18..bd99511582 100644 --- a/experimental/libbox/command_client.go +++ b/experimental/libbox/command_client.go @@ -25,8 +25,8 @@ type CommandClientOptions struct { type CommandClientHandler interface { Connected() Disconnected(message string) - ClearLog() - WriteLog(message string) + ClearLogs() + WriteLogs(messageList StringIterator) WriteStatus(message *StatusMessage) WriteGroups(message OutboundGroupIterator) InitializeClashMode(modeList StringIterator, currentMode string) @@ -84,6 +84,10 @@ func (c *CommandClient) Connect() error { } switch c.options.Command { case CommandLog: + err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval) + if err != nil { + return E.Cause(err, "write interval") + } c.handler.Connected() go c.handleLogConn(conn) case CommandStatus: diff --git a/experimental/libbox/command_log.go b/experimental/libbox/command_log.go index ce72010dd0..8a22aa2e74 100644 --- a/experimental/libbox/command_log.go +++ b/experimental/libbox/command_log.go @@ -1,10 +1,14 @@ package libbox import ( + "bufio" "context" - "encoding/binary" "io" "net" + "time" + + "github.com/sagernet/sing/common/binary" + E "github.com/sagernet/sing/common/exceptions" ) func (s *CommandServer) WriteMessage(message string) { @@ -17,43 +21,39 @@ func (s *CommandServer) WriteMessage(message string) { s.access.Unlock() } -func readLog(reader io.Reader) ([]byte, error) { - var messageLength uint16 - err := binary.Read(reader, binary.BigEndian, &messageLength) - if err != nil { - return nil, err - } - if messageLength == 0 { - return nil, nil - } - data := make([]byte, messageLength) - _, err = io.ReadFull(reader, data) - if err != nil { - return nil, err - } - return data, nil -} - -func writeLog(writer io.Writer, message []byte) error { +func writeLog(writer *bufio.Writer, messages []string) error { err := binary.Write(writer, binary.BigEndian, uint8(0)) if err != nil { return err } - err = binary.Write(writer, binary.BigEndian, uint16(len(message))) + err = binary.WriteData(writer, binary.BigEndian, messages) if err != nil { return err } - if len(message) > 0 { - _, err = writer.Write(message) - } - return err + return writer.Flush() } -func writeClearLog(writer io.Writer) error { - return binary.Write(writer, binary.BigEndian, uint8(1)) +func writeClearLog(writer *bufio.Writer) error { + err := binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + return writer.Flush() } func (s *CommandServer) handleLogConn(conn net.Conn) error { + var ( + interval int64 + timer *time.Timer + ) + err := binary.Read(conn, binary.BigEndian, &interval) + if err != nil { + return E.Cause(err, "read interval") + } + timer = time.NewTimer(time.Duration(interval)) + if !timer.Stop() { + <-timer.C + } var savedLines []string s.access.Lock() savedLines = make([]string, 0, s.savedLines.Len()) @@ -66,52 +66,67 @@ func (s *CommandServer) handleLogConn(conn net.Conn) error { return err } defer s.observer.UnSubscribe(subscription) - for _, line := range savedLines { - err = writeLog(conn, []byte(line)) + writer := bufio.NewWriter(conn) + if len(savedLines) > 0 { + err = writeLog(writer, savedLines) if err != nil { return err } } ctx := connKeepAlive(conn) + var logLines []string for { select { case <-ctx.Done(): return ctx.Err() - case message := <-subscription: - err = writeLog(conn, []byte(message)) - if err != nil { - return err - } case <-s.logReset: - err = writeClearLog(conn) + err = writeClearLog(writer) if err != nil { return err } case <-done: return nil + case logLine := <-subscription: + logLines = logLines[:0] + logLines = append(logLines, logLine) + timer.Reset(time.Duration(interval)) + loopLogs: + for { + select { + case logLine = <-subscription: + logLines = append(logLines, logLine) + case <-timer.C: + break loopLogs + } + } + err = writeLog(writer, logLines) + if err != nil { + return err + } } } } func (c *CommandClient) handleLogConn(conn net.Conn) { + reader := bufio.NewReader(conn) for { var messageType uint8 - err := binary.Read(conn, binary.BigEndian, &messageType) + err := binary.Read(reader, binary.BigEndian, &messageType) if err != nil { c.handler.Disconnected(err.Error()) return } - var message []byte + var messages []string switch messageType { case 0: - message, err = readLog(conn) + err = binary.ReadData(reader, binary.BigEndian, &messages) if err != nil { c.handler.Disconnected(err.Error()) return } - c.handler.WriteLog(string(message)) + c.handler.WriteLogs(newIterator(messages)) case 1: - c.handler.ClearLog() + c.handler.ClearLogs() } } } @@ -120,7 +135,7 @@ func connKeepAlive(reader io.Reader) context.Context { ctx, cancel := context.WithCancelCause(context.Background()) go func() { for { - _, err := readLog(reader) + _, err := reader.Read(make([]byte, 1)) if err != nil { cancel(err) return diff --git a/experimental/libbox/iterator.go b/experimental/libbox/iterator.go index 530a7e43bd..50d7385b36 100644 --- a/experimental/libbox/iterator.go +++ b/experimental/libbox/iterator.go @@ -3,8 +3,9 @@ package libbox import "github.com/sagernet/sing/common" type StringIterator interface { - Next() string + Len() int32 HasNext() bool + Next() string } var _ StringIterator = (*iterator[string])(nil) @@ -21,6 +22,14 @@ func newPtrIterator[T any](values []T) *iterator[*T] { return &iterator[*T]{common.Map(values, func(value T) *T { return &value })} } +func (i *iterator[T]) Len() int32 { + return int32(len(i.values)) +} + +func (i *iterator[T]) HasNext() bool { + return len(i.values) > 0 +} + func (i *iterator[T]) Next() T { if len(i.values) == 0 { return common.DefaultValue[T]() @@ -30,10 +39,6 @@ func (i *iterator[T]) Next() T { return nextValue } -func (i *iterator[T]) HasNext() bool { - return len(i.values) > 0 -} - type abstractIterator[T any] interface { Next() T HasNext() bool From e889c0c7f1d9487b21f67ea29c89edcb66990646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 25 Jun 2024 20:26:39 +0800 Subject: [PATCH 08/31] platform: Fix clash server reload on android --- experimental/clashapi/server.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index a1152baaf7..1e7804ce44 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -7,7 +7,9 @@ import ( "net" "net/http" "os" + "runtime" "strings" + "syscall" "time" "github.com/sagernet/sing-box/adapter" @@ -143,7 +145,18 @@ func (s *Server) PreStart() error { func (s *Server) Start() error { if s.externalController { s.checkAndDownloadExternalUI() - listener, err := net.Listen("tcp", s.httpServer.Addr) + var ( + listener net.Listener + err error + ) + for i := 0; i < 3; i++ { + listener, err = net.Listen("tcp", s.httpServer.Addr) + if runtime.GOOS == "android" && errors.Is(err, syscall.EADDRINUSE) { + time.Sleep(100 * time.Millisecond) + continue + } + break + } if err != nil { return E.Cause(err, "external controller listen error") } From 318c1f1d3877c5274cea852a1e014e52d5980138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 24 Jun 2024 09:49:15 +0800 Subject: [PATCH 09/31] WTF is this --- adapter/experimental.go | 17 +-- adapter/router.go | 2 +- box_outbound.go | 4 +- cmd/internal/build_libbox/main.go | 4 +- cmd/internal/build_shared/sdk.go | 10 +- cmd/sing-box/cmd_merge.go | 6 +- cmd/sing-box/cmd_run.go | 2 +- common/geosite/reader.go | 74 +++++---- common/geosite/writer.go | 25 ++-- common/srs/binary.go | 113 +++++--------- common/srs/ip_set.go | 105 +++++-------- constant/path.go | 6 +- experimental/libbox/command_clash_mode.go | 18 +-- .../libbox/command_close_connection.go | 5 +- experimental/libbox/command_connections.go | 36 +++-- experimental/libbox/command_group.go | 140 +++--------------- experimental/libbox/command_log.go | 66 +++++---- experimental/libbox/command_power.go | 10 +- experimental/libbox/command_select.go | 10 +- experimental/libbox/command_server.go | 8 - experimental/libbox/command_shared.go | 6 +- experimental/libbox/command_urltest.go | 6 +- experimental/libbox/profile_import.go | 43 +++--- experimental/libbox/setup.go | 6 + go.mod | 2 +- go.sum | 4 +- inbound/mixed.go | 14 +- inbound/vless.go | 11 +- inbound/vmess.go | 11 +- option/outbound.go | 2 +- option/route.go | 2 +- outbound/proxy.go | 18 +-- outbound/tor.go | 12 +- route/router.go | 4 +- route/router_geo_resources.go | 8 +- route/rule_abstract.go | 19 ++- transport/trojan/mux.go | 27 +++- transport/trojan/service.go | 4 +- transport/v2raygrpc/conn.go | 4 +- transport/v2raygrpclite/conn.go | 8 +- 40 files changed, 371 insertions(+), 501 deletions(-) diff --git a/adapter/experimental.go b/adapter/experimental.go index 5e1cbd9d9d..0cab5ed5a8 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -4,14 +4,13 @@ import ( "bytes" "context" "encoding/binary" - "io" "net" "time" "github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-dns" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" ) type ClashServer interface { @@ -56,16 +55,15 @@ func (s *SavedRuleSet) MarshalBinary() ([]byte, error) { if err != nil { return nil, err } - err = rw.WriteUVariant(&buffer, uint64(len(s.Content))) + err = varbin.Write(&buffer, binary.BigEndian, s.Content) if err != nil { return nil, err } - buffer.Write(s.Content) err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix()) if err != nil { return nil, err } - err = rw.WriteVString(&buffer, s.LastEtag) + err = varbin.Write(&buffer, binary.BigEndian, s.LastEtag) if err != nil { return nil, err } @@ -79,12 +77,7 @@ func (s *SavedRuleSet) UnmarshalBinary(data []byte) error { if err != nil { return err } - contentLen, err := rw.ReadUVariant(reader) - if err != nil { - return err - } - s.Content = make([]byte, contentLen) - _, err = io.ReadFull(reader, s.Content) + err = varbin.Read(reader, binary.BigEndian, &s.Content) if err != nil { return err } @@ -94,7 +87,7 @@ func (s *SavedRuleSet) UnmarshalBinary(data []byte) error { return err } s.LastUpdated = time.Unix(lastUpdated, 0) - s.LastEtag, err = rw.ReadVString(reader) + err = varbin.Read(reader, binary.BigEndian, &s.LastEtag) if err != nil { return err } diff --git a/adapter/router.go b/adapter/router.go index 54dc3396dc..c481f0c8c9 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -45,7 +45,7 @@ type Router interface { DefaultInterface() string AutoDetectInterface() bool AutoDetectInterfaceFunc() control.Func - DefaultMark() int + DefaultMark() uint32 NetworkMonitor() tun.NetworkUpdateMonitor InterfaceMonitor() tun.DefaultInterfaceMonitor PackageManager() tun.PackageManager diff --git a/box_outbound.go b/box_outbound.go index 6e3f0617fc..f03f3b7d41 100644 --- a/box_outbound.go +++ b/box_outbound.go @@ -45,7 +45,9 @@ func (s *Box) startOutbounds() error { } started[outboundTag] = true canContinue = true - if starter, isStarter := outboundToStart.(common.Starter); isStarter { + if starter, isStarter := outboundToStart.(interface { + Start() error + }); isStarter { monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]") err := starter.Start() monitor.Finish() diff --git a/cmd/internal/build_libbox/main.go b/cmd/internal/build_libbox/main.go index ae0fe34a6b..fc9308ff40 100644 --- a/cmd/internal/build_libbox/main.go +++ b/cmd/internal/build_libbox/main.go @@ -93,7 +93,7 @@ func buildAndroid() { const name = "libbox.aar" copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs") - if rw.FileExists(copyPath) { + if rw.IsDir(copyPath) { copyPath, _ = filepath.Abs(copyPath) err = rw.CopyFile(name, filepath.Join(copyPath, name)) if err != nil { @@ -134,7 +134,7 @@ func buildiOS() { } copyPath := filepath.Join("..", "sing-box-for-apple") - if rw.FileExists(copyPath) { + if rw.IsDir(copyPath) { targetDir := filepath.Join(copyPath, "Libbox.xcframework") targetDir, _ = filepath.Abs(targetDir) os.RemoveAll(targetDir) diff --git a/cmd/internal/build_shared/sdk.go b/cmd/internal/build_shared/sdk.go index ce7f0c86d0..b6c1ec9deb 100644 --- a/cmd/internal/build_shared/sdk.go +++ b/cmd/internal/build_shared/sdk.go @@ -30,7 +30,7 @@ func FindSDK() { } for _, path := range searchPath { path = os.ExpandEnv(path) - if rw.FileExists(filepath.Join(path, "licenses", "android-sdk-license")) { + if rw.IsFile(filepath.Join(path, "licenses", "android-sdk-license")) { androidSDKPath = path break } @@ -60,7 +60,7 @@ func FindSDK() { func findNDK() bool { const fixedVersion = "26.2.11394342" const versionFile = "source.properties" - if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.FileExists(filepath.Join(fixedPath, versionFile)) { + if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) { androidNDKPath = fixedPath return true } @@ -86,7 +86,7 @@ func findNDK() bool { }) for _, versionName := range versionNames { currentNDKPath := filepath.Join(androidSDKPath, "ndk", versionName) - if rw.FileExists(filepath.Join(androidSDKPath, versionFile)) { + if rw.IsFile(filepath.Join(androidSDKPath, versionFile)) { androidNDKPath = currentNDKPath log.Warn("reproducibility warning: using NDK version " + versionName + " instead of " + fixedVersion) return true @@ -100,11 +100,11 @@ var GoBinPath string func FindMobile() { goBin := filepath.Join(build.Default.GOPATH, "bin") if runtime.GOOS == "windows" { - if !rw.FileExists(filepath.Join(goBin, "gobind.exe")) { + if !rw.IsFile(filepath.Join(goBin, "gobind.exe")) { log.Fatal("missing gomobile installation") } } else { - if !rw.FileExists(filepath.Join(goBin, "gobind")) { + if !rw.IsFile(filepath.Join(goBin, "gobind")) { log.Fatal("missing gomobile installation") } } diff --git a/cmd/sing-box/cmd_merge.go b/cmd/sing-box/cmd_merge.go index 1d19ff17e1..10dd38a1db 100644 --- a/cmd/sing-box/cmd_merge.go +++ b/cmd/sing-box/cmd_merge.go @@ -54,7 +54,11 @@ func merge(outputPath string) error { return nil } } - err = rw.WriteFile(outputPath, buffer.Bytes()) + err = rw.MkdirParent(outputPath) + if err != nil { + return err + } + err = os.WriteFile(outputPath, buffer.Bytes(), 0o644) if err != nil { return err } diff --git a/cmd/sing-box/cmd_run.go b/cmd/sing-box/cmd_run.go index 3c4dd0d910..e717c5945d 100644 --- a/cmd/sing-box/cmd_run.go +++ b/cmd/sing-box/cmd_run.go @@ -109,7 +109,7 @@ func readConfigAndMerge() (option.Options, error) { } var mergedMessage json.RawMessage for _, options := range optionsList { - mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage) + mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage, false) if err != nil { return option.Options{}, E.Cause(err, "merge config at ", options.path) } diff --git a/common/geosite/reader.go b/common/geosite/reader.go index a1b39f28b4..3947779966 100644 --- a/common/geosite/reader.go +++ b/common/geosite/reader.go @@ -1,17 +1,24 @@ package geosite import ( + "bufio" + "encoding/binary" "io" "os" + "sync" + "sync/atomic" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" ) type Reader struct { - reader io.ReadSeeker - domainIndex map[string]int - domainLength map[string]int + access sync.Mutex + reader io.ReadSeeker + bufferedReader *bufio.Reader + metadataIndex int64 + domainIndex map[string]int + domainLength map[string]int } func Open(path string) (*Reader, []string, error) { @@ -34,15 +41,23 @@ func Open(path string) (*Reader, []string, error) { return reader, codes, nil } +type geositeMetadata struct { + Code string + Index uint64 + Length uint64 +} + func (r *Reader) readMetadata() error { - version, err := rw.ReadByte(r.reader) + counter := &readCounter{Reader: r.reader} + reader := bufio.NewReader(counter) + version, err := reader.ReadByte() if err != nil { return err } if version != 0 { return E.New("unknown version") } - entryLength, err := rw.ReadUVariant(r.reader) + entryLength, err := binary.ReadUvarint(reader) if err != nil { return err } @@ -55,16 +70,16 @@ func (r *Reader) readMetadata() error { codeIndex uint64 codeLength uint64 ) - code, err = rw.ReadVString(r.reader) + code, err = varbin.ReadValue[string](reader, binary.BigEndian) if err != nil { return err } keys[i] = code - codeIndex, err = rw.ReadUVariant(r.reader) + codeIndex, err = binary.ReadUvarint(reader) if err != nil { return err } - codeLength, err = rw.ReadUVariant(r.reader) + codeLength, err = binary.ReadUvarint(reader) if err != nil { return err } @@ -73,6 +88,8 @@ func (r *Reader) readMetadata() error { } r.domainIndex = domainIndex r.domainLength = domainLength + r.metadataIndex = counter.count - int64(reader.Buffered()) + r.bufferedReader = reader return nil } @@ -81,31 +98,32 @@ func (r *Reader) Read(code string) ([]Item, error) { if !exists { return nil, E.New("code ", code, " not exists!") } - _, err := r.reader.Seek(int64(index), io.SeekCurrent) + _, err := r.reader.Seek(r.metadataIndex+int64(index), io.SeekStart) if err != nil { return nil, err } - counter := &rw.ReadCounter{Reader: r.reader} - domain := make([]Item, r.domainLength[code]) - for i := range domain { - var ( - item Item - err error - ) - item.Type, err = rw.ReadByte(counter) - if err != nil { - return nil, err - } - item.Value, err = rw.ReadVString(counter) - if err != nil { - return nil, err - } - domain[i] = item + r.bufferedReader.Reset(r.reader) + itemList := make([]Item, r.domainLength[code]) + err = varbin.Read(r.bufferedReader, binary.BigEndian, &itemList) + if err != nil { + return nil, err } - _, err = r.reader.Seek(int64(-index)-counter.Count(), io.SeekCurrent) - return domain, err + return itemList, nil } func (r *Reader) Upstream() any { return r.reader } + +type readCounter struct { + io.Reader + count int64 +} + +func (r *readCounter) Read(p []byte) (n int, err error) { + n, err = r.Reader.Read(p) + if n > 0 { + atomic.AddInt64(&r.count, int64(n)) + } + return +} diff --git a/common/geosite/writer.go b/common/geosite/writer.go index 4e7ec514b1..2847a4a61f 100644 --- a/common/geosite/writer.go +++ b/common/geosite/writer.go @@ -2,13 +2,13 @@ package geosite import ( "bytes" - "io" + "encoding/binary" "sort" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" ) -func Write(writer io.Writer, domains map[string][]Item) error { +func Write(writer varbin.Writer, domains map[string][]Item) error { keys := make([]string, 0, len(domains)) for code := range domains { keys = append(keys, code) @@ -19,35 +19,32 @@ func Write(writer io.Writer, domains map[string][]Item) error { index := make(map[string]int) for _, code := range keys { index[code] = content.Len() - for _, domain := range domains[code] { - content.WriteByte(domain.Type) - err := rw.WriteVString(content, domain.Value) - if err != nil { - return err - } + err := varbin.Write(content, binary.BigEndian, domains[code]) + if err != nil { + return err } } - err := rw.WriteByte(writer, 0) + err := writer.WriteByte(0) if err != nil { return err } - err = rw.WriteUVariant(writer, uint64(len(keys))) + _, err = varbin.WriteUvarint(writer, uint64(len(keys))) if err != nil { return err } for _, code := range keys { - err = rw.WriteVString(writer, code) + err = varbin.Write(writer, binary.BigEndian, code) if err != nil { return err } - err = rw.WriteUVariant(writer, uint64(index[code])) + _, err = varbin.WriteUvarint(writer, uint64(index[code])) if err != nil { return err } - err = rw.WriteUVariant(writer, uint64(len(domains[code]))) + _, err = varbin.WriteUvarint(writer, uint64(len(domains[code]))) if err != nil { return err } diff --git a/common/srs/binary.go b/common/srs/binary.go index faf4cd17b2..c7c55e0838 100644 --- a/common/srs/binary.go +++ b/common/srs/binary.go @@ -1,6 +1,7 @@ package srs import ( + "bufio" "compress/zlib" "encoding/binary" "io" @@ -11,7 +12,7 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/domain" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" "go4.org/netipx" ) @@ -38,7 +39,7 @@ const ( ruleItemFinal uint8 = 0xFF ) -func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) { +func Read(reader io.Reader, recover bool) (ruleSet option.PlainRuleSet, err error) { var magicBytes [3]byte _, err = io.ReadFull(reader, magicBytes[:]) if err != nil { @@ -60,13 +61,14 @@ func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err err if err != nil { return } - length, err := rw.ReadUVariant(zReader) + bReader := bufio.NewReader(zReader) + length, err := binary.ReadUvarint(bReader) if err != nil { return } ruleSet.Rules = make([]option.HeadlessRule, length) for i := uint64(0); i < length; i++ { - ruleSet.Rules[i], err = readRule(zReader, recovery) + ruleSet.Rules[i], err = readRule(bReader, recover) if err != nil { err = E.Cause(err, "read rule[", i, "]") return @@ -88,20 +90,25 @@ func Write(writer io.Writer, ruleSet option.PlainRuleSet) error { if err != nil { return err } - err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules))) + bWriter := bufio.NewWriter(zWriter) + _, err = varbin.WriteUvarint(bWriter, uint64(len(ruleSet.Rules))) if err != nil { return err } for _, rule := range ruleSet.Rules { - err = writeRule(zWriter, rule) + err = writeRule(bWriter, rule) if err != nil { return err } } + err = bWriter.Flush() + if err != nil { + return err + } return zWriter.Close() } -func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) { +func readRule(reader varbin.Reader, recover bool) (rule option.HeadlessRule, err error) { var ruleType uint8 err = binary.Read(reader, binary.BigEndian, &ruleType) if err != nil { @@ -110,17 +117,17 @@ func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err er switch ruleType { case 0: rule.Type = C.RuleTypeDefault - rule.DefaultOptions, err = readDefaultRule(reader, recovery) + rule.DefaultOptions, err = readDefaultRule(reader, recover) case 1: rule.Type = C.RuleTypeLogical - rule.LogicalOptions, err = readLogicalRule(reader, recovery) + rule.LogicalOptions, err = readLogicalRule(reader, recover) default: err = E.New("unknown rule type: ", ruleType) } return } -func writeRule(writer io.Writer, rule option.HeadlessRule) error { +func writeRule(writer varbin.Writer, rule option.HeadlessRule) error { switch rule.Type { case C.RuleTypeDefault: return writeDefaultRule(writer, rule.DefaultOptions) @@ -131,7 +138,7 @@ func writeRule(writer io.Writer, rule option.HeadlessRule) error { } } -func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) { +func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHeadlessRule, err error) { var lastItemType uint8 for { var itemType uint8 @@ -158,6 +165,9 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle return } rule.DomainMatcher = matcher + if recover { + rule.Domain, rule.DomainSuffix = matcher.Dump() + } case ruleItemDomainKeyword: rule.DomainKeyword, err = readRuleItemString(reader) case ruleItemDomainRegex: @@ -167,7 +177,7 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle if err != nil { return } - if recovery { + if recover { rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String) } case ruleItemIPCIDR: @@ -175,7 +185,7 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle if err != nil { return } - if recovery { + if recover { rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String) } case ruleItemSourcePort: @@ -209,7 +219,7 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle } } -func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error { +func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule) error { err := binary.Write(writer, binary.BigEndian, uint8(0)) if err != nil { return err @@ -327,73 +337,31 @@ func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error { return nil } -func readRuleItemString(reader io.Reader) ([]string, error) { - length, err := rw.ReadUVariant(reader) - if err != nil { - return nil, err - } - value := make([]string, length) - for i := uint64(0); i < length; i++ { - value[i], err = rw.ReadVString(reader) - if err != nil { - return nil, err - } - } - return value, nil +func readRuleItemString(reader varbin.Reader) ([]string, error) { + return varbin.ReadValue[[]string](reader, binary.BigEndian) } -func writeRuleItemString(writer io.Writer, itemType uint8, value []string) error { - err := binary.Write(writer, binary.BigEndian, itemType) - if err != nil { - return err - } - err = rw.WriteUVariant(writer, uint64(len(value))) +func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) error { + err := writer.WriteByte(itemType) if err != nil { return err } - for _, item := range value { - err = rw.WriteVString(writer, item) - if err != nil { - return err - } - } - return nil + return varbin.Write(writer, binary.BigEndian, value) } -func readRuleItemUint16(reader io.Reader) ([]uint16, error) { - length, err := rw.ReadUVariant(reader) - if err != nil { - return nil, err - } - value := make([]uint16, length) - for i := uint64(0); i < length; i++ { - err = binary.Read(reader, binary.BigEndian, &value[i]) - if err != nil { - return nil, err - } - } - return value, nil +func readRuleItemUint16(reader varbin.Reader) ([]uint16, error) { + return varbin.ReadValue[[]uint16](reader, binary.BigEndian) } -func writeRuleItemUint16(writer io.Writer, itemType uint8, value []uint16) error { - err := binary.Write(writer, binary.BigEndian, itemType) +func writeRuleItemUint16(writer varbin.Writer, itemType uint8, value []uint16) error { + err := writer.WriteByte(itemType) if err != nil { return err } - err = rw.WriteUVariant(writer, uint64(len(value))) - if err != nil { - return err - } - for _, item := range value { - err = binary.Write(writer, binary.BigEndian, item) - if err != nil { - return err - } - } - return nil + return varbin.Write(writer, binary.BigEndian, value) } -func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error { +func writeRuleItemCIDR(writer varbin.Writer, itemType uint8, value []string) error { var builder netipx.IPSetBuilder for i, prefixString := range value { prefix, err := netip.ParsePrefix(prefixString) @@ -419,9 +387,8 @@ func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error { return writeIPSet(writer, ipSet) } -func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) { - var mode uint8 - err = binary.Read(reader, binary.BigEndian, &mode) +func readLogicalRule(reader varbin.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) { + mode, err := reader.ReadByte() if err != nil { return } @@ -434,7 +401,7 @@ func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.Logica err = E.New("unknown logical mode: ", mode) return } - length, err := rw.ReadUVariant(reader) + length, err := binary.ReadUvarint(reader) if err != nil { return } @@ -453,7 +420,7 @@ func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.Logica return } -func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error { +func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRule) error { err := binary.Write(writer, binary.BigEndian, uint8(1)) if err != nil { return err @@ -469,7 +436,7 @@ func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) if err != nil { return err } - err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules))) + _, err = varbin.WriteUvarint(writer, uint64(len(logicalRule.Rules))) if err != nil { return err } diff --git a/common/srs/ip_set.go b/common/srs/ip_set.go index b346da26f6..044dc823b3 100644 --- a/common/srs/ip_set.go +++ b/common/srs/ip_set.go @@ -2,11 +2,13 @@ package srs import ( "encoding/binary" - "io" "net/netip" + "os" "unsafe" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/varbin" "go4.org/netipx" ) @@ -20,94 +22,57 @@ type myIPRange struct { to netip.Addr } -func readIPSet(reader io.Reader) (*netipx.IPSet, error) { - var version uint8 - err := binary.Read(reader, binary.BigEndian, &version) +type myIPRangeData struct { + From []byte + To []byte +} + +func readIPSet(reader varbin.Reader) (*netipx.IPSet, error) { + version, err := reader.ReadByte() if err != nil { return nil, err } + if version != 1 { + return nil, os.ErrInvalid + } + // WTF why using uint64 here var length uint64 err = binary.Read(reader, binary.BigEndian, &length) if err != nil { return nil, err } + ranges := make([]myIPRangeData, length) + err = varbin.Read(reader, binary.BigEndian, &ranges) + if err != nil { + return nil, err + } mySet := &myIPSet{ - rr: make([]myIPRange, length), + rr: make([]myIPRange, len(ranges)), } - for i := uint64(0); i < length; i++ { - var ( - fromLen uint64 - toLen uint64 - fromAddr netip.Addr - toAddr netip.Addr - ) - fromLen, err = rw.ReadUVariant(reader) - if err != nil { - return nil, err - } - fromBytes := make([]byte, fromLen) - _, err = io.ReadFull(reader, fromBytes) - if err != nil { - return nil, err - } - err = fromAddr.UnmarshalBinary(fromBytes) - if err != nil { - return nil, err - } - toLen, err = rw.ReadUVariant(reader) - if err != nil { - return nil, err - } - toBytes := make([]byte, toLen) - _, err = io.ReadFull(reader, toBytes) - if err != nil { - return nil, err - } - err = toAddr.UnmarshalBinary(toBytes) - if err != nil { - return nil, err - } - mySet.rr[i] = myIPRange{fromAddr, toAddr} + for i, rangeData := range ranges { + mySet.rr[i].from = M.AddrFromIP(rangeData.From) + mySet.rr[i].to = M.AddrFromIP(rangeData.To) } return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil } -func writeIPSet(writer io.Writer, set *netipx.IPSet) error { - err := binary.Write(writer, binary.BigEndian, uint8(1)) +func writeIPSet(writer varbin.Writer, set *netipx.IPSet) error { + err := writer.WriteByte(1) if err != nil { return err } - mySet := (*myIPSet)(unsafe.Pointer(set)) - err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr))) + dataList := common.Map((*myIPSet)(unsafe.Pointer(set)).rr, func(rr myIPRange) myIPRangeData { + return myIPRangeData{ + From: rr.from.AsSlice(), + To: rr.to.AsSlice(), + } + }) + err = binary.Write(writer, binary.BigEndian, uint64(len(dataList))) if err != nil { return err } - for _, rr := range mySet.rr { - var ( - fromBinary []byte - toBinary []byte - ) - fromBinary, err = rr.from.MarshalBinary() - if err != nil { - return err - } - err = rw.WriteUVariant(writer, uint64(len(fromBinary))) - if err != nil { - return err - } - _, err = writer.Write(fromBinary) - if err != nil { - return err - } - toBinary, err = rr.to.MarshalBinary() - if err != nil { - return err - } - err = rw.WriteUVariant(writer, uint64(len(toBinary))) - if err != nil { - return err - } - _, err = writer.Write(toBinary) + for _, data := range dataList { + err = varbin.Write(writer, binary.BigEndian, data) if err != nil { return err } diff --git a/constant/path.go b/constant/path.go index 98acacdc37..ea2aad3e23 100644 --- a/constant/path.go +++ b/constant/path.go @@ -13,14 +13,14 @@ var resourcePaths []string func FindPath(name string) (string, bool) { name = os.ExpandEnv(name) - if rw.FileExists(name) { + if rw.IsFile(name) { return name, true } for _, dir := range resourcePaths { - if path := filepath.Join(dir, dirName, name); rw.FileExists(path) { + if path := filepath.Join(dir, dirName, name); rw.IsFile(path) { return path, true } - if path := filepath.Join(dir, name); rw.FileExists(path) { + if path := filepath.Join(dir, name); rw.IsFile(path) { return path, true } } diff --git a/experimental/libbox/command_clash_mode.go b/experimental/libbox/command_clash_mode.go index 3377ae3afd..1b6eb47085 100644 --- a/experimental/libbox/command_clash_mode.go +++ b/experimental/libbox/command_clash_mode.go @@ -9,7 +9,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/experimental/clashapi" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" ) func (c *CommandClient) SetClashMode(newMode string) error { @@ -22,7 +22,7 @@ func (c *CommandClient) SetClashMode(newMode string) error { if err != nil { return err } - err = rw.WriteVString(conn, newMode) + err = varbin.Write(conn, binary.BigEndian, newMode) if err != nil { return err } @@ -30,7 +30,7 @@ func (c *CommandClient) SetClashMode(newMode string) error { } func (s *CommandServer) handleSetClashMode(conn net.Conn) error { - newMode, err := rw.ReadVString(conn) + newMode, err := varbin.ReadValue[string](conn, binary.BigEndian) if err != nil { return err } @@ -50,7 +50,7 @@ func (c *CommandClient) handleModeConn(conn net.Conn) { defer conn.Close() for { - newMode, err := rw.ReadVString(conn) + newMode, err := varbin.ReadValue[string](conn, binary.BigEndian) if err != nil { c.handler.Disconnected(err.Error()) return @@ -80,7 +80,7 @@ func (s *CommandServer) handleModeConn(conn net.Conn) error { for { select { case <-s.modeUpdate: - err = rw.WriteVString(conn, clashServer.Mode()) + err = varbin.Write(conn, binary.BigEndian, clashServer.Mode()) if err != nil { return err } @@ -101,12 +101,12 @@ func readClashModeList(reader io.Reader) (modeList []string, currentMode string, } modeList = make([]string, modeListLength) for i := 0; i < int(modeListLength); i++ { - modeList[i], err = rw.ReadVString(reader) + modeList[i], err = varbin.ReadValue[string](reader, binary.BigEndian) if err != nil { return } } - currentMode, err = rw.ReadVString(reader) + currentMode, err = varbin.ReadValue[string](reader, binary.BigEndian) return } @@ -118,12 +118,12 @@ func writeClashModeList(writer io.Writer, clashServer adapter.ClashServer) error } if len(modeList) > 0 { for _, mode := range modeList { - err = rw.WriteVString(writer, mode) + err = varbin.Write(writer, binary.BigEndian, mode) if err != nil { return err } } - err = rw.WriteVString(writer, clashServer.Mode()) + err = varbin.Write(writer, binary.BigEndian, clashServer.Mode()) if err != nil { return err } diff --git a/experimental/libbox/command_close_connection.go b/experimental/libbox/command_close_connection.go index 62f5dc8419..1edd591122 100644 --- a/experimental/libbox/command_close_connection.go +++ b/experimental/libbox/command_close_connection.go @@ -7,6 +7,7 @@ import ( "github.com/sagernet/sing-box/experimental/clashapi" "github.com/sagernet/sing/common/binary" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/varbin" "github.com/gofrs/uuid/v5" ) @@ -18,7 +19,7 @@ func (c *CommandClient) CloseConnection(connId string) error { } defer conn.Close() writer := bufio.NewWriter(conn) - err = binary.WriteData(writer, binary.BigEndian, connId) + err = varbin.Write(writer, binary.BigEndian, connId) if err != nil { return err } @@ -32,7 +33,7 @@ func (c *CommandClient) CloseConnection(connId string) error { func (s *CommandServer) handleCloseConnection(conn net.Conn) error { reader := bufio.NewReader(conn) var connId string - err := binary.ReadData(reader, binary.BigEndian, &connId) + err := varbin.Read(reader, binary.BigEndian, &connId) if err != nil { return E.Cause(err, "read connection id") } diff --git a/experimental/libbox/command_connections.go b/experimental/libbox/command_connections.go index 44d35bbedc..de90c95259 100644 --- a/experimental/libbox/command_connections.go +++ b/experimental/libbox/command_connections.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing/common/binary" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/varbin" "github.com/gofrs/uuid/v5" ) @@ -19,13 +20,17 @@ import ( func (c *CommandClient) handleConnectionsConn(conn net.Conn) { defer conn.Close() reader := bufio.NewReader(conn) - var connections Connections + var ( + rawConnections []Connection + connections Connections + ) for { - err := binary.ReadData(reader, binary.BigEndian, &connections.connections) + err := varbin.Read(reader, binary.BigEndian, &rawConnections) if err != nil { c.handler.Disconnected(err.Error()) return } + connections.input = rawConnections c.handler.WriteConnections(&connections) } } @@ -69,7 +74,7 @@ func (s *CommandServer) handleConnectionsConn(conn net.Conn) error { for _, connection := range trafficManager.ClosedConnections() { outConnections = append(outConnections, newConnection(connections, connection, true)) } - err = binary.WriteData(writer, binary.BigEndian, outConnections) + err = varbin.Write(writer, binary.BigEndian, outConnections) if err != nil { return err } @@ -92,33 +97,32 @@ const ( ) type Connections struct { - connections []Connection - filteredConnections []Connection - outConnections *[]Connection + input []Connection + filtered []Connection } func (c *Connections) FilterState(state int32) { - c.filteredConnections = c.filteredConnections[:0] + c.filtered = c.filtered[:0] switch state { case ConnectionStateAll: - c.filteredConnections = append(c.filteredConnections, c.connections...) + c.filtered = append(c.filtered, c.input...) case ConnectionStateActive: - for _, connection := range c.connections { + for _, connection := range c.input { if connection.ClosedAt == 0 { - c.filteredConnections = append(c.filteredConnections, connection) + c.filtered = append(c.filtered, connection) } } case ConnectionStateClosed: - for _, connection := range c.connections { + for _, connection := range c.input { if connection.ClosedAt != 0 { - c.filteredConnections = append(c.filteredConnections, connection) + c.filtered = append(c.filtered, connection) } } } } func (c *Connections) SortByDate() { - slices.SortStableFunc(c.filteredConnections, func(x, y Connection) int { + slices.SortStableFunc(c.filtered, func(x, y Connection) int { if x.CreatedAt < y.CreatedAt { return 1 } else if x.CreatedAt > y.CreatedAt { @@ -130,7 +134,7 @@ func (c *Connections) SortByDate() { } func (c *Connections) SortByTraffic() { - slices.SortStableFunc(c.filteredConnections, func(x, y Connection) int { + slices.SortStableFunc(c.filtered, func(x, y Connection) int { xTraffic := x.Uplink + x.Downlink yTraffic := y.Uplink + y.Downlink if xTraffic < yTraffic { @@ -144,7 +148,7 @@ func (c *Connections) SortByTraffic() { } func (c *Connections) SortByTrafficTotal() { - slices.SortStableFunc(c.filteredConnections, func(x, y Connection) int { + slices.SortStableFunc(c.filtered, func(x, y Connection) int { xTraffic := x.UplinkTotal + x.DownlinkTotal yTraffic := y.UplinkTotal + y.DownlinkTotal if xTraffic < yTraffic { @@ -158,7 +162,7 @@ func (c *Connections) SortByTrafficTotal() { } func (c *Connections) Iterator() ConnectionIterator { - return newPtrIterator(c.filteredConnections) + return newPtrIterator(c.filtered) } type Connection struct { diff --git a/experimental/libbox/command_group.go b/experimental/libbox/command_group.go index a5572ea1f5..3a8d2a0749 100644 --- a/experimental/libbox/command_group.go +++ b/experimental/libbox/command_group.go @@ -1,6 +1,7 @@ package libbox import ( + "bufio" "encoding/binary" "io" "net" @@ -10,7 +11,7 @@ import ( "github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/outbound" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" "github.com/sagernet/sing/service" ) @@ -36,19 +37,24 @@ func (s *CommandServer) handleGroupConn(conn net.Conn) error { ticker := time.NewTicker(time.Duration(interval)) defer ticker.Stop() ctx := connKeepAlive(conn) + writer := bufio.NewWriter(conn) for { service := s.service if service != nil { - err := writeGroups(conn, service) + err = writeGroups(writer, service) if err != nil { return err } } else { - err := binary.Write(conn, binary.BigEndian, uint16(0)) + err = binary.Write(writer, binary.BigEndian, uint16(0)) if err != nil { return err } } + err = writer.Flush() + if err != nil { + return err + } select { case <-ctx.Done(): return ctx.Err() @@ -68,11 +74,11 @@ type OutboundGroup struct { Selectable bool Selected string IsExpand bool - items []*OutboundGroupItem + ItemList []*OutboundGroupItem } func (g *OutboundGroup) GetItems() OutboundGroupItemIterator { - return newIterator(g.items) + return newIterator(g.ItemList) } type OutboundGroupIterator interface { @@ -93,73 +99,10 @@ type OutboundGroupItemIterator interface { } func readGroups(reader io.Reader) (OutboundGroupIterator, error) { - var groupLength uint16 - err := binary.Read(reader, binary.BigEndian, &groupLength) + groups, err := varbin.ReadValue[[]*OutboundGroup](reader, binary.BigEndian) if err != nil { return nil, err } - - groups := make([]*OutboundGroup, 0, groupLength) - for i := 0; i < int(groupLength); i++ { - var group OutboundGroup - group.Tag, err = rw.ReadVString(reader) - if err != nil { - return nil, err - } - - group.Type, err = rw.ReadVString(reader) - if err != nil { - return nil, err - } - - err = binary.Read(reader, binary.BigEndian, &group.Selectable) - if err != nil { - return nil, err - } - - group.Selected, err = rw.ReadVString(reader) - if err != nil { - return nil, err - } - - err = binary.Read(reader, binary.BigEndian, &group.IsExpand) - if err != nil { - return nil, err - } - - var itemLength uint16 - err = binary.Read(reader, binary.BigEndian, &itemLength) - if err != nil { - return nil, err - } - - group.items = make([]*OutboundGroupItem, itemLength) - for j := 0; j < int(itemLength); j++ { - var item OutboundGroupItem - item.Tag, err = rw.ReadVString(reader) - if err != nil { - return nil, err - } - - item.Type, err = rw.ReadVString(reader) - if err != nil { - return nil, err - } - - err = binary.Read(reader, binary.BigEndian, &item.URLTestTime) - if err != nil { - return nil, err - } - - err = binary.Read(reader, binary.BigEndian, &item.URLTestDelay) - if err != nil { - return nil, err - } - - group.items[j] = &item - } - groups = append(groups, &group) - } return newIterator(groups), nil } @@ -199,63 +142,14 @@ func writeGroups(writer io.Writer, boxService *BoxService) error { item.URLTestTime = history.Time.Unix() item.URLTestDelay = int32(history.Delay) } - group.items = append(group.items, &item) + group.ItemList = append(group.ItemList, &item) } - if len(group.items) < 2 { + if len(group.ItemList) < 2 { continue } groups = append(groups, group) } - - err := binary.Write(writer, binary.BigEndian, uint16(len(groups))) - if err != nil { - return err - } - for _, group := range groups { - err = rw.WriteVString(writer, group.Tag) - if err != nil { - return err - } - err = rw.WriteVString(writer, group.Type) - if err != nil { - return err - } - err = binary.Write(writer, binary.BigEndian, group.Selectable) - if err != nil { - return err - } - err = rw.WriteVString(writer, group.Selected) - if err != nil { - return err - } - err = binary.Write(writer, binary.BigEndian, group.IsExpand) - if err != nil { - return err - } - err = binary.Write(writer, binary.BigEndian, uint16(len(group.items))) - if err != nil { - return err - } - for _, item := range group.items { - err = rw.WriteVString(writer, item.Tag) - if err != nil { - return err - } - err = rw.WriteVString(writer, item.Type) - if err != nil { - return err - } - err = binary.Write(writer, binary.BigEndian, item.URLTestTime) - if err != nil { - return err - } - err = binary.Write(writer, binary.BigEndian, item.URLTestDelay) - if err != nil { - return err - } - } - } - return nil + return varbin.Write(writer, binary.BigEndian, groups) } func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error { @@ -268,7 +162,7 @@ func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error { if err != nil { return err } - err = rw.WriteVString(conn, groupTag) + err = varbin.Write(conn, binary.BigEndian, groupTag) if err != nil { return err } @@ -280,7 +174,7 @@ func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error { } func (s *CommandServer) handleSetGroupExpand(conn net.Conn) error { - groupTag, err := rw.ReadVString(conn) + groupTag, err := varbin.ReadValue[string](conn, binary.BigEndian) if err != nil { return err } diff --git a/experimental/libbox/command_log.go b/experimental/libbox/command_log.go index 8a22aa2e74..07f6e83919 100644 --- a/experimental/libbox/command_log.go +++ b/experimental/libbox/command_log.go @@ -9,8 +9,19 @@ import ( "github.com/sagernet/sing/common/binary" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/varbin" ) +func (s *CommandServer) ResetLog() { + s.access.Lock() + defer s.access.Unlock() + s.savedLines.Init() + select { + case s.logReset <- struct{}{}: + default: + } +} + func (s *CommandServer) WriteMessage(message string) { s.subscriber.Emit(message) s.access.Lock() @@ -21,26 +32,6 @@ func (s *CommandServer) WriteMessage(message string) { s.access.Unlock() } -func writeLog(writer *bufio.Writer, messages []string) error { - err := binary.Write(writer, binary.BigEndian, uint8(0)) - if err != nil { - return err - } - err = binary.WriteData(writer, binary.BigEndian, messages) - if err != nil { - return err - } - return writer.Flush() -} - -func writeClearLog(writer *bufio.Writer) error { - err := binary.Write(writer, binary.BigEndian, uint8(1)) - if err != nil { - return err - } - return writer.Flush() -} - func (s *CommandServer) handleLogConn(conn net.Conn) error { var ( interval int64 @@ -67,8 +58,24 @@ func (s *CommandServer) handleLogConn(conn net.Conn) error { } defer s.observer.UnSubscribe(subscription) writer := bufio.NewWriter(conn) + select { + case <-s.logReset: + err = writer.WriteByte(1) + if err != nil { + return err + } + err = writer.Flush() + if err != nil { + return err + } + default: + } if len(savedLines) > 0 { - err = writeLog(writer, savedLines) + err = writer.WriteByte(0) + if err != nil { + return err + } + err = varbin.Write(writer, binary.BigEndian, savedLines) if err != nil { return err } @@ -76,11 +83,15 @@ func (s *CommandServer) handleLogConn(conn net.Conn) error { ctx := connKeepAlive(conn) var logLines []string for { + err = writer.Flush() + if err != nil { + return err + } select { case <-ctx.Done(): return ctx.Err() case <-s.logReset: - err = writeClearLog(writer) + err = writer.WriteByte(1) if err != nil { return err } @@ -99,7 +110,11 @@ func (s *CommandServer) handleLogConn(conn net.Conn) error { break loopLogs } } - err = writeLog(writer, logLines) + err = writer.WriteByte(0) + if err != nil { + return err + } + err = varbin.Write(writer, binary.BigEndian, logLines) if err != nil { return err } @@ -110,8 +125,7 @@ func (s *CommandServer) handleLogConn(conn net.Conn) error { func (c *CommandClient) handleLogConn(conn net.Conn) { reader := bufio.NewReader(conn) for { - var messageType uint8 - err := binary.Read(reader, binary.BigEndian, &messageType) + messageType, err := reader.ReadByte() if err != nil { c.handler.Disconnected(err.Error()) return @@ -119,7 +133,7 @@ func (c *CommandClient) handleLogConn(conn net.Conn) { var messages []string switch messageType { case 0: - err = binary.ReadData(reader, binary.BigEndian, &messages) + err = varbin.Read(reader, binary.BigEndian, &messages) if err != nil { c.handler.Disconnected(err.Error()) return diff --git a/experimental/libbox/command_power.go b/experimental/libbox/command_power.go index 619cb57b50..5ed7b01428 100644 --- a/experimental/libbox/command_power.go +++ b/experimental/libbox/command_power.go @@ -5,7 +5,7 @@ import ( "net" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" ) func (c *CommandClient) ServiceReload() error { @@ -24,7 +24,7 @@ func (c *CommandClient) ServiceReload() error { return err } if hasError { - errorMessage, err := rw.ReadVString(conn) + errorMessage, err := varbin.ReadValue[string](conn, binary.BigEndian) if err != nil { return err } @@ -40,7 +40,7 @@ func (s *CommandServer) handleServiceReload(conn net.Conn) error { return err } if rErr != nil { - return rw.WriteVString(conn, rErr.Error()) + return varbin.Write(conn, binary.BigEndian, rErr.Error()) } return nil } @@ -61,7 +61,7 @@ func (c *CommandClient) ServiceClose() error { return nil } if hasError { - errorMessage, err := rw.ReadVString(conn) + errorMessage, err := varbin.ReadValue[string](conn, binary.BigEndian) if err != nil { return nil } @@ -78,7 +78,7 @@ func (s *CommandServer) handleServiceClose(conn net.Conn) error { return err } if rErr != nil { - return rw.WriteVString(conn, rErr.Error()) + return varbin.Write(conn, binary.BigEndian, rErr.Error()) } return nil } diff --git a/experimental/libbox/command_select.go b/experimental/libbox/command_select.go index e7d5b08f86..e1e67e6068 100644 --- a/experimental/libbox/command_select.go +++ b/experimental/libbox/command_select.go @@ -6,7 +6,7 @@ import ( "github.com/sagernet/sing-box/outbound" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" ) func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) error { @@ -19,11 +19,11 @@ func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) erro if err != nil { return err } - err = rw.WriteVString(conn, groupTag) + err = varbin.Write(conn, binary.BigEndian, groupTag) if err != nil { return err } - err = rw.WriteVString(conn, outboundTag) + err = varbin.Write(conn, binary.BigEndian, outboundTag) if err != nil { return err } @@ -31,11 +31,11 @@ func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) erro } func (s *CommandServer) handleSelectOutbound(conn net.Conn) error { - groupTag, err := rw.ReadVString(conn) + groupTag, err := varbin.ReadValue[string](conn, binary.BigEndian) if err != nil { return err } - outboundTag, err := rw.ReadVString(conn) + outboundTag, err := varbin.ReadValue[string](conn, binary.BigEndian) if err != nil { return err } diff --git a/experimental/libbox/command_server.go b/experimental/libbox/command_server.go index 8918756dae..f913191d6a 100644 --- a/experimental/libbox/command_server.go +++ b/experimental/libbox/command_server.go @@ -66,14 +66,6 @@ func (s *CommandServer) SetService(newService *BoxService) { s.notifyURLTestUpdate() } -func (s *CommandServer) ResetLog() { - s.savedLines.Init() - select { - case s.logReset <- struct{}{}: - default: - } -} - func (s *CommandServer) notifyURLTestUpdate() { select { case s.urlTestUpdate <- struct{}{}: diff --git a/experimental/libbox/command_shared.go b/experimental/libbox/command_shared.go index ecad78dd8a..b98c2e5d3c 100644 --- a/experimental/libbox/command_shared.go +++ b/experimental/libbox/command_shared.go @@ -5,7 +5,7 @@ import ( "io" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" ) func readError(reader io.Reader) error { @@ -15,7 +15,7 @@ func readError(reader io.Reader) error { return err } if hasError { - errorMessage, err := rw.ReadVString(reader) + errorMessage, err := varbin.ReadValue[string](reader, binary.BigEndian) if err != nil { return err } @@ -30,7 +30,7 @@ func writeError(writer io.Writer, wErr error) error { return err } if wErr != nil { - err = rw.WriteVString(writer, wErr.Error()) + err = varbin.Write(writer, binary.BigEndian, wErr.Error()) if err != nil { return err } diff --git a/experimental/libbox/command_urltest.go b/experimental/libbox/command_urltest.go index 19ddf3da3c..6feda3f8ef 100644 --- a/experimental/libbox/command_urltest.go +++ b/experimental/libbox/command_urltest.go @@ -11,7 +11,7 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/batch" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" "github.com/sagernet/sing/service" ) @@ -25,7 +25,7 @@ func (c *CommandClient) URLTest(groupTag string) error { if err != nil { return err } - err = rw.WriteVString(conn, groupTag) + err = varbin.Write(conn, binary.BigEndian, groupTag) if err != nil { return err } @@ -33,7 +33,7 @@ func (c *CommandClient) URLTest(groupTag string) error { } func (s *CommandServer) handleURLTest(conn net.Conn) error { - groupTag, err := rw.ReadVString(conn) + groupTag, err := varbin.ReadValue[string](conn, binary.BigEndian) if err != nil { return err } diff --git a/experimental/libbox/profile_import.go b/experimental/libbox/profile_import.go index 75ddb06dcf..258c175a9f 100644 --- a/experimental/libbox/profile_import.go +++ b/experimental/libbox/profile_import.go @@ -1,13 +1,13 @@ package libbox import ( + "bufio" "bytes" "compress/gzip" "encoding/binary" - "io" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" ) func EncodeChunkedMessage(data []byte) []byte { @@ -35,13 +35,13 @@ type ErrorMessage struct { func (e *ErrorMessage) Encode() []byte { var buffer bytes.Buffer buffer.WriteByte(MessageTypeError) - rw.WriteVString(&buffer, e.Message) + varbin.Write(&buffer, binary.BigEndian, e.Message) return buffer.Bytes() } func DecodeErrorMessage(data []byte) (*ErrorMessage, error) { reader := bytes.NewReader(data) - messageType, err := rw.ReadByte(reader) + messageType, err := reader.ReadByte() if err != nil { return nil, err } @@ -49,7 +49,7 @@ func DecodeErrorMessage(data []byte) (*ErrorMessage, error) { return nil, E.New("invalid message") } var message ErrorMessage - message.Message, err = rw.ReadVString(reader) + message.Message, err = varbin.ReadValue[string](reader, binary.BigEndian) if err != nil { return nil, err } @@ -87,7 +87,7 @@ func (e *ProfileEncoder) Encode() []byte { binary.Write(&buffer, binary.BigEndian, uint16(len(e.profiles))) for _, preview := range e.profiles { binary.Write(&buffer, binary.BigEndian, preview.ProfileID) - rw.WriteVString(&buffer, preview.Name) + varbin.Write(&buffer, binary.BigEndian, preview.Name) binary.Write(&buffer, binary.BigEndian, preview.Type) } return buffer.Bytes() @@ -117,7 +117,7 @@ func (d *ProfileDecoder) Decode(data []byte) error { if err != nil { return err } - profile.Name, err = rw.ReadVString(reader) + profile.Name, err = varbin.ReadValue[string](reader, binary.BigEndian) if err != nil { return err } @@ -147,7 +147,7 @@ func (r *ProfileContentRequest) Encode() []byte { func DecodeProfileContentRequest(data []byte) (*ProfileContentRequest, error) { reader := bytes.NewReader(data) - messageType, err := rw.ReadByte(reader) + messageType, err := reader.ReadByte() if err != nil { return nil, err } @@ -176,12 +176,13 @@ func (c *ProfileContent) Encode() []byte { buffer := new(bytes.Buffer) buffer.WriteByte(MessageTypeProfileContent) buffer.WriteByte(1) - writer := gzip.NewWriter(buffer) - rw.WriteVString(writer, c.Name) + gWriter := gzip.NewWriter(buffer) + writer := bufio.NewWriter(gWriter) + varbin.Write(writer, binary.BigEndian, c.Name) binary.Write(writer, binary.BigEndian, c.Type) - rw.WriteVString(writer, c.Config) + varbin.Write(writer, binary.BigEndian, c.Config) if c.Type != ProfileTypeLocal { - rw.WriteVString(writer, c.RemotePath) + varbin.Write(writer, binary.BigEndian, c.RemotePath) } if c.Type == ProfileTypeRemote { binary.Write(writer, binary.BigEndian, c.AutoUpdate) @@ -189,29 +190,31 @@ func (c *ProfileContent) Encode() []byte { binary.Write(writer, binary.BigEndian, c.LastUpdated) } writer.Flush() - writer.Close() + gWriter.Flush() + gWriter.Close() return buffer.Bytes() } func DecodeProfileContent(data []byte) (*ProfileContent, error) { - var reader io.Reader = bytes.NewReader(data) - messageType, err := rw.ReadByte(reader) + reader := bytes.NewReader(data) + messageType, err := reader.ReadByte() if err != nil { return nil, err } if messageType != MessageTypeProfileContent { return nil, E.New("invalid message") } - version, err := rw.ReadByte(reader) + version, err := reader.ReadByte() if err != nil { return nil, err } - reader, err = gzip.NewReader(reader) + gReader, err := gzip.NewReader(reader) if err != nil { return nil, E.Cause(err, "unsupported profile") } + bReader := varbin.StubReader(gReader) var content ProfileContent - content.Name, err = rw.ReadVString(reader) + content.Name, err = varbin.ReadValue[string](bReader, binary.BigEndian) if err != nil { return nil, err } @@ -219,12 +222,12 @@ func DecodeProfileContent(data []byte) (*ProfileContent, error) { if err != nil { return nil, err } - content.Config, err = rw.ReadVString(reader) + content.Config, err = varbin.ReadValue[string](bReader, binary.BigEndian) if err != nil { return nil, err } if content.Type != ProfileTypeLocal { - content.RemotePath, err = rw.ReadVString(reader) + content.RemotePath, err = varbin.ReadValue[string](bReader, binary.BigEndian) if err != nil { return nil, err } diff --git a/experimental/libbox/setup.go b/experimental/libbox/setup.go index 31611354bf..ac67db38f2 100644 --- a/experimental/libbox/setup.go +++ b/experimental/libbox/setup.go @@ -3,6 +3,7 @@ package libbox import ( "os" "os/user" + "runtime/debug" "strconv" "time" @@ -21,6 +22,11 @@ var ( sTVOS bool ) +func init() { + debug.SetPanicOnFault(true) + debug.SetTraceback("all") +} + func Setup(basePath string, workingPath string, tempPath string, isTVOS bool) { sBasePath = basePath sWorkingPath = workingPath diff --git a/go.mod b/go.mod index d9193ba163..7a9239dc96 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f github.com/sagernet/quic-go v0.46.0-beta.4 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 - github.com/sagernet/sing v0.4.2 + github.com/sagernet/sing v0.5.0-beta.1 github.com/sagernet/sing-dns v0.2.3 github.com/sagernet/sing-mux v0.2.0 github.com/sagernet/sing-quic v0.2.2 diff --git a/go.sum b/go.sum index 40f303fa75..f9a7fd8d73 100644 --- a/go.sum +++ b/go.sum @@ -108,8 +108,8 @@ github.com/sagernet/quic-go v0.46.0-beta.4/go.mod h1:zJmVdJUNqEDXfubf4KtIOUHHerg 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.4.2 h1:jzGNJdZVRI0xlAfFugsIQUPvyB9SuWvbJK7zQCXc4QM= -github.com/sagernet/sing v0.4.2/go.mod h1:ieZHA/+Y9YZfXs2I3WtuwgyCZ6GPsIR7HdKb1SdEnls= +github.com/sagernet/sing v0.5.0-beta.1 h1:THZMZgJcDQxutE++6Ckih1HlvMtXple94RBGa6GSg2I= +github.com/sagernet/sing v0.5.0-beta.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-dns v0.2.3 h1:YzeBUn2tR38F7HtvGEQ0kLRLmZWMEgi/+7wqa4Twb1k= github.com/sagernet/sing-dns v0.2.3/go.mod h1:BJpJv6XLnrUbSyIntOT6DG9FW0f4fETmPAHvNjOprLg= github.com/sagernet/sing-mux v0.2.0 h1:4C+vd8HztJCWNYfufvgL49xaOoOHXty2+EAjnzN3IYo= diff --git a/inbound/mixed.go b/inbound/mixed.go index 982842efeb..3933f7af05 100644 --- a/inbound/mixed.go +++ b/inbound/mixed.go @@ -12,10 +12,7 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/auth" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/protocol/http" "github.com/sagernet/sing/protocol/socks" "github.com/sagernet/sing/protocol/socks/socks4" @@ -51,16 +48,17 @@ func NewMixed(ctx context.Context, router adapter.Router, logger log.ContextLogg } func (h *Mixed) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - headerType, err := rw.ReadByte(conn) + reader := std_bufio.NewReader(conn) + headerBytes, err := reader.Peek(1) if err != nil { return err } - switch headerType { + switch headerBytes[0] { case socks4.Version, socks5.Version: - return socks.HandleConnection0(ctx, conn, headerType, h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata)) + return socks.HandleConnection0(ctx, conn, reader, h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata)) + default: + return http.HandleConnection(ctx, conn, reader, h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata)) } - reader := std_bufio.NewReader(bufio.NewCachedReader(conn, buf.As([]byte{headerType}))) - return http.HandleConnection(ctx, conn, reader, h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata)) } func (h *Mixed) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { diff --git a/inbound/vless.go b/inbound/vless.go index 69ed042bc3..567475647a 100644 --- a/inbound/vless.go +++ b/inbound/vless.go @@ -83,12 +83,11 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg } func (h *VLESS) Start() error { - err := common.Start( - h.service, - h.tlsConfig, - ) - if err != nil { - return err + if h.tlsConfig != nil { + err := h.tlsConfig.Start() + if err != nil { + return err + } } if h.transport == nil { return h.myInboundAdapter.Start() diff --git a/inbound/vmess.go b/inbound/vmess.go index 70676bbd8c..154512751c 100644 --- a/inbound/vmess.go +++ b/inbound/vmess.go @@ -93,13 +93,16 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg } func (h *VMess) Start() error { - err := common.Start( - h.service, - h.tlsConfig, - ) + err := h.service.Start() if err != nil { return err } + if h.tlsConfig != nil { + err = h.tlsConfig.Start() + if err != nil { + return err + } + } if h.transport == nil { return h.myInboundAdapter.Start() } diff --git a/option/outbound.go b/option/outbound.go index 59ee85ab69..6c943cd9af 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -113,7 +113,7 @@ type DialerOptions struct { Inet4BindAddress *ListenAddress `json:"inet4_bind_address,omitempty"` Inet6BindAddress *ListenAddress `json:"inet6_bind_address,omitempty"` ProtectPath string `json:"protect_path,omitempty"` - RoutingMark int `json:"routing_mark,omitempty"` + RoutingMark uint32 `json:"routing_mark,omitempty"` ReuseAddr bool `json:"reuse_addr,omitempty"` ConnectTimeout Duration `json:"connect_timeout,omitempty"` TCPFastOpen bool `json:"tcp_fast_open,omitempty"` diff --git a/option/route.go b/option/route.go index e313fcf242..dfd72986ff 100644 --- a/option/route.go +++ b/option/route.go @@ -10,7 +10,7 @@ type RouteOptions struct { AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"` DefaultInterface string `json:"default_interface,omitempty"` - DefaultMark int `json:"default_mark,omitempty"` + DefaultMark uint32 `json:"default_mark,omitempty"` } type GeoIPOptions struct { diff --git a/outbound/proxy.go b/outbound/proxy.go index 6127f0f215..fbc4848180 100644 --- a/outbound/proxy.go +++ b/outbound/proxy.go @@ -1,7 +1,6 @@ package outbound import ( - std_bufio "bufio" "context" "crypto/rand" "encoding/hex" @@ -11,16 +10,10 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" 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/rw" - "github.com/sagernet/sing/protocol/http" "github.com/sagernet/sing/protocol/socks" - "github.com/sagernet/sing/protocol/socks/socks4" - "github.com/sagernet/sing/protocol/socks/socks5" ) type ProxyListener struct { @@ -102,16 +95,7 @@ func (l *ProxyListener) acceptLoop() { } func (l *ProxyListener) accept(ctx context.Context, conn *net.TCPConn) error { - headerType, err := rw.ReadByte(conn) - if err != nil { - return err - } - switch headerType { - case socks4.Version, socks5.Version: - return socks.HandleConnection0(ctx, conn, headerType, l.authenticator, l, M.Metadata{}) - } - reader := std_bufio.NewReader(bufio.NewCachedReader(conn, buf.As([]byte{headerType}))) - return http.HandleConnection(ctx, conn, reader, l.authenticator, l, M.Metadata{}) + return socks.HandleConnection(ctx, conn, l.authenticator, l, M.Metadata{}) } func (l *ProxyListener) NewConnection(ctx context.Context, conn net.Conn, upstreamMetadata M.Metadata) error { diff --git a/outbound/tor.go b/outbound/tor.go index 76c7955da7..8ae73a66fa 100644 --- a/outbound/tor.go +++ b/outbound/tor.go @@ -44,10 +44,10 @@ func NewTor(ctx context.Context, router adapter.Router, logger log.ContextLogger startConf.ExtraArgs = options.ExtraArgs if options.DataDirectory != "" { dataDirAbs, _ := filepath.Abs(startConf.DataDir) - if geoIPPath := filepath.Join(dataDirAbs, "geoip"); rw.FileExists(geoIPPath) && !common.Contains(options.ExtraArgs, "--GeoIPFile") { + if geoIPPath := filepath.Join(dataDirAbs, "geoip"); rw.IsFile(geoIPPath) && !common.Contains(options.ExtraArgs, "--GeoIPFile") { options.ExtraArgs = append(options.ExtraArgs, "--GeoIPFile", geoIPPath) } - if geoIP6Path := filepath.Join(dataDirAbs, "geoip6"); rw.FileExists(geoIP6Path) && !common.Contains(options.ExtraArgs, "--GeoIPv6File") { + if geoIP6Path := filepath.Join(dataDirAbs, "geoip6"); rw.IsFile(geoIP6Path) && !common.Contains(options.ExtraArgs, "--GeoIPv6File") { options.ExtraArgs = append(options.ExtraArgs, "--GeoIPv6File", geoIP6Path) } } @@ -58,8 +58,12 @@ func NewTor(ctx context.Context, router adapter.Router, logger log.ContextLogger } if startConf.DataDir != "" { torrcFile := filepath.Join(startConf.DataDir, "torrc") - if !rw.FileExists(torrcFile) { - err := rw.WriteFile(torrcFile, []byte("")) + err := rw.MkdirParent(torrcFile) + if err != nil { + return nil, err + } + if !rw.IsFile(torrcFile) { + err := os.WriteFile(torrcFile, []byte(""), 0o600) if err != nil { return nil, err } diff --git a/route/router.go b/route/router.go index 8b51b8593c..022a104606 100644 --- a/route/router.go +++ b/route/router.go @@ -82,7 +82,7 @@ type Router struct { interfaceFinder *control.DefaultInterfaceFinder autoDetectInterface bool defaultInterface string - defaultMark int + defaultMark uint32 networkMonitor tun.NetworkUpdateMonitor interfaceMonitor tun.DefaultInterfaceMonitor packageManager tun.PackageManager @@ -1171,7 +1171,7 @@ func (r *Router) DefaultInterface() string { return r.defaultInterface } -func (r *Router) DefaultMark() int { +func (r *Router) DefaultMark() uint32 { return r.defaultMark } diff --git a/route/router_geo_resources.go b/route/router_geo_resources.go index e0a572c92f..14364d210d 100644 --- a/route/router_geo_resources.go +++ b/route/router_geo_resources.go @@ -50,7 +50,7 @@ func (r *Router) prepareGeoIPDatabase() error { geoPath = foundPath } } - if !rw.FileExists(geoPath) { + if !rw.IsFile(geoPath) { geoPath = filemanager.BasePath(r.ctx, geoPath) } if stat, err := os.Stat(geoPath); err == nil { @@ -61,7 +61,7 @@ func (r *Router) prepareGeoIPDatabase() error { os.Remove(geoPath) } } - if !rw.FileExists(geoPath) { + if !rw.IsFile(geoPath) { r.logger.Warn("geoip database not exists: ", geoPath) var err error for attempts := 0; attempts < 3; attempts++ { @@ -96,7 +96,7 @@ func (r *Router) prepareGeositeDatabase() error { geoPath = foundPath } } - if !rw.FileExists(geoPath) { + if !rw.IsFile(geoPath) { geoPath = filemanager.BasePath(r.ctx, geoPath) } if stat, err := os.Stat(geoPath); err == nil { @@ -107,7 +107,7 @@ func (r *Router) prepareGeositeDatabase() error { os.Remove(geoPath) } } - if !rw.FileExists(geoPath) { + if !rw.IsFile(geoPath) { r.logger.Warn("geosite database not exists: ", geoPath) var err error for attempts := 0; attempts < 3; attempts++ { diff --git a/route/rule_abstract.go b/route/rule_abstract.go index c13bdd8d96..9ef2e93277 100644 --- a/route/rule_abstract.go +++ b/route/rule_abstract.go @@ -29,9 +29,13 @@ func (r *abstractDefaultRule) Type() string { func (r *abstractDefaultRule) Start() error { for _, item := range r.allItems { - err := common.Start(item) - if err != nil { - return err + if starter, isStarter := item.(interface { + Start() error + }); isStarter { + err := starter.Start() + if err != nil { + return err + } } } return nil @@ -183,8 +187,13 @@ func (r *abstractLogicalRule) UpdateGeosite() error { } func (r *abstractLogicalRule) Start() error { - for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (common.Starter, bool) { - rule, loaded := it.(common.Starter) + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (interface { + Start() error + }, bool, + ) { + rule, loaded := it.(interface { + Start() error + }) return rule, loaded }) { err := rule.Start() diff --git a/transport/trojan/mux.go b/transport/trojan/mux.go index 13ac1e83a9..b1cc9985c9 100644 --- a/transport/trojan/mux.go +++ b/transport/trojan/mux.go @@ -1,12 +1,14 @@ package trojan import ( + std_bufio "bufio" "context" "net" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" - "github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/task" "github.com/sagernet/smux" ) @@ -33,27 +35,36 @@ func HandleMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata return group.Run(ctx) } -func newMuxConnection(ctx context.Context, stream net.Conn, metadata M.Metadata, handler Handler) { - err := newMuxConnection0(ctx, stream, metadata, handler) +func newMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata, handler Handler) { + err := newMuxConnection0(ctx, conn, metadata, handler) if err != nil { handler.NewError(ctx, E.Cause(err, "process trojan-go multiplex connection")) } } -func newMuxConnection0(ctx context.Context, stream net.Conn, metadata M.Metadata, handler Handler) error { - command, err := rw.ReadByte(stream) +func newMuxConnection0(ctx context.Context, conn net.Conn, metadata M.Metadata, handler Handler) error { + reader := std_bufio.NewReader(conn) + command, err := reader.ReadByte() if err != nil { return E.Cause(err, "read command") } - metadata.Destination, err = M.SocksaddrSerializer.ReadAddrPort(stream) + metadata.Destination, err = M.SocksaddrSerializer.ReadAddrPort(reader) if err != nil { return E.Cause(err, "read destination") } + if reader.Buffered() > 0 { + buffer := buf.NewSize(reader.Buffered()) + _, err = buffer.ReadFullFrom(reader, buffer.Len()) + if err != nil { + return err + } + conn = bufio.NewCachedConn(conn, buffer) + } switch command { case CommandTCP: - return handler.NewConnection(ctx, stream, metadata) + return handler.NewConnection(ctx, conn, metadata) case CommandUDP: - return handler.NewPacketConnection(ctx, &PacketConn{Conn: stream}, metadata) + return handler.NewPacketConnection(ctx, &PacketConn{Conn: conn}, metadata) default: return E.New("unknown command ", command) } diff --git a/transport/trojan/service.go b/transport/trojan/service.go index 9078276c73..97f674ab93 100644 --- a/transport/trojan/service.go +++ b/transport/trojan/service.go @@ -2,6 +2,7 @@ package trojan import ( "context" + "encoding/binary" "net" "github.com/sagernet/sing/common/auth" @@ -76,7 +77,8 @@ func (s *Service[K]) NewConnection(ctx context.Context, conn net.Conn, metadata return E.Cause(err, "skip crlf") } - command, err := rw.ReadByte(conn) + var command byte + err = binary.Read(conn, binary.BigEndian, &command) if err != nil { return E.Cause(err, "read command") } diff --git a/transport/v2raygrpc/conn.go b/transport/v2raygrpc/conn.go index 0fecbf33d3..bc78f91e3c 100644 --- a/transport/v2raygrpc/conn.go +++ b/transport/v2raygrpc/conn.go @@ -8,7 +8,7 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/baderror" M "github.com/sagernet/sing/common/metadata" - "github.com/sagernet/sing/common/rw" + N "github.com/sagernet/sing/common/network" ) var _ net.Conn = (*GRPCConn)(nil) @@ -90,7 +90,7 @@ func (c *GRPCConn) Upstream() any { return c.GunService } -var _ rw.WriteCloser = (*clientConnWrapper)(nil) +var _ N.WriteCloser = (*clientConnWrapper)(nil) type clientConnWrapper struct { GunService_TunClient diff --git a/transport/v2raygrpclite/conn.go b/transport/v2raygrpclite/conn.go index f5a71939d3..5ab02569fd 100644 --- a/transport/v2raygrpclite/conn.go +++ b/transport/v2raygrpclite/conn.go @@ -13,7 +13,7 @@ import ( "github.com/sagernet/sing/common/baderror" "github.com/sagernet/sing/common/buf" M "github.com/sagernet/sing/common/metadata" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" ) // kanged from: https://github.com/Qv2ray/gun-lite @@ -96,7 +96,7 @@ func (c *GunConn) read(b []byte) (n int, err error) { } func (c *GunConn) Write(b []byte) (n int, err error) { - varLen := rw.UVariantLen(uint64(len(b))) + varLen := varbin.UvarintLen(uint64(len(b))) buffer := buf.NewSize(6 + varLen + len(b)) header := buffer.Extend(6 + varLen) header[0] = 0x00 @@ -117,13 +117,13 @@ func (c *GunConn) Write(b []byte) (n int, err error) { func (c *GunConn) WriteBuffer(buffer *buf.Buffer) error { defer buffer.Release() dataLen := buffer.Len() - varLen := rw.UVariantLen(uint64(dataLen)) + varLen := varbin.UvarintLen(uint64(dataLen)) header := buffer.ExtendHeader(6 + varLen) header[0] = 0x00 binary.BigEndian.PutUint32(header[1:5], uint32(1+varLen+dataLen)) header[5] = 0x0A binary.PutUvarint(header[6:], uint64(dataLen)) - err := rw.WriteBytes(c.writer, buffer.Bytes()) + err := common.Error(c.writer.Write(buffer.Bytes())) if err != nil { return baderror.WrapH2(err) } From defa521c89510af70321bdded89c59a84820e210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 19 Aug 2024 07:34:11 +0800 Subject: [PATCH 10/31] Implement read deadline for QUIC based UDP inbounds --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7a9239dc96..1db4dac454 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/sagernet/sing v0.5.0-beta.1 github.com/sagernet/sing-dns v0.2.3 github.com/sagernet/sing-mux v0.2.0 - github.com/sagernet/sing-quic v0.2.2 + github.com/sagernet/sing-quic v0.3.0-beta.2 github.com/sagernet/sing-shadowsocks v0.2.7 github.com/sagernet/sing-shadowsocks2 v0.2.0 github.com/sagernet/sing-shadowtls v0.1.4 diff --git a/go.sum b/go.sum index f9a7fd8d73..d45616e855 100644 --- a/go.sum +++ b/go.sum @@ -114,8 +114,8 @@ github.com/sagernet/sing-dns v0.2.3 h1:YzeBUn2tR38F7HtvGEQ0kLRLmZWMEgi/+7wqa4Twb github.com/sagernet/sing-dns v0.2.3/go.mod h1:BJpJv6XLnrUbSyIntOT6DG9FW0f4fETmPAHvNjOprLg= github.com/sagernet/sing-mux v0.2.0 h1:4C+vd8HztJCWNYfufvgL49xaOoOHXty2+EAjnzN3IYo= github.com/sagernet/sing-mux v0.2.0/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ= -github.com/sagernet/sing-quic v0.2.2 h1:Ryp02zMhHh/ZDrG7MdLsmhuBU8+BEpOdJonFQiqIopo= -github.com/sagernet/sing-quic v0.2.2/go.mod h1:YLV1dUDv8Eyp/8e55O/EvfsrwxOgEDVgDCIoPqmDREE= +github.com/sagernet/sing-quic v0.3.0-beta.2 h1:9TiaW4js4fXD6GPCGMbwb3/bIRKpXm7skJBdV1OdvMs= +github.com/sagernet/sing-quic v0.3.0-beta.2/go.mod h1:YLV1dUDv8Eyp/8e55O/EvfsrwxOgEDVgDCIoPqmDREE= 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 6c96e8170717b6b2741c553e1a40dc702eb01446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 17 Jul 2024 17:57:35 +0800 Subject: [PATCH 11/31] Bump rule-set version --- cmd/sing-box/cmd_geoip_export.go | 2 +- cmd/sing-box/cmd_geosite_export.go | 2 +- cmd/sing-box/cmd_rule_set_compile.go | 6 +- cmd/sing-box/cmd_rule_set_upgrade.go | 91 ++++++++++++++++++++ common/srs/binary.go | 38 ++++---- constant/rule.go | 6 +- docs/configuration/rule-set/source-format.md | 21 ++++- option/rule_set.go | 6 +- route/rule_item_domain.go | 2 +- 9 files changed, 145 insertions(+), 29 deletions(-) create mode 100644 cmd/sing-box/cmd_rule_set_upgrade.go diff --git a/cmd/sing-box/cmd_geoip_export.go b/cmd/sing-box/cmd_geoip_export.go index 5787d2e5ad..b80e5cd3d0 100644 --- a/cmd/sing-box/cmd_geoip_export.go +++ b/cmd/sing-box/cmd_geoip_export.go @@ -87,7 +87,7 @@ func geoipExport(countryCode string) error { headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String()) } var plainRuleSet option.PlainRuleSetCompat - plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Version = C.RuleSetVersion2 plainRuleSet.Options.Rules = []option.HeadlessRule{ { Type: C.RuleTypeDefault, diff --git a/cmd/sing-box/cmd_geosite_export.go b/cmd/sing-box/cmd_geosite_export.go index 2a6c27a0ed..90a7955b99 100644 --- a/cmd/sing-box/cmd_geosite_export.go +++ b/cmd/sing-box/cmd_geosite_export.go @@ -70,7 +70,7 @@ func geositeExport(category string) error { headlessRule.DomainKeyword = defaultRule.DomainKeyword headlessRule.DomainRegex = defaultRule.DomainRegex var plainRuleSet option.PlainRuleSetCompat - plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Version = C.RuleSetVersion2 plainRuleSet.Options.Rules = []option.HeadlessRule{ { Type: C.RuleTypeDefault, diff --git a/cmd/sing-box/cmd_rule_set_compile.go b/cmd/sing-box/cmd_rule_set_compile.go index 6e065101a8..7e3753c928 100644 --- a/cmd/sing-box/cmd_rule_set_compile.go +++ b/cmd/sing-box/cmd_rule_set_compile.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/sagernet/sing-box/common/srs" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/json" @@ -55,9 +56,6 @@ func compileRuleSet(sourcePath string) error { if err != nil { return err } - if err != nil { - return err - } ruleSet := plainRuleSet.Upgrade() var outputPath string if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput { @@ -73,7 +71,7 @@ func compileRuleSet(sourcePath string) error { if err != nil { return err } - err = srs.Write(outputFile, ruleSet) + err = srs.Write(outputFile, ruleSet, plainRuleSet.Version == C.RuleSetVersion2) if err != nil { outputFile.Close() os.Remove(outputPath) diff --git a/cmd/sing-box/cmd_rule_set_upgrade.go b/cmd/sing-box/cmd_rule_set_upgrade.go new file mode 100644 index 0000000000..0ec039fd71 --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_upgrade.go @@ -0,0 +1,91 @@ +package main + +import ( + "bytes" + "io" + "os" + "path/filepath" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + + "github.com/spf13/cobra" +) + +var commandRuleSetUpgradeFlagWrite bool + +var commandRuleSetUpgrade = &cobra.Command{ + Use: "upgrade ", + Short: "Upgrade rule-set json", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := upgradeRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSetUpgrade.Flags().BoolVarP(&commandRuleSetUpgradeFlagWrite, "write", "w", false, "write result to (source) file instead of stdout") + commandRuleSet.AddCommand(commandRuleSetUpgrade) +} + +func upgradeRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + content, err := io.ReadAll(reader) + if err != nil { + return err + } + plainRuleSetCompat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content) + if err != nil { + return err + } + switch plainRuleSetCompat.Version { + case C.RuleSetVersion1: + default: + log.Info("already up-to-date") + return nil + } + plainRuleSet := plainRuleSetCompat.Upgrade() + buffer := new(bytes.Buffer) + encoder := json.NewEncoder(buffer) + encoder.SetIndent("", " ") + err = encoder.Encode(plainRuleSet) + if err != nil { + return E.Cause(err, "encode config") + } + outputPath, _ := filepath.Abs(sourcePath) + if !commandRuleSetUpgradeFlagWrite || sourcePath == "stdin" { + os.Stdout.WriteString(buffer.String() + "\n") + return nil + } + if bytes.Equal(content, buffer.Bytes()) { + return nil + } + output, err := os.Create(sourcePath) + if err != nil { + return E.Cause(err, "open output") + } + _, err = output.Write(buffer.Bytes()) + output.Close() + if err != nil { + return E.Cause(err, "write output") + } + os.Stderr.WriteString(outputPath + "\n") + return nil +} diff --git a/common/srs/binary.go b/common/srs/binary.go index c7c55e0838..0bd5a9099b 100644 --- a/common/srs/binary.go +++ b/common/srs/binary.go @@ -54,14 +54,14 @@ func Read(reader io.Reader, recover bool) (ruleSet option.PlainRuleSet, err erro if err != nil { return ruleSet, err } - if version != 1 { + if version > C.RuleSetVersion2 { return ruleSet, E.New("unsupported version: ", version) } - zReader, err := zlib.NewReader(reader) + compressReader, err := zlib.NewReader(reader) if err != nil { return } - bReader := bufio.NewReader(zReader) + bReader := bufio.NewReader(compressReader) length, err := binary.ReadUvarint(bReader) if err != nil { return @@ -77,26 +77,32 @@ func Read(reader io.Reader, recover bool) (ruleSet option.PlainRuleSet, err erro return } -func Write(writer io.Writer, ruleSet option.PlainRuleSet) error { +func Write(writer io.Writer, ruleSet option.PlainRuleSet, generateUnstable bool) error { _, err := writer.Write(MagicBytes[:]) if err != nil { return err } - err = binary.Write(writer, binary.BigEndian, uint8(1)) + var version uint8 + if generateUnstable { + version = C.RuleSetVersion2 + } else { + version = C.RuleSetVersion1 + } + err = binary.Write(writer, binary.BigEndian, version) if err != nil { return err } - zWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression) + compressWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression) if err != nil { return err } - bWriter := bufio.NewWriter(zWriter) + bWriter := bufio.NewWriter(compressWriter) _, err = varbin.WriteUvarint(bWriter, uint64(len(ruleSet.Rules))) if err != nil { return err } for _, rule := range ruleSet.Rules { - err = writeRule(bWriter, rule) + err = writeRule(bWriter, rule, generateUnstable) if err != nil { return err } @@ -105,7 +111,7 @@ func Write(writer io.Writer, ruleSet option.PlainRuleSet) error { if err != nil { return err } - return zWriter.Close() + return compressWriter.Close() } func readRule(reader varbin.Reader, recover bool) (rule option.HeadlessRule, err error) { @@ -127,12 +133,12 @@ func readRule(reader varbin.Reader, recover bool) (rule option.HeadlessRule, err return } -func writeRule(writer varbin.Writer, rule option.HeadlessRule) error { +func writeRule(writer varbin.Writer, rule option.HeadlessRule, generateUnstable bool) error { switch rule.Type { case C.RuleTypeDefault: - return writeDefaultRule(writer, rule.DefaultOptions) + return writeDefaultRule(writer, rule.DefaultOptions, generateUnstable) case C.RuleTypeLogical: - return writeLogicalRule(writer, rule.LogicalOptions) + return writeLogicalRule(writer, rule.LogicalOptions, generateUnstable) default: panic("unknown rule type: " + rule.Type) } @@ -219,7 +225,7 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea } } -func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule) error { +func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, generateUnstable bool) error { err := binary.Write(writer, binary.BigEndian, uint8(0)) if err != nil { return err @@ -243,7 +249,7 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule) err if err != nil { return err } - err = domain.NewMatcher(rule.Domain, rule.DomainSuffix).Write(writer) + err = domain.NewMatcher(rule.Domain, rule.DomainSuffix, !generateUnstable).Write(writer) if err != nil { return err } @@ -420,7 +426,7 @@ func readLogicalRule(reader varbin.Reader, recovery bool) (logicalRule option.Lo return } -func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRule) error { +func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRule, generateUnstable bool) error { err := binary.Write(writer, binary.BigEndian, uint8(1)) if err != nil { return err @@ -441,7 +447,7 @@ func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRu return err } for _, rule := range logicalRule.Rules { - err = writeRule(writer, rule) + err = writeRule(writer, rule, generateUnstable) if err != nil { return err } diff --git a/constant/rule.go b/constant/rule.go index 5a8eaf127f..fb0f39e22e 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -13,7 +13,11 @@ const ( const ( RuleSetTypeLocal = "local" RuleSetTypeRemote = "remote" - RuleSetVersion1 = 1 RuleSetFormatSource = "source" RuleSetFormatBinary = "binary" ) + +const ( + RuleSetVersion1 = 1 + iota + RuleSetVersion2 +) diff --git a/docs/configuration/rule-set/source-format.md b/docs/configuration/rule-set/source-format.md index ee5e48e04c..8c46cbf02b 100644 --- a/docs/configuration/rule-set/source-format.md +++ b/docs/configuration/rule-set/source-format.md @@ -1,12 +1,20 @@ +--- +icon: material/new-box +--- + # Source Format +!!! quote "Changes in sing-box 1.10.0" + + :material-plus: version `2` + !!! question "Since sing-box 1.8.0" ### Structure ```json { - "version": 1, + "version": 2, "rules": [] } ``` @@ -21,7 +29,16 @@ Use `sing-box rule-set compile [--output .srs] .json` to c ==Required== -Version of Rule Set, must be `1`. +Version of rule-set, one of `1` or `2`. + +* 1: Initial rule-set version, since sing-box 1.8.0. +* 2: Optimized memory usages of `domain_suffix` rules. + +The new rule-set version `2` does not make any changes to the format, only affecting `binary` rule-sets compiled by command `rule-set compile` + +Since 1.10.0, the optimization is always applied to `source` rule-sets even if version is set to `1`. + +It is recommended to upgrade to `2` after sing-box 1.10.0 becomes a stable version. #### rules diff --git a/option/rule_set.go b/option/rule_set.go index 8e367e69a5..5498400f2f 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -185,7 +185,7 @@ type PlainRuleSetCompat _PlainRuleSetCompat func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { var v any switch r.Version { - case C.RuleSetVersion1: + case C.RuleSetVersion1, C.RuleSetVersion2: v = r.Options default: return nil, E.New("unknown rule set version: ", r.Version) @@ -200,7 +200,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { } var v any switch r.Version { - case C.RuleSetVersion1: + case C.RuleSetVersion1, C.RuleSetVersion2: v = &r.Options case 0: return E.New("missing rule set version") @@ -217,7 +217,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { func (r PlainRuleSetCompat) Upgrade() PlainRuleSet { var result PlainRuleSet switch r.Version { - case C.RuleSetVersion1: + case C.RuleSetVersion1, C.RuleSetVersion2: result = r.Options default: panic("unknown rule set version: " + F.ToString(r.Version)) diff --git a/route/rule_item_domain.go b/route/rule_item_domain.go index 36839a5556..c77890df24 100644 --- a/route/rule_item_domain.go +++ b/route/rule_item_domain.go @@ -38,7 +38,7 @@ func NewDomainItem(domains []string, domainSuffixes []string) *DomainItem { } } return &DomainItem{ - domain.NewMatcher(domains, domainSuffixes), + domain.NewMatcher(domains, domainSuffixes, false), description, } } From b67eb68d854e200a87e5383e5005403ca0423cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 22 Jul 2024 12:10:22 +0800 Subject: [PATCH 12/31] Improve usages of `json.Unmarshal` --- option/json.go | 6 +----- option/types.go | 4 ++-- option/udp_over_tcp.go | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/option/json.go b/option/json.go index 07580e9f7b..775141d5aa 100644 --- a/option/json.go +++ b/option/json.go @@ -1,8 +1,6 @@ package option import ( - "bytes" - "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" @@ -69,7 +67,5 @@ func UnmarshallExcluded(inputContent []byte, parentObject any, object any) error if err != nil { return err } - decoder := json.NewDecoder(bytes.NewReader(inputContent)) - decoder.DisallowUnknownFields() - return decoder.Decode(object) + return json.UnmarshalDisallowUnknownFields(inputContent, object) } diff --git a/option/types.go b/option/types.go index 83fee8f05a..b17f222248 100644 --- a/option/types.go +++ b/option/types.go @@ -128,12 +128,12 @@ func (l Listable[T]) MarshalJSON() ([]byte, error) { } func (l *Listable[T]) UnmarshalJSON(content []byte) error { - err := json.Unmarshal(content, (*[]T)(l)) + err := json.UnmarshalDisallowUnknownFields(content, (*[]T)(l)) if err == nil { return nil } var singleItem T - newError := json.Unmarshal(content, &singleItem) + newError := json.UnmarshalDisallowUnknownFields(content, &singleItem) if newError != nil { return E.Errors(err, newError) } diff --git a/option/udp_over_tcp.go b/option/udp_over_tcp.go index e8a7a9726e..b496017a41 100644 --- a/option/udp_over_tcp.go +++ b/option/udp_over_tcp.go @@ -26,5 +26,5 @@ func (o *UDPOverTCPOptions) UnmarshalJSON(bytes []byte) error { if err == nil { return nil } - return json.Unmarshal(bytes, (*_UDPOverTCPOptions)(o)) + return json.UnmarshalDisallowUnknownFields(bytes, (*_UDPOverTCPOptions)(o)) } From 8593c8e3eb2153ba0b07a2c3296e9450990c9695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 3 Jul 2024 19:42:06 +0800 Subject: [PATCH 13/31] Add IP address support for `rule-set match` match --- cmd/sing-box/cmd_rule_set_match.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/cmd/sing-box/cmd_rule_set_match.go b/cmd/sing-box/cmd_rule_set_match.go index 08784caf11..fb2560afb1 100644 --- a/cmd/sing-box/cmd_rule_set_match.go +++ b/cmd/sing-box/cmd_rule_set_match.go @@ -14,6 +14,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" + M "github.com/sagernet/sing/common/metadata" "github.com/spf13/cobra" ) @@ -21,8 +22,8 @@ import ( var flagRuleSetMatchFormat string var commandRuleSetMatch = &cobra.Command{ - Use: "match ", - Short: "Check if a domain matches the rule set", + Use: "match ", + Short: "Check if an IP address or a domain matches the rule set", Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { err := ruleSetMatch(args[0], args[1]) @@ -71,15 +72,20 @@ func ruleSetMatch(sourcePath string, domain string) error { default: return E.New("unknown rule set format: ", flagRuleSetMatchFormat) } + ipAddress := M.ParseAddr(domain) + var metadata adapter.InboundContext + if ipAddress.IsValid() { + metadata.Destination = M.SocksaddrFrom(ipAddress, 0) + } else { + metadata.Domain = domain + } for i, ruleOptions := range plainRuleSet.Rules { var currentRule adapter.HeadlessRule currentRule, err = route.NewHeadlessRule(nil, ruleOptions) if err != nil { return E.Cause(err, "parse rule_set.rules.[", i, "]") } - if currentRule.Match(&adapter.InboundContext{ - Domain: domain, - }) { + if currentRule.Match(&metadata) { println(F.ToString("match rules.[", i, "]: ", currentRule)) } } From b253e2256e471c44f38b4ab922a27c099d42098e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 3 Jul 2024 19:42:21 +0800 Subject: [PATCH 14/31] Add `rule-set decompile` command --- cmd/sing-box/cmd_rule_set_decompile.go | 83 ++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 cmd/sing-box/cmd_rule_set_decompile.go diff --git a/cmd/sing-box/cmd_rule_set_decompile.go b/cmd/sing-box/cmd_rule_set_decompile.go new file mode 100644 index 0000000000..02af03dd67 --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_decompile.go @@ -0,0 +1,83 @@ +package main + +import ( + "io" + "os" + "strings" + + "github.com/sagernet/sing-box/common/srs" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/json" + + "github.com/spf13/cobra" +) + +var flagRuleSetDecompileOutput string + +const flagRuleSetDecompileDefaultOutput = ".json" + +var commandRuleSetDecompile = &cobra.Command{ + Use: "decompile [binary-path]", + Short: "Decompile rule-set binary to json", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := decompileRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSet.AddCommand(commandRuleSetDecompile) + commandRuleSetDecompile.Flags().StringVarP(&flagRuleSetDecompileOutput, "output", "o", flagRuleSetDecompileDefaultOutput, "Output file") +} + +func decompileRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + plainRuleSet, err := srs.Read(reader, true) + if err != nil { + return err + } + ruleSet := option.PlainRuleSetCompat{ + Version: C.RuleSetVersion1, + Options: plainRuleSet, + } + var outputPath string + if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput { + if strings.HasSuffix(sourcePath, ".srs") { + outputPath = sourcePath[:len(sourcePath)-4] + ".json" + } else { + outputPath = sourcePath + ".json" + } + } else { + outputPath = flagRuleSetDecompileOutput + } + outputFile, err := os.Create(outputPath) + if err != nil { + return err + } + encoder := json.NewEncoder(outputFile) + encoder.SetIndent("", " ") + err = encoder.Encode(ruleSet) + if err != nil { + outputFile.Close() + os.Remove(outputPath) + return err + } + outputFile.Close() + return nil +} From 8c60321002193b4b008aedd10b6bb53a0e0e5d64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 7 Jun 2024 15:55:21 +0800 Subject: [PATCH 15/31] Add auto-redirect & Improve auto-route --- Dockerfile | 2 +- adapter/router.go | 15 ++ box.go | 28 ++- cmd/sing-box/cmd_tools_fetch.go | 32 ++- cmd/sing-box/cmd_tools_fetch_http3.go | 36 +++ cmd/sing-box/cmd_tools_fetch_http3_stub.go | 18 ++ common/dialer/default.go | 18 +- docs/configuration/inbound/tun.md | 144 +++++++++++- docs/configuration/inbound/tun.zh.md | 147 ++++++++++++- docs/deprecated.md | 8 + docs/deprecated.zh.md | 8 + docs/migration.md | 68 ++++++ docs/migration.zh.md | 68 ++++++ go.mod | 19 +- go.sum | 40 ++-- inbound/tun.go | 243 ++++++++++++++++++--- option/tun.go | 63 ++++-- route/router.go | 34 ++- route/rule_item_rule_set.go | 1 + route/rule_set.go | 21 ++ route/rule_set_local.go | 59 ++++- route/rule_set_remote.go | 74 ++++++- 22 files changed, 1036 insertions(+), 110 deletions(-) create mode 100644 cmd/sing-box/cmd_tools_fetch_http3.go create mode 100644 cmd/sing-box/cmd_tools_fetch_http3_stub.go diff --git a/Dockerfile b/Dockerfile index af121d40b1..0b1ac735e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ FROM --platform=$TARGETPLATFORM alpine AS dist LABEL maintainer="nekohasekai " RUN set -ex \ && apk upgrade \ - && apk add bash tzdata ca-certificates \ + && apk add bash tzdata ca-certificates nftables \ && rm -rf /var/cache/apk/* COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box ENTRYPOINT ["sing-box"] diff --git a/adapter/router.go b/adapter/router.go index c481f0c8c9..619c1110cb 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -10,15 +10,18 @@ import ( "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" mdns "github.com/miekg/dns" + "go4.org/netipx" ) type Router interface { Service PreStarter PostStarter + Cleanup() error Outbounds() []Outbound Outbound(tag string) (Outbound, bool) @@ -46,6 +49,8 @@ type Router interface { AutoDetectInterface() bool AutoDetectInterfaceFunc() control.Func DefaultMark() uint32 + RegisterAutoRedirectOutputMark(mark uint32) error + AutoRedirectOutputMark() uint32 NetworkMonitor() tun.NetworkUpdateMonitor InterfaceMonitor() tun.DefaultInterfaceMonitor PackageManager() tun.PackageManager @@ -92,12 +97,22 @@ type DNSRule interface { } type RuleSet interface { + Name() string StartContext(ctx context.Context, startContext RuleSetStartContext) error + PostStart() error Metadata() RuleSetMetadata + ExtractIPSet() []*netipx.IPSet + IncRef() + DecRef() + Cleanup() + RegisterCallback(callback RuleSetUpdateCallback) *list.Element[RuleSetUpdateCallback] + UnregisterCallback(element *list.Element[RuleSetUpdateCallback]) Close() error HeadlessRule } +type RuleSetUpdateCallback func(it RuleSet) + type RuleSetMetadata struct { ContainsProcessRule bool ContainsWIFIRule bool diff --git a/box.go b/box.go index 3c514cfee2..716b1b093c 100644 --- a/box.go +++ b/box.go @@ -303,7 +303,11 @@ func (s *Box) start() error { return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]") } } - return s.postStart() + err = s.postStart() + if err != nil { + return err + } + return s.router.Cleanup() } func (s *Box) postStart() error { @@ -313,16 +317,28 @@ func (s *Box) postStart() error { return E.Cause(err, "start ", serviceName) } } - for _, outbound := range s.outbounds { - if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound { + // TODO: reorganize ALL start order + for _, out := range s.outbounds { + if lateOutbound, isLateOutbound := out.(adapter.PostStarter); isLateOutbound { err := lateOutbound.PostStart() if err != nil { - return E.Cause(err, "post-start outbound/", outbound.Tag()) + return E.Cause(err, "post-start outbound/", out.Tag()) } } } - - return s.router.PostStart() + err := s.router.PostStart() + if err != nil { + return err + } + for _, in := range s.inbounds { + if lateInbound, isLateInbound := in.(adapter.PostStarter); isLateInbound { + err = lateInbound.PostStart() + if err != nil { + return E.Cause(err, "post-start inbound/", in.Tag()) + } + } + } + return nil } func (s *Box) Close() error { diff --git a/cmd/sing-box/cmd_tools_fetch.go b/cmd/sing-box/cmd_tools_fetch.go index 256c3f42b6..3f62424a49 100644 --- a/cmd/sing-box/cmd_tools_fetch.go +++ b/cmd/sing-box/cmd_tools_fetch.go @@ -9,8 +9,10 @@ import ( "net/url" "os" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "github.com/spf13/cobra" @@ -32,7 +34,10 @@ func init() { commandTools.AddCommand(commandFetch) } -var httpClient *http.Client +var ( + httpClient *http.Client + http3Client *http.Client +) func fetch(args []string) error { instance, err := createPreStartedClient() @@ -53,8 +58,16 @@ func fetch(args []string) error { }, } defer httpClient.CloseIdleConnections() + if C.WithQUIC { + err = initializeHTTP3Client(instance) + if err != nil { + return err + } + defer http3Client.CloseIdleConnections() + } for _, urlString := range args { - parsedURL, err := url.Parse(urlString) + var parsedURL *url.URL + parsedURL, err = url.Parse(urlString) if err != nil { return err } @@ -63,16 +76,27 @@ func fetch(args []string) error { parsedURL.Scheme = "http" fallthrough case "http", "https": - err = fetchHTTP(parsedURL) + err = fetchHTTP(httpClient, parsedURL) + if err != nil { + return err + } + case "http3": + if !C.WithQUIC { + return C.ErrQUICNotIncluded + } + parsedURL.Scheme = "https" + err = fetchHTTP(http3Client, parsedURL) if err != nil { return err } + default: + return E.New("unsupported scheme: ", parsedURL.Scheme) } } return nil } -func fetchHTTP(parsedURL *url.URL) error { +func fetchHTTP(httpClient *http.Client, parsedURL *url.URL) error { request, err := http.NewRequest("GET", parsedURL.String(), nil) if err != nil { return err diff --git a/cmd/sing-box/cmd_tools_fetch_http3.go b/cmd/sing-box/cmd_tools_fetch_http3.go new file mode 100644 index 0000000000..5dc3d9157f --- /dev/null +++ b/cmd/sing-box/cmd_tools_fetch_http3.go @@ -0,0 +1,36 @@ +//go:build with_quic + +package main + +import ( + "context" + "crypto/tls" + "net/http" + + "github.com/sagernet/quic-go" + "github.com/sagernet/quic-go/http3" + box "github.com/sagernet/sing-box" + "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func initializeHTTP3Client(instance *box.Box) error { + dialer, err := createDialer(instance, N.NetworkUDP, commandToolsFlagOutbound) + if err != nil { + return err + } + http3Client = &http.Client{ + Transport: &http3.RoundTripper{ + Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + destination := M.ParseSocksaddr(addr) + udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination) + if dErr != nil { + return nil, dErr + } + return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsCfg, cfg) + }, + }, + } + return nil +} diff --git a/cmd/sing-box/cmd_tools_fetch_http3_stub.go b/cmd/sing-box/cmd_tools_fetch_http3_stub.go new file mode 100644 index 0000000000..ae13f54c42 --- /dev/null +++ b/cmd/sing-box/cmd_tools_fetch_http3_stub.go @@ -0,0 +1,18 @@ +//go:build !with_quic + +package main + +import ( + "net/url" + "os" + + box "github.com/sagernet/sing-box" +) + +func initializeHTTP3Client(instance *box.Box) error { + return os.ErrInvalid +} + +func fetchHTTP3(parsedURL *url.URL) error { + return os.ErrInvalid +} diff --git a/common/dialer/default.go b/common/dialer/default.go index 4fbad07deb..488e8000eb 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -50,12 +50,26 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) } - if options.RoutingMark != 0 { + var autoRedirectOutputMark uint32 + if router != nil { + autoRedirectOutputMark = router.AutoRedirectOutputMark() + } + if autoRedirectOutputMark > 0 { + dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark)) + listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark)) + } + if options.RoutingMark > 0 { dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark)) listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark)) - } else if router != nil && router.DefaultMark() != 0 { + if autoRedirectOutputMark > 0 { + return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `routing_mark`") + } + } else if router != nil && router.DefaultMark() > 0 { dialer.Control = control.Append(dialer.Control, control.RoutingMark(router.DefaultMark())) listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark())) + if autoRedirectOutputMark > 0 { + return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `default_mark`") + } } if options.ReuseAddr { listener.Control = control.Append(listener.Control, control.ReuseAddr()) diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index 1d5d8d0f65..1e2bf40058 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -2,6 +2,21 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.10.0" + + :material-plus: [address](#address) + :material-delete-clock: [inet4_address](#inet4_address) + :material-delete-clock: [inet6_address](#inet6_address) + :material-plus: [route_address](#route_address) + :material-delete-clock: [inet4_route_address](#inet4_route_address) + :material-delete-clock: [inet6_route_address](#inet6_route_address) + :material-plus: [route_exclude_address](#route_address) + :material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address) + :material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address) + :material-plus: [auto_redirect](#auto_redirect) + :material-plus: [route_address_set](#route_address_set) + :material-plus: [route_exclude_address_set](#route_address_set) + !!! quote "Changes in sing-box 1.9.0" :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain) @@ -23,26 +38,57 @@ icon: material/new-box "type": "tun", "tag": "tun-in", "interface_name": "tun0", - "inet4_address": "172.19.0.1/30", - "inet6_address": "fdfe:dcba:9876::1/126", + "address": [ + "172.18.0.1/30", + "fdfe:dcba:9876::1/126" + ], + // deprecated + "inet4_address": [ + "172.19.0.1/30" + ], + // deprecated + "inet6_address": [ + "fdfe:dcba:9876::1/126" + ], "mtu": 9000, "gso": false, "auto_route": true, "strict_route": true, + "auto_redirect": false, + "route_address": [ + "0.0.0.0/1", + "128.0.0.0/1", + "::/1", + "8000::/1" + ], + // deprecated "inet4_route_address": [ "0.0.0.0/1", "128.0.0.0/1" ], + // deprecated "inet6_route_address": [ "::/1", "8000::/1" ], + "route_exclude_address": [ + "192.168.0.0/16", + "fc00::/7" + ], + // deprecated "inet4_route_exclude_address": [ "192.168.0.0/16" ], + // deprecated "inet6_route_exclude_address": [ "fc00::/7" ], + "route_address_set": [ + "geoip-cloudflare" + ], + "route_exclude_address_set": [ + "geoip-cn" + ], "endpoint_independent_nat": false, "udp_timeout": "5m", "stack": "system", @@ -102,14 +148,26 @@ icon: material/new-box Virtual device name, automatically selected if empty. +#### address + +!!! question "Since sing-box 1.10.0" + +IPv4 and IPv6 prefix for the tun interface. + #### inet4_address -==Required== +!!! failure "Deprecated in sing-box 1.10.0" + + `inet4_address` is merged to `address` and will be removed in sing-box 1.11.0. IPv4 prefix for the tun interface. #### inet6_address +!!! failure "Deprecated in sing-box 1.10.0" + + `inet6_address` is merged to `address` and will be removed in sing-box 1.11.0. + IPv6 prefix for the tun interface. #### mtu @@ -145,9 +203,10 @@ Enforce strict routing rules when `auto_route` is enabled: *In Linux*: * Let unsupported network unreachable +* Make ICMP traffic route to tun instead of upstream interfaces * Route all connections to tun -It prevents address leaks and makes DNS hijacking work on Android. +It prevents IP address leaks and makes DNS hijacking work on Android. *In Windows*: @@ -156,22 +215,95 @@ It prevents address leaks and makes DNS hijacking work on Android. It may prevent some applications (such as VirtualBox) from working properly in certain situations. +#### auto_redirect + +!!! question "Since sing-box 1.10.0" + +!!! quote "" + + Only supported on Linux with `auto_route` enabled. + +Automatically configure iptables/nftables to redirect connections. + +*In Android*: + +Only local connections are forwarded. To share your VPN connection over hotspot or repeater, +use [VPNHotspot](https://github.com/Mygod/VPNHotspot). + +*In Linux*: + +`auto_route` with `auto_redirect` now works as expected on routers **without intervention**. + +#### route_address + +!!! question "Since sing-box 1.10.0" + +Use custom routes instead of default when `auto_route` is enabled. + #### inet4_route_address +!!! failure "Deprecated in sing-box 1.10.0" + + `inet4_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address) instead. + Use custom routes instead of default when `auto_route` is enabled. #### inet6_route_address +!!! failure "Deprecated in sing-box 1.10.0" + + `inet6_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address) instead. + Use custom routes instead of default when `auto_route` is enabled. +#### route_exclude_address + +!!! question "Since sing-box 1.10.0" + +Exclude custom routes when `auto_route` is enabled. + #### inet4_route_exclude_address +!!! failure "Deprecated in sing-box 1.10.0" + + `inet4_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_exclude_address](#route_exclude_address) instead. + Exclude custom routes when `auto_route` is enabled. #### inet6_route_exclude_address +!!! failure "Deprecated in sing-box 1.10.0" + + `inet6_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_exclude_address](#route_exclude_address) instead. + Exclude custom routes when `auto_route` is enabled. +#### route_address_set + +!!! question "Since sing-box 1.10.0" + +!!! quote "" + + Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled. + +Add the destination IP CIDR rules in the specified rule-sets to the firewall. +Unmatched traffic will bypass the sing-box routes. + +Conflict with `route.default_mark` and `[dialOptions].routing_mark`. + +#### route_exclude_address_set + +!!! question "Since sing-box 1.10.0" + +!!! quote "" + + Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled. + +Add the destination IP CIDR rules in the specified rule-sets to the firewall. +Matched traffic will bypass the sing-box routes. + +Conflict with `route.default_mark` and `[dialOptions].routing_mark`. + #### endpoint_independent_nat !!! info "" @@ -214,6 +346,10 @@ Conflict with `exclude_interface`. #### exclude_interface +!!! warning "" + + When `strict_route` enabled, return traffic to excluded interfaces will not be automatically excluded, so add them as well (example: `br-lan` and `pppoe-wan`). + Exclude interfaces in route. Conflict with `include_interface`. diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index 73d31d6497..5b1d35afb5 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -2,6 +2,21 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.10.0" + + :material-plus: [address](#address) + :material-delete-clock: [inet4_address](#inet4_address) + :material-delete-clock: [inet6_address](#inet6_address) + :material-plus: [route_address](#route_address) + :material-delete-clock: [inet4_route_address](#inet4_route_address) + :material-delete-clock: [inet6_route_address](#inet6_route_address) + :material-plus: [route_exclude_address](#route_address) + :material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address) + :material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address) + :material-plus: [auto_redirect](#auto_redirect) + :material-plus: [route_address_set](#route_address_set) + :material-plus: [route_exclude_address_set](#route_address_set) + !!! quote "sing-box 1.9.0 中的更改" :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain) @@ -23,26 +38,57 @@ icon: material/new-box "type": "tun", "tag": "tun-in", "interface_name": "tun0", - "inet4_address": "172.19.0.1/30", - "inet6_address": "fdfe:dcba:9876::1/126", + "address": [ + "172.18.0.1/30", + "fdfe:dcba:9876::1/126" + ], + // 已弃用 + "inet4_address": [ + "172.19.0.1/30" + ], + // 已弃用 + "inet6_address": [ + "fdfe:dcba:9876::1/126" + ], "mtu": 9000, "gso": false, "auto_route": true, "strict_route": true, + "auto_redirect": false, + "route_address": [ + "0.0.0.0/1", + "128.0.0.0/1", + "::/1", + "8000::/1" + ], + // 已弃用 "inet4_route_address": [ "0.0.0.0/1", "128.0.0.0/1" ], + // 已弃用 "inet6_route_address": [ "::/1", "8000::/1" ], + "route_exclude_address": [ + "192.168.0.0/16", + "fc00::/7" + ], + // 已弃用 "inet4_route_exclude_address": [ "192.168.0.0/16" ], + // 已弃用 "inet6_route_exclude_address": [ "fc00::/7" ], + "route_address_set": [ + "geoip-cloudflare" + ], + "route_exclude_address_set": [ + "geoip-cn" + ], "endpoint_independent_nat": false, "udp_timeout": "5m", "stack": "system", @@ -102,14 +148,30 @@ icon: material/new-box 虚拟设备名称,默认自动选择。 +#### address + +!!! question "自 sing-box 1.10.0 起" + +==必填== + +tun 接口的 IPv4 和 IPv6 前缀。 + #### inet4_address +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet4_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除. + ==必填== tun 接口的 IPv4 前缀。 #### inet6_address +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet6_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除. + tun 接口的 IPv6 前缀。 #### mtu @@ -145,9 +207,10 @@ tun 接口的 IPv6 前缀。 *在 Linux 中*: * 让不支持的网络无法到达 +* 使 ICMP 流量路由到 tun 而不是上游接口 * 将所有连接路由到 tun -它可以防止地址泄漏,并使 DNS 劫持在 Android 上工作。 +它可以防止 IP 地址泄漏,并使 DNS 劫持在 Android 上工作。 *在 Windows 中*: @@ -157,22 +220,94 @@ tun 接口的 IPv6 前缀。 它可能会使某些应用程序(如 VirtualBox)在某些情况下无法正常工作。 +#### auto_redirect + +!!! question "自 sing-box 1.10.0 起" + +!!! quote "" + + 仅支持 Linux。 + +自动配置 iptables 以重定向 TCP 连接。 + +*在 Android 中*: + +仅转发本地 IPv4 连接。 要通过热点或中继共享您的 VPN 连接,请使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot)。 + +*在 Linux 中*: + +带有 `auto_redirect `的 `auto_route` 现在可以在路由器上按预期工作,**无需干预**。 + +#### route_address + +!!! question "自 sing-box 1.10.0 起" + +设置到 Tun 的自定义路由。 + #### inet4_route_address +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet4_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除. + 启用 `auto_route` 时使用自定义路由而不是默认路由。 #### inet6_route_address +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet6_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除. + 启用 `auto_route` 时使用自定义路由而不是默认路由。 +#### route_exclude_address + +!!! question "自 sing-box 1.10.0 起" + +设置到 Tun 的排除自定义路由。 + #### inet4_route_exclude_address +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet4_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除. + 启用 `auto_route` 时排除自定义路由。 #### inet6_route_exclude_address +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet6_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除. + 启用 `auto_route` 时排除自定义路由。 +#### route_address_set + +!!! question "自 sing-box 1.10.0 起" + +!!! quote "" + + 仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。 + +将指定规则集中的目标 IP CIDR 规则添加到防火墙。 +不匹配的流量将绕过 sing-box 路由。 + +与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。 + +#### route_exclude_address_set + +!!! question "自 sing-box 1.10.0 起" + +!!! quote "" + + 仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。 + +将指定规则集中的目标 IP CIDR 规则添加到防火墙。 +匹配的流量将绕过 sing-box 路由。 + +与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。 + #### endpoint_independent_nat 启用独立于端点的 NAT。 @@ -211,6 +346,10 @@ TCP/IP 栈。 #### exclude_interface +!!! warning "" + + 当 `strict_route` 启用,到被排除接口的回程流量将不会被自动排除,因此也要添加它们(例:`br-lan` 与 `pppoe-wan`)。 + 排除路由的接口。 与 `include_interface` 冲突。 @@ -284,7 +423,7 @@ TCP/IP 栈。 !!! note "" - 在 Apple 平台,`bypass_domain` 项匹配主机名 **后缀**. + 在 Apple 平台,`bypass_domain` 项匹配主机名 **后缀**. 绕过代理的主机名列表。 diff --git a/docs/deprecated.md b/docs/deprecated.md index 439bf7e8f5..249bc492a2 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -6,6 +6,14 @@ icon: material/delete-alert ## 1.10.0 +#### TUN address fields are merged + +`inet4_address` and `inet6_address` are merged into `address`, +`inet4_route_address` and `inet6_route_address` are merged into `route_address`, +`inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`. + +Old fields are deprecated and will be removed in sing-box 1.11.0. + #### Drop support for go1.18 and go1.19 Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile. diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index 76e7e7684c..6815e9fc8b 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -6,6 +6,14 @@ icon: material/delete-alert ## 1.10.0 +#### TUN 地址字段已合并 + +`inet4_address` 和 `inet6_address` 已合并为 `address`, +`inet4_route_address` 和 `inet6_route_address` 已合并为 `route_address`, +`inet4_route_exclude_address` 和 `inet6_route_exclude_address` 已合并为 `route_exclude_address`。 + +旧字段已废弃,且将在 sing-box 1.11.0 中移除。 + #### 移除对 go1.18 和 go1.19 的支持 由于维护困难,sing-box 1.10.0 要求至少 Go 1.20 才能编译。 diff --git a/docs/migration.md b/docs/migration.md index efe92dfd6a..c696a836b1 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -2,6 +2,74 @@ icon: material/arrange-bring-forward --- +## 1.10.0 + +### TUN address fields are merged + +`inet4_address` and `inet6_address` are merged into `address`, +`inet4_route_address` and `inet6_route_address` are merged into `route_address`, +`inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`. + +Old fields are deprecated and will be removed in sing-box 1.11.0. + +!!! info "References" + + [TUN](/configuration/inbound/tun/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "inbounds": [ + { + "type": "tun", + "inet4_address": "172.19.0.1/30", + "inet6_address": "fdfe:dcba:9876::1/126", + "inet4_route_address": [ + "0.0.0.0/1", + "128.0.0.0/1" + ], + "inet6_route_address": [ + "::/1", + "8000::/1" + ], + "inet4_route_exclude_address": [ + "192.168.0.0/16" + ], + "inet6_route_exclude_address": [ + "fc00::/7" + ] + } + ] + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "inbounds": [ + { + "type": "tun", + "address": [ + "172.19.0.1/30", + "fdfe:dcba:9876::1/126" + ], + "route_address": [ + "0.0.0.0/1", + "128.0.0.0/1", + "::/1", + "8000::/1" + ], + "route_exclude_address": [ + "192.168.0.0/16", + "fc00::/7" + ] + } + ] + } + ``` + ## 1.9.0 ### `domain_suffix` behavior update diff --git a/docs/migration.zh.md b/docs/migration.zh.md index ce23875a93..9fe40cc9fd 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -2,6 +2,74 @@ icon: material/arrange-bring-forward --- +## 1.10.0 + +### TUN 地址字段已合并 + +`inet4_address` 和 `inet6_address` 已合并为 `address`, +`inet4_route_address` 和 `inet6_route_address` 已合并为 `route_address`, +`inet4_route_exclude_address` 和 `inet6_route_exclude_address` 已合并为 `route_exclude_address`。 + +旧字段已废弃,且将在 sing-box 1.11.0 中移除。 + +!!! info "参考" + + [TUN](/zh/configuration/inbound/tun/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "inbounds": [ + { + "type": "tun", + "inet4_address": "172.19.0.1/30", + "inet6_address": "fdfe:dcba:9876::1/126", + "inet4_route_address": [ + "0.0.0.0/1", + "128.0.0.0/1" + ], + "inet6_route_address": [ + "::/1", + "8000::/1" + ], + "inet4_route_exclude_address": [ + "192.168.0.0/16" + ], + "inet6_route_exclude_address": [ + "fc00::/7" + ] + } + ] + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "inbounds": [ + { + "type": "tun", + "address": [ + "172.19.0.1/30", + "fdfe:dcba:9876::1/126" + ], + "route_address": [ + "0.0.0.0/1", + "128.0.0.0/1", + "::/1", + "8000::/1" + ], + "route_exclude_address": [ + "192.168.0.0/16", + "fc00::/7" + ] + } + ] + } + ``` + ## 1.9.0 ### `domain_suffix` 行为更新 diff --git a/go.mod b/go.mod index 1db4dac454..724b04886d 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.2.7 github.com/sagernet/sing-shadowsocks2 v0.2.0 github.com/sagernet/sing-shadowtls v0.1.4 - github.com/sagernet/sing-tun v0.3.2 + github.com/sagernet/sing-tun v0.4.0-beta.13.0.20240703164908-1f043289199d github.com/sagernet/sing-vmess v0.1.12 github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 github.com/sagernet/utls v1.5.4 @@ -44,8 +44,8 @@ require ( github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.23.0 - golang.org/x/net v0.25.0 + golang.org/x/crypto v0.24.0 + golang.org/x/net v0.26.0 golang.org/x/sys v0.21.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 google.golang.org/grpc v1.63.2 @@ -65,6 +65,7 @@ require ( github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/google/btree v1.1.2 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -72,24 +73,28 @@ require ( github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/libdns/libdns v0.2.2 // indirect + github.com/mdlayher/netlink v1.7.2 // indirect + github.com/mdlayher/socket v0.4.1 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/onsi/ginkgo/v2 v2.9.7 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect - github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba // indirect + github.com/sagernet/fswatch v0.1.1 // indirect + github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect + github.com/sagernet/nftables v0.3.0-beta.4 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect - github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect + github.com/vishvananda/netns v0.0.4 // indirect github.com/zeebo/blake3 v0.2.3 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/mod v0.18.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + golang.org/x/tools v0.22.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index d45616e855..bd39576b1c 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,7 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk= github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= @@ -69,6 +70,10 @@ github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d h1:j9LtzkYstLFoNvXW824QQeN7Y26uPL5249kzWKbzO9U= github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts= github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30= @@ -97,12 +102,16 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 h1:YbmpqPQEMdlk9oFSKYWRqVuu9qzNiOayIonKmv1gCXY= github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1/go.mod h1:J2yAxTFPDjrDPhuAi9aWFz2L3ox9it4qAluBBbN0H5k= +github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= +github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= github.com/sagernet/gomobile v0.1.3 h1:ohjIb1Ou2+1558PnZour3od69suSuvkdSVOlO1tC4B8= github.com/sagernet/gomobile v0.1.3/go.mod h1:Pqq2+ZVvs10U7xK+UwJgwYWUykewi8H6vlslAO73n9E= github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f h1:NkhuupzH5ch7b/Y/6ZHJWrnNLoiNnSJaow6DPb8VW2I= github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f/go.mod h1:KXmw+ouSJNOsuRpg4wgwwCQuunrGz4yoAqQjsLjc6N0= -github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba h1:EY5AS7CCtfmARNv2zXUOrsEMPFDGYxaw65JzA2p51Vk= -github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= +github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= +github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= +github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= +github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/quic-go v0.46.0-beta.4 h1:k9f7VSKaM47AY6MPND0Qf1KRN7HwimPg9zdOFTXTiCk= github.com/sagernet/quic-go v0.46.0-beta.4/go.mod h1:zJmVdJUNqEDXfubf4KtIOUHHerggjBduiGRLNzJspcM= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc= @@ -122,8 +131,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wK github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k= github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4= -github.com/sagernet/sing-tun v0.3.2 h1:z0bLUT/YXH9RrJS9DsIpB0Bb9afl2hVJOmHd0zA3HJY= -github.com/sagernet/sing-tun v0.3.2/go.mod h1:DxLIyhjWU/HwGYoX0vNGg2c5QgTQIakphU1MuERR5tQ= +github.com/sagernet/sing-tun v0.4.0-beta.13.0.20240703164908-1f043289199d h1:2nBM9W9fOCM45hjlu1Fh9qyzBCgKEkq+SOuRCbCCs7c= +github.com/sagernet/sing-tun v0.4.0-beta.13.0.20240703164908-1f043289199d/go.mod h1:81JwnnYw8X9W9XvmZetSTTiPgIE3SbAbnc+EHKwPJ5U= github.com/sagernet/sing-vmess v0.1.12 h1:2gFD8JJb+eTFMoa8FIVMnknEi+vCSfaiTXTfEYAYAPg= github.com/sagernet/sing-vmess v0.1.12/go.mod h1:luTSsfyBGAc9VhtCqwjR+dt1QgqBhuYBCONB/POhF8I= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= @@ -146,8 +155,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= -github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= -github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= @@ -163,20 +172,19 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -187,7 +195,7 @@ golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= @@ -195,8 +203,8 @@ golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= diff --git a/inbound/tun.go b/inbound/tun.go index 7bd700d348..cb6a02c301 100644 --- a/inbound/tun.go +++ b/inbound/tun.go @@ -3,6 +3,9 @@ package inbound import ( "context" "net" + "net/netip" + "os" + "runtime" "strconv" "strings" "time" @@ -19,27 +22,91 @@ import ( M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ranges" + "github.com/sagernet/sing/common/x/list" + + "go4.org/netipx" ) var _ adapter.Inbound = (*Tun)(nil) type Tun struct { - tag string - ctx context.Context - router adapter.Router - logger log.ContextLogger - inboundOptions option.InboundOptions - tunOptions tun.Options - endpointIndependentNat bool - udpTimeout int64 - stack string - tunIf tun.Tun - tunStack tun.Stack - platformInterface platform.Interface - platformOptions option.TunPlatformOptions + tag string + ctx context.Context + router adapter.Router + logger log.ContextLogger + inboundOptions option.InboundOptions + tunOptions tun.Options + endpointIndependentNat bool + udpTimeout int64 + stack string + tunIf tun.Tun + tunStack tun.Stack + platformInterface platform.Interface + platformOptions option.TunPlatformOptions + autoRedirect tun.AutoRedirect + routeRuleSet []adapter.RuleSet + routeRuleSetCallback []*list.Element[adapter.RuleSetUpdateCallback] + routeExcludeRuleSet []adapter.RuleSet + routeExcludeRuleSetCallback []*list.Element[adapter.RuleSetUpdateCallback] + routeAddressSet []*netipx.IPSet + routeExcludeAddressSet []*netipx.IPSet } func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions, platformInterface platform.Interface) (*Tun, error) { + address := options.Address + //nolint:staticcheck + //goland:noinspection GoDeprecation + if len(options.Inet4Address) > 0 { + address = append(address, options.Inet4Address...) + } + //nolint:staticcheck + //goland:noinspection GoDeprecation + if len(options.Inet6Address) > 0 { + address = append(address, options.Inet6Address...) + } + inet4Address := common.Filter(address, func(it netip.Prefix) bool { + return it.Addr().Is4() + }) + inet6Address := common.Filter(address, func(it netip.Prefix) bool { + return it.Addr().Is6() + }) + + routeAddress := options.RouteAddress + //nolint:staticcheck + //goland:noinspection GoDeprecation + if len(options.Inet4RouteAddress) > 0 { + routeAddress = append(routeAddress, options.Inet4RouteAddress...) + } + //nolint:staticcheck + //goland:noinspection GoDeprecation + if len(options.Inet6RouteAddress) > 0 { + routeAddress = append(routeAddress, options.Inet6RouteAddress...) + } + inet4RouteAddress := common.Filter(routeAddress, func(it netip.Prefix) bool { + return it.Addr().Is4() + }) + inet6RouteAddress := common.Filter(routeAddress, func(it netip.Prefix) bool { + return it.Addr().Is6() + }) + + routeExcludeAddress := options.RouteExcludeAddress + //nolint:staticcheck + //goland:noinspection GoDeprecation + if len(options.Inet4RouteExcludeAddress) > 0 { + routeExcludeAddress = append(routeExcludeAddress, options.Inet4RouteExcludeAddress...) + } + //nolint:staticcheck + //goland:noinspection GoDeprecation + if len(options.Inet6RouteExcludeAddress) > 0 { + routeExcludeAddress = append(routeExcludeAddress, options.Inet6RouteExcludeAddress...) + } + inet4RouteExcludeAddress := common.Filter(routeExcludeAddress, func(it netip.Prefix) bool { + return it.Addr().Is4() + }) + inet6RouteExcludeAddress := common.Filter(routeExcludeAddress, func(it netip.Prefix) bool { + return it.Addr().Is6() + }) + tunMTU := options.MTU if tunMTU == 0 { tunMTU = 9000 @@ -50,9 +117,9 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger } else { udpTimeout = C.UDPTimeout } + var err error includeUID := uidToRange(options.IncludeUID) if len(options.IncludeUIDRange) > 0 { - var err error includeUID, err = parseRange(includeUID, options.IncludeUIDRange) if err != nil { return nil, E.Cause(err, "parse include_uid_range") @@ -60,13 +127,30 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger } excludeUID := uidToRange(options.ExcludeUID) if len(options.ExcludeUIDRange) > 0 { - var err error excludeUID, err = parseRange(excludeUID, options.ExcludeUIDRange) if err != nil { return nil, E.Cause(err, "parse exclude_uid_range") } } - return &Tun{ + + tableIndex := options.IPRoute2TableIndex + if tableIndex == 0 { + tableIndex = tun.DefaultIPRoute2TableIndex + } + ruleIndex := options.IPRoute2RuleIndex + if ruleIndex == 0 { + ruleIndex = tun.DefaultIPRoute2RuleIndex + } + inputMark := options.AutoRedirectInputMark + if inputMark == 0 { + inputMark = tun.DefaultAutoRedirectInputMark + } + outputMark := options.AutoRedirectOutputMark + if outputMark == 0 { + outputMark = tun.DefaultAutoRedirectOutputMark + } + + inbound := &Tun{ tag: tag, ctx: ctx, router: router, @@ -76,30 +160,83 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger Name: options.InterfaceName, MTU: tunMTU, GSO: options.GSO, - Inet4Address: options.Inet4Address, - Inet6Address: options.Inet6Address, + Inet4Address: inet4Address, + Inet6Address: inet6Address, AutoRoute: options.AutoRoute, + IPRoute2TableIndex: tableIndex, + IPRoute2RuleIndex: ruleIndex, + AutoRedirectInputMark: inputMark, + AutoRedirectOutputMark: outputMark, StrictRoute: options.StrictRoute, IncludeInterface: options.IncludeInterface, ExcludeInterface: options.ExcludeInterface, - Inet4RouteAddress: options.Inet4RouteAddress, - Inet6RouteAddress: options.Inet6RouteAddress, - Inet4RouteExcludeAddress: options.Inet4RouteExcludeAddress, - Inet6RouteExcludeAddress: options.Inet6RouteExcludeAddress, + Inet4RouteAddress: inet4RouteAddress, + Inet6RouteAddress: inet6RouteAddress, + Inet4RouteExcludeAddress: inet4RouteExcludeAddress, + Inet6RouteExcludeAddress: inet6RouteExcludeAddress, IncludeUID: includeUID, ExcludeUID: excludeUID, IncludeAndroidUser: options.IncludeAndroidUser, IncludePackage: options.IncludePackage, ExcludePackage: options.ExcludePackage, InterfaceMonitor: router.InterfaceMonitor(), - TableIndex: 2022, }, endpointIndependentNat: options.EndpointIndependentNat, udpTimeout: int64(udpTimeout.Seconds()), stack: options.Stack, platformInterface: platformInterface, platformOptions: common.PtrValueOrDefault(options.Platform), - }, nil + } + if options.AutoRedirect { + if !options.AutoRoute { + return nil, E.New("`auto_route` is required by `auto_redirect`") + } + disableNFTables, dErr := strconv.ParseBool(os.Getenv("DISABLE_NFTABLES")) + inbound.autoRedirect, err = tun.NewAutoRedirect(tun.AutoRedirectOptions{ + TunOptions: &inbound.tunOptions, + Context: ctx, + Handler: inbound, + Logger: logger, + NetworkMonitor: router.NetworkMonitor(), + InterfaceFinder: router.InterfaceFinder(), + TableName: "sing-box", + DisableNFTables: dErr == nil && disableNFTables, + RouteAddressSet: &inbound.routeAddressSet, + RouteExcludeAddressSet: &inbound.routeExcludeAddressSet, + }) + if err != nil { + return nil, E.Cause(err, "initialize auto-redirect") + } + if runtime.GOOS != "android" { + var markMode bool + for _, routeAddressSet := range options.RouteAddressSet { + ruleSet, loaded := router.RuleSet(routeAddressSet) + if !loaded { + return nil, E.New("parse route_address_set: rule-set not found: ", routeAddressSet) + } + ruleSet.IncRef() + inbound.routeRuleSet = append(inbound.routeRuleSet, ruleSet) + markMode = true + } + for _, routeExcludeAddressSet := range options.RouteExcludeAddressSet { + ruleSet, loaded := router.RuleSet(routeExcludeAddressSet) + if !loaded { + return nil, E.New("parse route_exclude_address_set: rule-set not found: ", routeExcludeAddressSet) + } + ruleSet.IncRef() + inbound.routeExcludeRuleSet = append(inbound.routeExcludeRuleSet, ruleSet) + markMode = true + } + if markMode { + inbound.tunOptions.AutoRedirectMarkMode = true + err = router.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark) + if err != nil { + return nil, err + } + } + } + } + return inbound, nil } func uidToRange(uidList option.Listable[uint32]) []ranges.Range[uint32] { @@ -121,11 +258,11 @@ func parseRange(uidRanges []ranges.Range[uint32], rangeList []string) ([]ranges. } var start, end uint64 var err error - start, err = strconv.ParseUint(uidRange[:subIndex], 10, 32) + start, err = strconv.ParseUint(uidRange[:subIndex], 0, 32) if err != nil { return nil, E.Cause(err, "parse range start") } - end, err = strconv.ParseUint(uidRange[subIndex+1:], 10, 32) + end, err = strconv.ParseUint(uidRange[subIndex+1:], 0, 32) if err != nil { return nil, E.Cause(err, "parse range end") } @@ -200,10 +337,58 @@ func (t *Tun) Start() error { return nil } +func (t *Tun) PostStart() error { + monitor := taskmonitor.New(t.logger, C.StartTimeout) + if t.autoRedirect != nil { + t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet) + for _, routeRuleSet := range t.routeRuleSet { + ipSets := routeRuleSet.ExtractIPSet() + if len(ipSets) == 0 { + t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeRuleSet.Name()) + } + t.routeAddressSet = append(t.routeAddressSet, ipSets...) + } + t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet) + for _, routeExcludeRuleSet := range t.routeExcludeRuleSet { + ipSets := routeExcludeRuleSet.ExtractIPSet() + if len(ipSets) == 0 { + t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeExcludeRuleSet.Name()) + } + t.routeExcludeAddressSet = append(t.routeExcludeAddressSet, ipSets...) + } + monitor.Start("initialize auto-redirect") + err := t.autoRedirect.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "auto-redirect") + } + for _, routeRuleSet := range t.routeRuleSet { + t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet)) + routeRuleSet.DecRef() + } + for _, routeExcludeRuleSet := range t.routeExcludeRuleSet { + t.routeExcludeRuleSetCallback = append(t.routeExcludeRuleSetCallback, routeExcludeRuleSet.RegisterCallback(t.updateRouteAddressSet)) + routeExcludeRuleSet.DecRef() + } + t.routeAddressSet = nil + t.routeExcludeAddressSet = nil + } + return nil +} + +func (t *Tun) updateRouteAddressSet(it adapter.RuleSet) { + t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet) + t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet) + t.autoRedirect.UpdateRouteAddressSet() + t.routeAddressSet = nil + t.routeExcludeAddressSet = nil +} + func (t *Tun) Close() error { return common.Close( t.tunStack, t.tunIf, + t.autoRedirect, ) } @@ -215,7 +400,11 @@ func (t *Tun) NewConnection(ctx context.Context, conn net.Conn, upstreamMetadata metadata.Source = upstreamMetadata.Source metadata.Destination = upstreamMetadata.Destination metadata.InboundOptions = t.inboundOptions - t.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) + if upstreamMetadata.Protocol != "" { + t.logger.InfoContext(ctx, "inbound ", upstreamMetadata.Protocol, " connection from ", metadata.Source) + } else { + t.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) + } t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) err := t.router.RouteConnection(ctx, conn, metadata) if err != nil { diff --git a/option/tun.go b/option/tun.go index ac66a8061c..cbc73e7d4b 100644 --- a/option/tun.go +++ b/option/tun.go @@ -3,29 +3,46 @@ package option import "net/netip" type TunInboundOptions struct { - InterfaceName string `json:"interface_name,omitempty"` - MTU uint32 `json:"mtu,omitempty"` - GSO bool `json:"gso,omitempty"` - Inet4Address Listable[netip.Prefix] `json:"inet4_address,omitempty"` - Inet6Address Listable[netip.Prefix] `json:"inet6_address,omitempty"` - AutoRoute bool `json:"auto_route,omitempty"` - StrictRoute bool `json:"strict_route,omitempty"` - Inet4RouteAddress Listable[netip.Prefix] `json:"inet4_route_address,omitempty"` - Inet6RouteAddress Listable[netip.Prefix] `json:"inet6_route_address,omitempty"` + InterfaceName string `json:"interface_name,omitempty"` + MTU uint32 `json:"mtu,omitempty"` + GSO bool `json:"gso,omitempty"` + Address Listable[netip.Prefix] `json:"address,omitempty"` + AutoRoute bool `json:"auto_route,omitempty"` + IPRoute2TableIndex int `json:"iproute2_table_index,omitempty"` + IPRoute2RuleIndex int `json:"iproute2_rule_index,omitempty"` + AutoRedirect bool `json:"auto_redirect,omitempty"` + AutoRedirectInputMark uint32 `json:"auto_redirect_input_mark,omitempty"` + AutoRedirectOutputMark uint32 `json:"auto_redirect_output_mark,omitempty"` + StrictRoute bool `json:"strict_route,omitempty"` + RouteAddress Listable[netip.Prefix] `json:"route_address,omitempty"` + RouteAddressSet Listable[string] `json:"route_address_set,omitempty"` + RouteExcludeAddress Listable[netip.Prefix] `json:"route_exclude_address,omitempty"` + RouteExcludeAddressSet Listable[string] `json:"route_exclude_address_set,omitempty"` + IncludeInterface Listable[string] `json:"include_interface,omitempty"` + ExcludeInterface Listable[string] `json:"exclude_interface,omitempty"` + IncludeUID Listable[uint32] `json:"include_uid,omitempty"` + IncludeUIDRange Listable[string] `json:"include_uid_range,omitempty"` + ExcludeUID Listable[uint32] `json:"exclude_uid,omitempty"` + ExcludeUIDRange Listable[string] `json:"exclude_uid_range,omitempty"` + IncludeAndroidUser Listable[int] `json:"include_android_user,omitempty"` + IncludePackage Listable[string] `json:"include_package,omitempty"` + ExcludePackage Listable[string] `json:"exclude_package,omitempty"` + EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"` + UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` + Stack string `json:"stack,omitempty"` + Platform *TunPlatformOptions `json:"platform,omitempty"` + InboundOptions + + // Deprecated: merged to Address + Inet4Address Listable[netip.Prefix] `json:"inet4_address,omitempty"` + // Deprecated: merged to Address + Inet6Address Listable[netip.Prefix] `json:"inet6_address,omitempty"` + // Deprecated: merged to RouteAddress + Inet4RouteAddress Listable[netip.Prefix] `json:"inet4_route_address,omitempty"` + // Deprecated: merged to RouteAddress + Inet6RouteAddress Listable[netip.Prefix] `json:"inet6_route_address,omitempty"` + // Deprecated: merged to RouteExcludeAddress Inet4RouteExcludeAddress Listable[netip.Prefix] `json:"inet4_route_exclude_address,omitempty"` + // Deprecated: merged to RouteExcludeAddress Inet6RouteExcludeAddress Listable[netip.Prefix] `json:"inet6_route_exclude_address,omitempty"` - IncludeInterface Listable[string] `json:"include_interface,omitempty"` - ExcludeInterface Listable[string] `json:"exclude_interface,omitempty"` - IncludeUID Listable[uint32] `json:"include_uid,omitempty"` - IncludeUIDRange Listable[string] `json:"include_uid_range,omitempty"` - ExcludeUID Listable[uint32] `json:"exclude_uid,omitempty"` - ExcludeUIDRange Listable[string] `json:"exclude_uid_range,omitempty"` - IncludeAndroidUser Listable[int] `json:"include_android_user,omitempty"` - IncludePackage Listable[string] `json:"include_package,omitempty"` - ExcludePackage Listable[string] `json:"exclude_package,omitempty"` - EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"` - UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` - Stack string `json:"stack,omitempty"` - Platform *TunPlatformOptions `json:"platform,omitempty"` - InboundOptions } diff --git a/route/router.go b/route/router.go index 022a104606..9a89becb34 100644 --- a/route/router.go +++ b/route/router.go @@ -83,6 +83,7 @@ type Router struct { autoDetectInterface bool defaultInterface string defaultMark uint32 + autoRedirectOutputMark uint32 networkMonitor tun.NetworkUpdateMonitor interfaceMonitor tun.DefaultInterfaceMonitor packageManager tun.PackageManager @@ -533,7 +534,10 @@ func (r *Router) Start() error { if C.IsAndroid && r.platformInterface == nil { monitor.Start("initialize package manager") - packageManager, err := tun.NewPackageManager(r) + packageManager, err := tun.NewPackageManager(tun.PackageManagerOptions{ + Callback: r, + Logger: r.logger, + }) monitor.Finish() if err != nil { return E.Cause(err, "create package manager") @@ -736,10 +740,26 @@ func (r *Router) PostStart() error { return E.Cause(err, "initialize rule[", i, "]") } } + for _, ruleSet := range r.ruleSets { + monitor.Start("post start rule_set[", ruleSet.Name(), "]") + err := ruleSet.PostStart() + monitor.Finish() + if err != nil { + return E.Cause(err, "post start rule_set[", ruleSet.Name(), "]") + } + } r.started = true return nil } +func (r *Router) Cleanup() error { + for _, ruleSet := range r.ruleSetMap { + ruleSet.Cleanup() + } + runtime.GC() + return nil +} + func (r *Router) Outbound(tag string) (adapter.Outbound, bool) { outbound, loaded := r.outboundByTag[tag] return outbound, loaded @@ -1167,6 +1187,18 @@ func (r *Router) AutoDetectInterfaceFunc() control.Func { } } +func (r *Router) RegisterAutoRedirectOutputMark(mark uint32) error { + if r.autoRedirectOutputMark > 0 { + return E.New("only one auto-redirect can be configured") + } + r.autoRedirectOutputMark = mark + return nil +} + +func (r *Router) AutoRedirectOutputMark() uint32 { + return r.autoRedirectOutputMark +} + func (r *Router) DefaultInterface() string { return r.defaultInterface } diff --git a/route/rule_item_rule_set.go b/route/rule_item_rule_set.go index 482a9c7b45..4ecf8c18f0 100644 --- a/route/rule_item_rule_set.go +++ b/route/rule_item_rule_set.go @@ -32,6 +32,7 @@ func (r *RuleSetItem) Start() error { if !loaded { return E.New("rule-set not found: ", tag) } + ruleSet.IncRef() r.setList = append(r.setList, ruleSet) } return nil diff --git a/route/rule_set.go b/route/rule_set.go index f644fb406f..ff28858e58 100644 --- a/route/rule_set.go +++ b/route/rule_set.go @@ -9,10 +9,13 @@ import ( "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + + "go4.org/netipx" ) func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) { @@ -26,6 +29,24 @@ func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.Contex } } +func extractIPSetFromRule(rawRule adapter.HeadlessRule) []*netipx.IPSet { + switch rule := rawRule.(type) { + case *DefaultHeadlessRule: + return common.FlatMap(rule.destinationIPCIDRItems, func(rawItem RuleItem) []*netipx.IPSet { + switch item := rawItem.(type) { + case *IPCIDRItem: + return []*netipx.IPSet{item.ipSet} + default: + return nil + } + }) + case *LogicalHeadlessRule: + return common.FlatMap(rule.rules, extractIPSetFromRule) + default: + panic("unexpected rule type") + } +} + var _ adapter.RuleSetStartContext = (*RuleSetStartContext)(nil) type RuleSetStartContext struct { diff --git a/route/rule_set_local.go b/route/rule_set_local.go index 3945826708..5344618320 100644 --- a/route/rule_set_local.go +++ b/route/rule_set_local.go @@ -9,16 +9,23 @@ import ( "github.com/sagernet/sing-box/common/srs" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/atomic" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/x/list" + + "go4.org/netipx" ) var _ adapter.RuleSet = (*LocalRuleSet)(nil) type LocalRuleSet struct { + tag string rules []adapter.HeadlessRule metadata adapter.RuleSetMetadata + refs atomic.Int32 } func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) { @@ -58,16 +65,11 @@ func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleS metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule) - return &LocalRuleSet{rules, metadata}, nil + return &LocalRuleSet{tag: options.Tag, rules: rules, metadata: metadata}, nil } -func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { - for _, rule := range s.rules { - if rule.Match(metadata) { - return true - } - } - return false +func (s *LocalRuleSet) Name() string { + return s.tag } func (s *LocalRuleSet) String() string { @@ -78,10 +80,51 @@ func (s *LocalRuleSet) StartContext(ctx context.Context, startContext adapter.Ru return nil } +func (s *LocalRuleSet) PostStart() error { + return nil +} + func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata { return s.metadata } +func (s *LocalRuleSet) ExtractIPSet() []*netipx.IPSet { + return common.FlatMap(s.rules, extractIPSetFromRule) +} + +func (s *LocalRuleSet) IncRef() { + s.refs.Add(1) +} + +func (s *LocalRuleSet) DecRef() { + if s.refs.Add(-1) < 0 { + panic("rule-set: negative refs") + } +} + +func (s *LocalRuleSet) Cleanup() { + if s.refs.Load() == 0 { + s.rules = nil + } +} + +func (s *LocalRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { + return nil +} + +func (s *LocalRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) { +} + func (s *LocalRuleSet) Close() error { + s.rules = nil return nil } + +func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +} diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go index 8389c2f46b..bf0cfe204c 100644 --- a/route/rule_set_remote.go +++ b/route/rule_set_remote.go @@ -8,20 +8,26 @@ import ( "net/http" "runtime" "strings" + "sync" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/srs" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/atomic" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/logger" 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" + + "go4.org/netipx" ) var _ adapter.RuleSet = (*RemoteRuleSet)(nil) @@ -40,6 +46,9 @@ type RemoteRuleSet struct { lastEtag string updateTicker *time.Ticker pauseManager pause.Manager + callbackAccess sync.Mutex + callbacks list.List[adapter.RuleSetUpdateCallback] + refs atomic.Int32 } func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet { @@ -61,13 +70,8 @@ func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger. } } -func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { - for _, rule := range s.rules { - if rule.Match(metadata) { - return true - } - } - return false +func (s *RemoteRuleSet) Name() string { + return s.options.Tag } func (s *RemoteRuleSet) String() string { @@ -108,6 +112,10 @@ func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext adapter.R } } s.updateTicker = time.NewTicker(s.updateInterval) + return nil +} + +func (s *RemoteRuleSet) PostStart() error { go s.loopUpdate() return nil } @@ -116,6 +124,38 @@ func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata { return s.metadata } +func (s *RemoteRuleSet) ExtractIPSet() []*netipx.IPSet { + return common.FlatMap(s.rules, extractIPSetFromRule) +} + +func (s *RemoteRuleSet) IncRef() { + s.refs.Add(1) +} + +func (s *RemoteRuleSet) DecRef() { + if s.refs.Add(-1) < 0 { + panic("rule-set: negative refs") + } +} + +func (s *RemoteRuleSet) Cleanup() { + if s.refs.Load() == 0 { + s.rules = nil + } +} + +func (s *RemoteRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { + s.callbackAccess.Lock() + defer s.callbackAccess.Unlock() + return s.callbacks.PushBack(callback) +} + +func (s *RemoteRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) { + s.callbackAccess.Lock() + defer s.callbackAccess.Unlock() + s.callbacks.Remove(element) +} + func (s *RemoteRuleSet) loadBytes(content []byte) error { var ( plainRuleSet option.PlainRuleSet @@ -148,6 +188,12 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error { s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) s.metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule) s.rules = rules + s.callbackAccess.Lock() + callbacks := s.callbacks.Array() + s.callbackAccess.Unlock() + for _, callback := range callbacks { + callback(s) + } return nil } @@ -156,6 +202,8 @@ func (s *RemoteRuleSet) loopUpdate() { 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 } } for { @@ -168,6 +216,8 @@ func (s *RemoteRuleSet) loopUpdate() { 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 } } } @@ -253,7 +303,17 @@ func (s *RemoteRuleSet) fetchOnce(ctx context.Context, startContext adapter.Rule } func (s *RemoteRuleSet) Close() error { + s.rules = nil s.updateTicker.Stop() s.cancel() return nil } + +func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +} From f08a46ff8cfccfec76051d29bb331d3931f76c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 7 Jun 2024 15:59:56 +0800 Subject: [PATCH 16/31] Improve base DNS transports & Minor fixes --- go.mod | 16 ++++++++-------- go.sum | 34 +++++++++++++++++----------------- route/router_dns.go | 7 ------- 3 files changed, 25 insertions(+), 32 deletions(-) diff --git a/go.mod b/go.mod index 724b04886d..83dc74abe5 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/logrusorgru/aurora v2.0.3+incompatible github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d github.com/mholt/acmez v1.2.0 - github.com/miekg/dns v1.1.59 + github.com/miekg/dns v1.1.61 github.com/ooni/go-libtor v1.1.8 github.com/oschwald/maxminddb-golang v1.12.0 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a @@ -28,7 +28,7 @@ require ( github.com/sagernet/quic-go v0.46.0-beta.4 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 github.com/sagernet/sing v0.5.0-beta.1 - github.com/sagernet/sing-dns v0.2.3 + github.com/sagernet/sing-dns v0.3.0-beta.14 github.com/sagernet/sing-mux v0.2.0 github.com/sagernet/sing-quic v0.3.0-beta.2 github.com/sagernet/sing-shadowsocks v0.2.7 @@ -44,9 +44,9 @@ require ( github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.24.0 - golang.org/x/net v0.26.0 - golang.org/x/sys v0.21.0 + golang.org/x/crypto v0.25.0 + golang.org/x/net v0.27.0 + golang.org/x/sys v0.22.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 google.golang.org/grpc v1.63.2 google.golang.org/protobuf v1.33.0 @@ -89,12 +89,12 @@ require ( github.com/vishvananda/netns v0.0.4 // indirect github.com/zeebo/blake3 v0.2.3 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect - golang.org/x/mod v0.18.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/mod v0.19.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.22.0 // indirect + golang.org/x/tools v0.23.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index bd39576b1c..38faa8847d 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d h1:j9LtzkYstLFoNv github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts= github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30= github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE= -github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= -github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= +github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= +github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss= @@ -119,8 +119,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.5.0-beta.1 h1:THZMZgJcDQxutE++6Ckih1HlvMtXple94RBGa6GSg2I= github.com/sagernet/sing v0.5.0-beta.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= -github.com/sagernet/sing-dns v0.2.3 h1:YzeBUn2tR38F7HtvGEQ0kLRLmZWMEgi/+7wqa4Twb1k= -github.com/sagernet/sing-dns v0.2.3/go.mod h1:BJpJv6XLnrUbSyIntOT6DG9FW0f4fETmPAHvNjOprLg= +github.com/sagernet/sing-dns v0.3.0-beta.14 h1:/s+fJzYKsvLaNDt/2rjpsrDcN8wmCO2JbX6OFrl8Nww= +github.com/sagernet/sing-dns v0.3.0-beta.14/go.mod h1:rscgSr5ixOPk8XM9ZMLuMXCyldEQ1nLvdl0nfv+lp00= github.com/sagernet/sing-mux v0.2.0 h1:4C+vd8HztJCWNYfufvgL49xaOoOHXty2+EAjnzN3IYo= github.com/sagernet/sing-mux v0.2.0/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ= github.com/sagernet/sing-quic v0.3.0-beta.2 h1:9TiaW4js4fXD6GPCGMbwb3/bIRKpXm7skJBdV1OdvMs= @@ -172,16 +172,16 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= -golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -192,10 +192,10 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= @@ -203,8 +203,8 @@ golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= diff --git a/route/router_dns.go b/route/router_dns.go index e0055009b0..51994072ab 100644 --- a/route/router_dns.go +++ b/route/router_dns.go @@ -8,7 +8,6 @@ import ( "time" "github.com/sagernet/sing-box/adapter" - C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-dns" "github.com/sagernet/sing/common/cache" E "github.com/sagernet/sing/common/exceptions" @@ -125,12 +124,10 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er for { var ( dnsCtx context.Context - cancel context.CancelFunc addressLimit bool ) dnsCtx, transport, strategy, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message)) - dnsCtx, cancel = context.WithTimeout(dnsCtx, C.DNSTimeout) if rule != nil && rule.WithAddressLimit() { addressLimit = true response, err = r.dnsClient.ExchangeWithResponseCheck(dnsCtx, transport, message, strategy, func(response *mDNS.Msg) bool { @@ -141,7 +138,6 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er addressLimit = false response, err = r.dnsClient.Exchange(dnsCtx, transport, message, strategy) } - cancel() var rejected bool if err != nil { if errors.Is(err, dns.ErrResponseRejectedCached) { @@ -206,7 +202,6 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS for { var ( dnsCtx context.Context - cancel context.CancelFunc addressLimit bool ) metadata.ResetRuleCache() @@ -215,7 +210,6 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS if strategy == dns.DomainStrategyAsIS { strategy = transportStrategy } - dnsCtx, cancel = context.WithTimeout(dnsCtx, C.DNSTimeout) if rule != nil && rule.WithAddressLimit() { addressLimit = true responseAddrs, err = r.dnsClient.LookupWithResponseCheck(dnsCtx, transport, domain, strategy, func(responseAddrs []netip.Addr) bool { @@ -226,7 +220,6 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS addressLimit = false responseAddrs, err = r.dnsClient.Lookup(dnsCtx, transport, domain, strategy) } - cancel() if err != nil { if errors.Is(err, dns.ErrResponseRejectedCached) { r.dnsLogger.DebugContext(ctx, "response rejected for ", domain, " (cached)") From 345cf674aa9b599822fa57d66e086f547d2439bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 22 Jun 2024 14:11:49 +0800 Subject: [PATCH 17/31] Add custom options for TUN `auto-route` and `auto-redirect` --- docs/configuration/inbound/tun.md | 82 +++++++++++++++++++++------- docs/configuration/inbound/tun.zh.md | 70 +++++++++++++++++++----- inbound/tun.go | 4 +- option/tun.go | 36 +++++++++++- 4 files changed, 153 insertions(+), 39 deletions(-) diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index 1e2bf40058..812b20d163 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -13,7 +13,11 @@ icon: material/new-box :material-plus: [route_exclude_address](#route_address) :material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address) :material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address) + :material-plus: [iproute2_table_index](#iproute2_table_index) + :material-plus: [iproute2_rule_index](#iproute2_table_index) :material-plus: [auto_redirect](#auto_redirect) + :material-plus: [auto_redirect_input_mark](#auto_redirect_input_mark) + :material-plus: [auto_redirect_output_mark](#auto_redirect_output_mark) :material-plus: [route_address_set](#route_address_set) :material-plus: [route_exclude_address_set](#route_address_set) @@ -53,8 +57,12 @@ icon: material/new-box "mtu": 9000, "gso": false, "auto_route": true, - "strict_route": true, + "iproute2_table_index": 2022, + "iproute2_rule_index": 9000, "auto_redirect": false, + "auto_redirect_input_mark": "0x2023", + "auto_redirect_output_mark": "0x2024", + "strict_route": true, "route_address": [ "0.0.0.0/1", "128.0.0.0/1", @@ -129,8 +137,8 @@ icon: material/new-box "match_domain": [] } }, - - ... // Listen Fields + ... + // Listen Fields } ``` @@ -180,7 +188,7 @@ The maximum transmission unit. !!! quote "" - Only supported on Linux. + Only supported on Linux with `auto_route` enabled. Enable generic segmentation offload. @@ -196,24 +204,21 @@ Set the default route to the Tun. By default, VPN takes precedence over tun. To make tun go through VPN, enable `route.override_android_vpn`. -#### strict_route +#### iproute2_table_index -Enforce strict routing rules when `auto_route` is enabled: +!!! question "Since sing-box 1.10.0" -*In Linux*: +Linux iproute2 table index generated by `auto_route`. -* Let unsupported network unreachable -* Make ICMP traffic route to tun instead of upstream interfaces -* Route all connections to tun +`2022` is used by default. -It prevents IP address leaks and makes DNS hijacking work on Android. +#### iproute2_rule_index -*In Windows*: +!!! question "Since sing-box 1.10.0" -* Add firewall rules to prevent DNS leak caused by - Windows' [ordinary multihomed DNS resolution behavior](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29) +Linux iproute2 rule start index generated by `auto_route`. -It may prevent some applications (such as VirtualBox) from working properly in certain situations. +`9000` is used by default. #### auto_redirect @@ -234,6 +239,41 @@ use [VPNHotspot](https://github.com/Mygod/VPNHotspot). `auto_route` with `auto_redirect` now works as expected on routers **without intervention**. +#### auto_redirect_input_mark + +!!! question "Since sing-box 1.10.0" + +Connection input mark used by `route_address_set` and `route_exclude_address_set`. + +`0x2023` is used by default. + +#### auto_redirect_output_mark + +!!! question "Since sing-box 1.10.0" + +Connection output mark used by `route_address_set` and `route_exclude_address_set`. + +`0x2024` is used by default. + +#### strict_route + +Enforce strict routing rules when `auto_route` is enabled: + +*In Linux*: + +* Let unsupported network unreachable +* Make ICMP traffic route to tun instead of upstream interfaces +* Route all connections to tun + +It prevents IP address leaks and makes DNS hijacking work on Android. + +*In Windows*: + +* Add firewall rules to prevent DNS leak caused by + Windows' [ordinary multihomed DNS resolution behavior](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29) + +It may prevent some applications (such as VirtualBox) from working properly in certain situations. + #### route_address !!! question "Since sing-box 1.10.0" @@ -244,7 +284,8 @@ Use custom routes instead of default when `auto_route` is enabled. !!! failure "Deprecated in sing-box 1.10.0" - `inet4_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address) instead. +`inet4_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address) +instead. Use custom routes instead of default when `auto_route` is enabled. @@ -252,7 +293,8 @@ Use custom routes instead of default when `auto_route` is enabled. !!! failure "Deprecated in sing-box 1.10.0" - `inet6_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address) instead. +`inet6_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address) +instead. Use custom routes instead of default when `auto_route` is enabled. @@ -266,7 +308,8 @@ Exclude custom routes when `auto_route` is enabled. !!! failure "Deprecated in sing-box 1.10.0" - `inet4_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_exclude_address](#route_exclude_address) instead. +`inet4_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please +use [route_exclude_address](#route_exclude_address) instead. Exclude custom routes when `auto_route` is enabled. @@ -274,7 +317,8 @@ Exclude custom routes when `auto_route` is enabled. !!! failure "Deprecated in sing-box 1.10.0" - `inet6_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_exclude_address](#route_exclude_address) instead. +`inet6_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please +use [route_exclude_address](#route_exclude_address) instead. Exclude custom routes when `auto_route` is enabled. diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index 5b1d35afb5..bff493d4f9 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -12,8 +12,12 @@ icon: material/new-box :material-delete-clock: [inet6_route_address](#inet6_route_address) :material-plus: [route_exclude_address](#route_address) :material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address) - :material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address) + :material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address) + :material-plus: [iproute2_table_index](#iproute2_table_index) + :material-plus: [iproute2_rule_index](#iproute2_table_index) :material-plus: [auto_redirect](#auto_redirect) + :material-plus: [auto_redirect_input_mark](#auto_redirect_input_mark) + :material-plus: [auto_redirect_output_mark](#auto_redirect_output_mark) :material-plus: [route_address_set](#route_address_set) :material-plus: [route_exclude_address_set](#route_address_set) @@ -53,8 +57,12 @@ icon: material/new-box "mtu": 9000, "gso": false, "auto_route": true, - "strict_route": true, + "iproute2_table_index": 2022, + "iproute2_rule_index": 9000, "auto_redirect": false, + "auto_redirect_input_mark": "0x2023", + "auto_redirect_output_mark": "0x2024", + "strict_route": true, "route_address": [ "0.0.0.0/1", "128.0.0.0/1", @@ -200,25 +208,21 @@ tun 接口的 IPv6 前缀。 VPN 默认优先于 tun。要使 tun 经过 VPN,启用 `route.override_android_vpn`。 -#### strict_route +#### iproute2_table_index -启用 `auto_route` 时执行严格的路由规则。 +!!! question "自 sing-box 1.10.0 起" -*在 Linux 中*: +`auto_route` 生成的 iproute2 路由表索引。 -* 让不支持的网络无法到达 -* 使 ICMP 流量路由到 tun 而不是上游接口 -* 将所有连接路由到 tun +默认使用 `2022`。 -它可以防止 IP 地址泄漏,并使 DNS 劫持在 Android 上工作。 +#### iproute2_rule_index -*在 Windows 中*: +!!! question "自 sing-box 1.10.0 起" -* 添加防火墙规则以阻止 Windows - 的 [普通多宿主 DNS 解析行为](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29) - 造成的 DNS 泄露 +`auto_route` 生成的 iproute2 规则起始索引。 -它可能会使某些应用程序(如 VirtualBox)在某些情况下无法正常工作。 +默认使用 `9000`。 #### auto_redirect @@ -226,7 +230,7 @@ tun 接口的 IPv6 前缀。 !!! quote "" - 仅支持 Linux。 + 仅支持 Linux,且需要 `auto_route` 已启用。 自动配置 iptables 以重定向 TCP 连接。 @@ -238,6 +242,42 @@ tun 接口的 IPv6 前缀。 带有 `auto_redirect `的 `auto_route` 现在可以在路由器上按预期工作,**无需干预**。 +#### auto_redirect_input_mark + +!!! question "自 sing-box 1.10.0 起" + +`route_address_set` 和 `route_exclude_address_set` 使用的连接输入标记。 + +默认使用 `0x2023`。 + +#### auto_redirect_output_mark + +!!! question "自 sing-box 1.10.0 起" + +`route_address_set` 和 `route_exclude_address_set` 使用的连接输出标记。 + +默认使用 `0x2024`。 + +#### strict_route + +启用 `auto_route` 时执行严格的路由规则。 + +*在 Linux 中*: + +* 让不支持的网络无法到达 +* 使 ICMP 流量路由到 tun 而不是上游接口 +* 将所有连接路由到 tun + +它可以防止 IP 地址泄漏,并使 DNS 劫持在 Android 上工作。 + +*在 Windows 中*: + +* 添加防火墙规则以阻止 Windows + 的 [普通多宿主 DNS 解析行为](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29) + 造成的 DNS 泄露 + +它可能会使某些应用程序(如 VirtualBox)在某些情况下无法正常工作。 + #### route_address !!! question "自 sing-box 1.10.0 起" diff --git a/inbound/tun.go b/inbound/tun.go index cb6a02c301..6cb65de249 100644 --- a/inbound/tun.go +++ b/inbound/tun.go @@ -141,11 +141,11 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger if ruleIndex == 0 { ruleIndex = tun.DefaultIPRoute2RuleIndex } - inputMark := options.AutoRedirectInputMark + inputMark := uint32(options.AutoRedirectInputMark) if inputMark == 0 { inputMark = tun.DefaultAutoRedirectInputMark } - outputMark := options.AutoRedirectOutputMark + outputMark := uint32(options.AutoRedirectOutputMark) if outputMark == 0 { outputMark = tun.DefaultAutoRedirectOutputMark } diff --git a/option/tun.go b/option/tun.go index cbc73e7d4b..dbb1bfeae0 100644 --- a/option/tun.go +++ b/option/tun.go @@ -1,6 +1,13 @@ package option -import "net/netip" +import ( + "net/netip" + "strconv" + + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json" +) type TunInboundOptions struct { InterfaceName string `json:"interface_name,omitempty"` @@ -11,8 +18,8 @@ type TunInboundOptions struct { IPRoute2TableIndex int `json:"iproute2_table_index,omitempty"` IPRoute2RuleIndex int `json:"iproute2_rule_index,omitempty"` AutoRedirect bool `json:"auto_redirect,omitempty"` - AutoRedirectInputMark uint32 `json:"auto_redirect_input_mark,omitempty"` - AutoRedirectOutputMark uint32 `json:"auto_redirect_output_mark,omitempty"` + AutoRedirectInputMark FwMark `json:"auto_redirect_input_mark,omitempty"` + AutoRedirectOutputMark FwMark `json:"auto_redirect_output_mark,omitempty"` StrictRoute bool `json:"strict_route,omitempty"` RouteAddress Listable[netip.Prefix] `json:"route_address,omitempty"` RouteAddressSet Listable[string] `json:"route_address_set,omitempty"` @@ -46,3 +53,26 @@ type TunInboundOptions struct { // Deprecated: merged to RouteExcludeAddress Inet6RouteExcludeAddress Listable[netip.Prefix] `json:"inet6_route_exclude_address,omitempty"` } + +type FwMark uint32 + +func (f FwMark) MarshalJSON() ([]byte, error) { + return json.Marshal(F.ToString("0x", strconv.FormatUint(uint64(f), 16))) +} + +func (f *FwMark) UnmarshalJSON(bytes []byte) error { + var stringValue string + err := json.Unmarshal(bytes, &stringValue) + if err != nil { + if rawErr := json.Unmarshal(bytes, (*uint32)(f)); rawErr == nil { + return nil + } + return E.Cause(err, "invalid number or string mark") + } + intValue, err := strconv.ParseUint(stringValue, 0, 32) + if err != nil { + return err + } + *f = FwMark(intValue) + return nil +} From 79dece922b3952a3a4da5843fd7499d5f8a65035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 24 Jun 2024 09:41:00 +0800 Subject: [PATCH 18/31] Add accept empty DNS rule option --- adapter/inbound.go | 5 ++++- docs/configuration/dns/rule.md | 29 ++++++++++++++++++++++++-- docs/configuration/dns/rule.zh.md | 31 +++++++++++++++++++++++++--- docs/configuration/inbound/tun.zh.md | 12 +++++------ docs/configuration/route/rule.md | 23 ++++++++++++++++++++- docs/configuration/route/rule.zh.md | 23 ++++++++++++++++++++- option/rule.go | 25 +++++++++++++++++++--- option/rule_dns.go | 26 ++++++++++++++++++++--- route/router_dns.go | 17 +++++++++------ route/rule_default.go | 2 +- route/rule_dns.go | 2 +- route/rule_item_cidr.go | 19 +++++++++-------- route/rule_item_rule_set.go | 13 +++++++----- 13 files changed, 185 insertions(+), 42 deletions(-) diff --git a/adapter/inbound.go b/adapter/inbound.go index 063671c1a1..56eb562308 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -51,7 +51,9 @@ type InboundContext struct { // rule cache - IPCIDRMatchSource bool + IPCIDRMatchSource bool + IPCIDRAcceptEmpty bool + SourceAddressMatch bool SourcePortMatch bool DestinationAddressMatch bool @@ -62,6 +64,7 @@ type InboundContext struct { func (c *InboundContext) ResetRuleCache() { c.IPCIDRMatchSource = false + c.IPCIDRAcceptEmpty = false c.SourceAddressMatch = false c.SourcePortMatch = false c.DestinationAddressMatch = false diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 4c4abacb65..0faae0e680 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -2,6 +2,12 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.10.0" + + :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) + :material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) + !!! quote "Changes in sing-box 1.9.0" :material-plus: [geoip](#geoip) @@ -117,7 +123,10 @@ icon: material/new-box "geoip-cn", "geosite-cn" ], + // deprecated "rule_set_ipcidr_match_source": false, + "rule_set_ip_cidr_match_source": false, + "rule_set_ip_cidr_accept_empty": false, "invert": false, "outbound": [ "direct" @@ -309,7 +318,17 @@ Match [Rule Set](/configuration/route/#rule_set). !!! question "Since sing-box 1.9.0" -Make `ipcidr` in rule sets match the source IP. +!!! failure "Deprecated in sing-box 1.10.0" + + `rule_set_ipcidr_match_source` is renamed to `rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0. + +Make `ip_cidr` rule items in rule sets match the source IP. + +#### rule_set_ip_cidr_match_source + +!!! question "Since sing-box 1.10.0" + +Make `ip_cidr` rule items in rule sets match the source IP. #### invert @@ -347,7 +366,7 @@ Will overrides `dns.client_subnet` and `servers.[].client_subnet`. ### Address Filter Fields -Only takes effect for IP address requests. When the query results do not match the address filtering rule items, the current rule will be skipped. +Only takes effect for address requests (A/AAAA/HTTPS). When the query results do not match the address filtering rule items, the current rule will be skipped. !!! info "" @@ -375,6 +394,12 @@ Match IP CIDR with query response. Match private IP with query response. +#### rule_set_ip_cidr_accept_empty + +!!! question "Since sing-box 1.10.0" + +Make `ip_cidr` rules in rule sets accept empty query response. + ### Logical Fields #### type diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 796d29e8c4..eecddb3816 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -2,6 +2,12 @@ icon: material/new-box --- +!!! quote "sing-box 1.10.0 中的更改" + + :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) + :material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) + !!! quote "sing-box 1.9.0 中的更改" :material-plus: [geoip](#geoip) @@ -117,7 +123,10 @@ icon: material/new-box "geoip-cn", "geosite-cn" ], + // 已弃用 "rule_set_ipcidr_match_source": false, + "rule_set_ip_cidr_match_source": false, + "rule_set_ip_cidr_accept_empty": false, "invert": false, "outbound": [ "direct" @@ -307,7 +316,17 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 !!! question "自 sing-box 1.9.0 起" -使规则集中的 `ipcidr` 规则匹配源 IP。 +!!! failure "已在 sing-box 1.10.0 废弃" + + `rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 移除。 + +使规则集中的 `ip_cidr` 规则匹配源 IP。 + +#### rule_set_ip_cidr_match_source + +!!! question "自 sing-box 1.10.0 起" + +使规则集中的 `ip_cidr` 规则匹配源 IP。 #### invert @@ -345,7 +364,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 ### 地址筛选字段 -仅对IP地址请求生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。 +仅对地址请求 (A/AAAA/HTTPS) 生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。 !!! info "" @@ -365,7 +384,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 !!! question "自 sing-box 1.9.0 起" -与查询相应匹配 IP CIDR。 +与查询响应匹配 IP CIDR。 #### ip_is_private @@ -373,6 +392,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 与查询响应匹配非公开 IP。 +#### rule_set_ip_cidr_accept_empty + +!!! question "自 sing-box 1.10.0 起" + +使规则集中的 `ip_cidr` 规则接受空查询响应。 + ### 逻辑字段 #### type diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index bff493d4f9..88e02fe767 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -168,7 +168,7 @@ tun 接口的 IPv4 和 IPv6 前缀。 !!! failure "已在 sing-box 1.10.0 废弃" - `inet4_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除. + `inet4_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除。 ==必填== @@ -178,7 +178,7 @@ tun 接口的 IPv4 前缀。 !!! failure "已在 sing-box 1.10.0 废弃" - `inet6_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除. + `inet6_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除。 tun 接口的 IPv6 前缀。 @@ -288,7 +288,7 @@ tun 接口的 IPv6 前缀。 !!! failure "已在 sing-box 1.10.0 废弃" - `inet4_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除. + `inet4_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除。 启用 `auto_route` 时使用自定义路由而不是默认路由。 @@ -296,7 +296,7 @@ tun 接口的 IPv6 前缀。 !!! failure "已在 sing-box 1.10.0 废弃" - `inet6_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除. + `inet6_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除。 启用 `auto_route` 时使用自定义路由而不是默认路由。 @@ -310,7 +310,7 @@ tun 接口的 IPv6 前缀。 !!! failure "已在 sing-box 1.10.0 废弃" - `inet4_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除. + `inet4_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除。 启用 `auto_route` 时排除自定义路由。 @@ -318,7 +318,7 @@ tun 接口的 IPv6 前缀。 !!! failure "已在 sing-box 1.10.0 废弃" - `inet6_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除. + `inet6_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除。 启用 `auto_route` 时排除自定义路由。 diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 62d33c6c53..1d06a875a1 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -1,3 +1,12 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.10.0" + + :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) + !!! quote "Changes in sing-box 1.8.0" :material-plus: [rule_set](#rule_set) @@ -105,7 +114,9 @@ "geoip-cn", "geosite-cn" ], + // deprecated "rule_set_ipcidr_match_source": false, + "rule_set_ip_cidr_match_source": false, "invert": false, "outbound": "direct" }, @@ -303,7 +314,17 @@ Match [Rule Set](/configuration/route/#rule_set). !!! question "Since sing-box 1.8.0" -Make `ipcidr` in rule sets match the source IP. +!!! failure "Deprecated in sing-box 1.10.0" + + `rule_set_ipcidr_match_source` is renamed to `rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0. + +Make `ip_cidr` in rule sets match the source IP. + +#### rule_set_ip_cidr_match_source + +!!! question "Since sing-box 1.10.0" + +Make `ip_cidr` in rule sets match the source IP. #### invert diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index cba35bc581..52d334f22c 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -1,3 +1,12 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.10.0 中的更改" + + :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) + !!! quote "sing-box 1.8.0 中的更改" :material-plus: [rule_set](#rule_set) @@ -103,7 +112,9 @@ "geoip-cn", "geosite-cn" ], + // 已弃用 "rule_set_ipcidr_match_source": false, + "rule_set_ip_cidr_match_source": false, "invert": false, "outbound": "direct" }, @@ -301,7 +312,17 @@ !!! question "自 sing-box 1.8.0 起" -使规则集中的 `ipcidr` 规则匹配源 IP。 +!!! failure "已在 sing-box 1.10.0 废弃" + + `rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 移除。 + +使规则集中的 `ip_cidr` 规则匹配源 IP。 + +#### rule_set_ip_cidr_match_source + +!!! question "自 sing-box 1.10.0 起" + +使规则集中的 `ip_cidr` 规则匹配源 IP。 #### invert diff --git a/option/rule.go b/option/rule.go index 0ea133c75f..74dd13c615 100644 --- a/option/rule.go +++ b/option/rule.go @@ -64,7 +64,7 @@ func (r Rule) IsValid() bool { } } -type DefaultRule struct { +type _DefaultRule struct { Inbound Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` Network Listable[string] `json:"network,omitempty"` @@ -94,12 +94,31 @@ type DefaultRule struct { WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` RuleSet Listable[string] `json:"rule_set,omitempty"` - RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` Invert bool `json:"invert,omitempty"` Outbound string `json:"outbound,omitempty"` + + // Deprecated: renamed to rule_set_ip_cidr_match_source + Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` +} + +type DefaultRule _DefaultRule + +func (r *DefaultRule) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_DefaultRule)(r)) + if err != nil { + return err + } + //nolint:staticcheck + //goland:noinspection GoDeprecation + if r.Deprecated_RulesetIPCIDRMatchSource { + r.Deprecated_RulesetIPCIDRMatchSource = false + r.RuleSetIPCIDRMatchSource = true + } + return nil } -func (r DefaultRule) IsValid() bool { +func (r *DefaultRule) IsValid() bool { var defaultValue DefaultRule defaultValue.Invert = r.Invert defaultValue.Outbound = r.Outbound diff --git a/option/rule_dns.go b/option/rule_dns.go index c5994e1cec..2afe245ea4 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -64,7 +64,7 @@ func (r DNSRule) IsValid() bool { } } -type DefaultDNSRule struct { +type _DefaultDNSRule struct { Inbound Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` @@ -96,15 +96,35 @@ type DefaultDNSRule struct { WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` RuleSet Listable[string] `json:"rule_set,omitempty"` - RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` + RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"` Invert bool `json:"invert,omitempty"` Server string `json:"server,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"` + + // Deprecated: renamed to rule_set_ip_cidr_match_source + Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` +} + +type DefaultDNSRule _DefaultDNSRule + +func (r *DefaultDNSRule) UnmarshalJSON(bytes []byte) error { + err := json.UnmarshalDisallowUnknownFields(bytes, (*_DefaultDNSRule)(r)) + if err != nil { + return err + } + //nolint:staticcheck + //goland:noinspection GoDeprecation + if r.Deprecated_RulesetIPCIDRMatchSource { + r.Deprecated_RulesetIPCIDRMatchSource = false + r.RuleSetIPCIDRMatchSource = true + } + return nil } -func (r DefaultDNSRule) IsValid() bool { +func (r *DefaultDNSRule) IsValid() bool { var defaultValue DefaultDNSRule defaultValue.Invert = r.Invert defaultValue.Server = r.Server diff --git a/route/router_dns.go b/route/router_dns.go index 51994072ab..ead8c28943 100644 --- a/route/router_dns.go +++ b/route/router_dns.go @@ -104,7 +104,8 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er response, cached = r.dnsClient.ExchangeCache(ctx, message) if !cached { var metadata *adapter.InboundContext - ctx, metadata = adapter.AppendContext(ctx) + ctx, metadata = adapter.ExtendContext(ctx) + metadata.Destination = M.Socksaddr{} if len(message.Question) > 0 { metadata.QueryType = message.Question[0].Qtype switch metadata.QueryType { @@ -126,12 +127,16 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er dnsCtx context.Context addressLimit bool ) - dnsCtx, transport, strategy, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message)) + dnsCtx = adapter.OverrideContext(dnsCtx) if rule != nil && rule.WithAddressLimit() { addressLimit = true response, err = r.dnsClient.ExchangeWithResponseCheck(dnsCtx, transport, message, strategy, func(response *mDNS.Msg) bool { - metadata.DestinationAddresses, _ = dns.MessageToAddresses(response) + addresses, addrErr := dns.MessageToAddresses(response) + if addrErr != nil { + return false + } + metadata.DestinationAddresses = addresses return rule.MatchAddressLimit(metadata) }) } else { @@ -190,7 +195,8 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS return responseAddrs, nil } r.dnsLogger.DebugContext(ctx, "lookup domain ", domain) - ctx, metadata := adapter.AppendContext(ctx) + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Destination = M.Socksaddr{} metadata.Domain = domain var ( transport dns.Transport @@ -204,9 +210,8 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS dnsCtx context.Context addressLimit bool ) - metadata.ResetRuleCache() - metadata.DestinationAddresses = nil dnsCtx, transport, transportStrategy, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true) + dnsCtx = adapter.OverrideContext(dnsCtx) if strategy == dns.DomainStrategyAsIS { strategy = transportStrategy } diff --git a/route/rule_default.go b/route/rule_default.go index d1d13f7d72..53e53bdf86 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -205,7 +205,7 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.allItems = append(rule.allItems, item) } if len(options.RuleSet) > 0 { - item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource) + item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource, false) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } diff --git a/route/rule_dns.go b/route/rule_dns.go index 955526fc6f..1b79d30b25 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -219,7 +219,7 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options rule.allItems = append(rule.allItems, item) } if len(options.RuleSet) > 0 { - item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource) + item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource, options.RuleSetIPCIDRAcceptEmpty) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } diff --git a/route/rule_item_cidr.go b/route/rule_item_cidr.go index 85b9c8d7d3..be0bb1369c 100644 --- a/route/rule_item_cidr.go +++ b/route/rule_item_cidr.go @@ -75,18 +75,19 @@ func NewRawIPCIDRItem(isSource bool, ipSet *netipx.IPSet) *IPCIDRItem { func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool { if r.isSource || metadata.IPCIDRMatchSource { return r.ipSet.Contains(metadata.Source.Addr) - } else { - if metadata.Destination.IsIP() { - return r.ipSet.Contains(metadata.Destination.Addr) - } else { - for _, address := range metadata.DestinationAddresses { - if r.ipSet.Contains(address) { - return true - } + } + if metadata.Destination.IsIP() { + return r.ipSet.Contains(metadata.Destination.Addr) + } + if len(metadata.DestinationAddresses) > 0 { + for _, address := range metadata.DestinationAddresses { + if r.ipSet.Contains(address) { + return true } } + return false } - return false + return metadata.IPCIDRAcceptEmpty } func (r *IPCIDRItem) String() string { diff --git a/route/rule_item_rule_set.go b/route/rule_item_rule_set.go index 4ecf8c18f0..b80fca995c 100644 --- a/route/rule_item_rule_set.go +++ b/route/rule_item_rule_set.go @@ -15,14 +15,16 @@ type RuleSetItem struct { router adapter.Router tagList []string setList []adapter.RuleSet - ipcidrMatchSource bool + ipCidrMatchSource bool + ipCidrAcceptEmpty bool } -func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource bool) *RuleSetItem { +func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource bool, ipCidrAcceptEmpty bool) *RuleSetItem { return &RuleSetItem{ router: router, tagList: tagList, - ipcidrMatchSource: ipCIDRMatchSource, + ipCidrMatchSource: ipCIDRMatchSource, + ipCidrAcceptEmpty: ipCidrAcceptEmpty, } } @@ -39,7 +41,8 @@ func (r *RuleSetItem) Start() error { } func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { - metadata.IPCIDRMatchSource = r.ipcidrMatchSource + metadata.IPCIDRMatchSource = r.ipCidrMatchSource + metadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty for _, ruleSet := range r.setList { if ruleSet.Match(metadata) { return true @@ -49,7 +52,7 @@ func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { } func (r *RuleSetItem) ContainsDestinationIPCIDRRule() bool { - if r.ipcidrMatchSource { + if r.ipCidrMatchSource { return false } return common.Any(r.setList, func(ruleSet adapter.RuleSet) bool { From 45eb3b98fe350b5bdb69fba0def76c64873b45c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 26 Jun 2024 00:45:10 +0800 Subject: [PATCH 19/31] Unique rule-set names --- cmd/sing-box/cmd_rule_set.go | 2 +- cmd/sing-box/cmd_rule_set_match.go | 4 ++-- common/srs/binary.go | 2 +- docs/changelog.md | 26 +++++++++++++------------- docs/configuration/dns/rule.md | 12 ++++++------ docs/configuration/route/index.md | 2 +- docs/configuration/route/rule.md | 8 ++++---- docs/configuration/rule-set/index.md | 14 +++++++------- docs/deprecated.md | 4 ++-- docs/migration.md | 16 ++++++++-------- docs/migration.zh.md | 4 ++-- option/rule_set.go | 14 +++++++------- route/rule_set.go | 2 +- route/rule_set_local.go | 2 +- route/rule_set_remote.go | 2 +- 15 files changed, 57 insertions(+), 57 deletions(-) diff --git a/cmd/sing-box/cmd_rule_set.go b/cmd/sing-box/cmd_rule_set.go index f4112a087b..242ea8b666 100644 --- a/cmd/sing-box/cmd_rule_set.go +++ b/cmd/sing-box/cmd_rule_set.go @@ -6,7 +6,7 @@ import ( var commandRuleSet = &cobra.Command{ Use: "rule-set", - Short: "Manage rule sets", + Short: "Manage rule-sets", } func init() { diff --git a/cmd/sing-box/cmd_rule_set_match.go b/cmd/sing-box/cmd_rule_set_match.go index fb2560afb1..8bf2ec7e12 100644 --- a/cmd/sing-box/cmd_rule_set_match.go +++ b/cmd/sing-box/cmd_rule_set_match.go @@ -23,7 +23,7 @@ var flagRuleSetMatchFormat string var commandRuleSetMatch = &cobra.Command{ Use: "match ", - Short: "Check if an IP address or a domain matches the rule set", + Short: "Check if an IP address or a domain matches the rule-set", Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { err := ruleSetMatch(args[0], args[1]) @@ -70,7 +70,7 @@ func ruleSetMatch(sourcePath string, domain string) error { return err } default: - return E.New("unknown rule set format: ", flagRuleSetMatchFormat) + return E.New("unknown rule-set format: ", flagRuleSetMatchFormat) } ipAddress := M.ParseAddr(domain) var metadata adapter.InboundContext diff --git a/common/srs/binary.go b/common/srs/binary.go index 0bd5a9099b..69075f7881 100644 --- a/common/srs/binary.go +++ b/common/srs/binary.go @@ -46,7 +46,7 @@ func Read(reader io.Reader, recover bool) (ruleSet option.PlainRuleSet, err erro return } if magicBytes != MagicBytes { - err = E.New("invalid sing-box rule set file") + err = E.New("invalid sing-box rule-set file") return } var version uint8 diff --git a/docs/changelog.md b/docs/changelog.md index 2ba895beb9..6dbef0204c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -338,7 +338,7 @@ See [Address Filter Fields](/configuration/dns/rule#address-filter-fields). Important changes since 1.7: * Migrate cache file from Clash API to independent options **1** -* Introducing [Rule Set](/configuration/rule-set/) **2** +* Introducing [rule-set](/configuration/rule-set/) **2** * Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3** * Allow nested logical rules **4** * Independent `source_ip_is_private` and `ip_is_private` rules **5** @@ -358,7 +358,7 @@ See [Cache File](/configuration/experimental/cache-file/) and **2**: -Rule set is independent collections of rules that can be compiled into binaries to improve performance. +rule-set is independent collections of rules that can be compiled into binaries to improve performance. Compared to legacy GeoIP and Geosite resources, it can include more types of rules, load faster, use less memory, and update automatically. @@ -366,16 +366,16 @@ use less memory, and update automatically. See [Route#rule_set](/configuration/route/#rule_set), [Route Rule](/configuration/route/rule/), [DNS Rule](/configuration/dns/rule/), -[Rule Set](/configuration/rule-set/), +[rule-set](/configuration/rule-set/), [Source Format](/configuration/rule-set/source-format/) and [Headless Rule](/configuration/rule-set/headless-rule/). -For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and -[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets). +For GEO resources migration, see [Migrate GeoIP to rule-sets](/migration/#migrate-geoip-to-rule-sets) and +[Migrate Geosite to rule-sets](/migration/#migrate-geosite-to-rule-sets). **3**: -New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets. +New commands manage GeoIP, Geosite and rule-set resources, and help you migrate GEO resources to rule-sets. **4**: @@ -572,7 +572,7 @@ This change is intended to break incorrect usage and essentially requires no act **1**: -Now the rules in the `rule_set` rule item can be logically considered to be merged into the rule using rule sets, +Now the rules in the `rule_set` rule item can be logically considered to be merged into the rule using rule-sets, rather than completely following the AND logic. #### 1.8.0-alpha.5 @@ -588,7 +588,7 @@ Since GeoIP was deprecated, we made this rule independent, see [Migration](/migr #### 1.8.0-alpha.1 * Migrate cache file from Clash API to independent options **1** -* Introducing [Rule Set](/configuration/rule-set/) **2** +* Introducing [rule-set](/configuration/rule-set/) **2** * Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3** * Allow nested logical rules **4** @@ -599,7 +599,7 @@ See [Cache File](/configuration/experimental/cache-file/) and **2**: -Rule set is independent collections of rules that can be compiled into binaries to improve performance. +rule-set is independent collections of rules that can be compiled into binaries to improve performance. Compared to legacy GeoIP and Geosite resources, it can include more types of rules, load faster, use less memory, and update automatically. @@ -607,16 +607,16 @@ use less memory, and update automatically. See [Route#rule_set](/configuration/route/#rule_set), [Route Rule](/configuration/route/rule/), [DNS Rule](/configuration/dns/rule/), -[Rule Set](/configuration/rule-set/), +[rule-set](/configuration/rule-set/), [Source Format](/configuration/rule-set/source-format/) and [Headless Rule](/configuration/rule-set/headless-rule/). -For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and -[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets). +For GEO resources migration, see [Migrate GeoIP to rule-sets](/migration/#migrate-geoip-to-rule-sets) and +[Migrate Geosite to rule-sets](/migration/#migrate-geosite-to-rule-sets). **3**: -New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets. +New commands manage GeoIP, Geosite and rule-set resources, and help you migrate GEO resources to rule-sets. **4**: diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 0faae0e680..6b3d65194f 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -166,7 +166,7 @@ icon: material/new-box (`source_port` || `source_port_range`) && `other fields` - Additionally, included rule sets can be considered merged rather than as a single rule sub-item. + Additionally, included rule-sets can be considered merged rather than as a single rule sub-item. #### inbound @@ -312,7 +312,7 @@ Match WiFi BSSID. !!! question "Since sing-box 1.8.0" -Match [Rule Set](/configuration/route/#rule_set). +Match [rule-set](/configuration/route/#rule_set). #### rule_set_ipcidr_match_source @@ -322,13 +322,13 @@ Match [Rule Set](/configuration/route/#rule_set). `rule_set_ipcidr_match_source` is renamed to `rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0. -Make `ip_cidr` rule items in rule sets match the source IP. +Make `ip_cidr` rule items in rule-sets match the source IP. #### rule_set_ip_cidr_match_source !!! question "Since sing-box 1.10.0" -Make `ip_cidr` rule items in rule sets match the source IP. +Make `ip_cidr` rule items in rule-sets match the source IP. #### invert @@ -370,7 +370,7 @@ Only takes effect for address requests (A/AAAA/HTTPS). When the query results do !!! info "" - `ip_cidr` items in included rule sets also takes effect as an address filtering field. + `ip_cidr` items in included rule-sets also takes effect as an address filtering field. !!! note "" @@ -398,7 +398,7 @@ Match private IP with query response. !!! question "Since sing-box 1.10.0" -Make `ip_cidr` rules in rule sets accept empty query response. +Make `ip_cidr` rules in rule-sets accept empty query response. ### Logical Fields diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md index 7b2a7e7ef2..507cb140b6 100644 --- a/docs/configuration/route/index.md +++ b/docs/configuration/route/index.md @@ -39,7 +39,7 @@ List of [Route Rule](./rule/) !!! question "Since sing-box 1.8.0" -List of [Rule Set](/configuration/rule-set/) +List of [rule-set](/configuration/rule-set/) #### final diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 1d06a875a1..23d2bf1b0c 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -148,7 +148,7 @@ icon: material/alert-decagram (`source_port` || `source_port_range`) && `other fields` - Additionally, included rule sets can be considered merged rather than as a single rule sub-item. + Additionally, included rule-sets can be considered merged rather than as a single rule sub-item. #### inbound @@ -308,7 +308,7 @@ Match WiFi BSSID. !!! question "Since sing-box 1.8.0" -Match [Rule Set](/configuration/route/#rule_set). +Match [rule-set](/configuration/route/#rule_set). #### rule_set_ipcidr_match_source @@ -318,13 +318,13 @@ Match [Rule Set](/configuration/route/#rule_set). `rule_set_ipcidr_match_source` is renamed to `rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0. -Make `ip_cidr` in rule sets match the source IP. +Make `ip_cidr` in rule-sets match the source IP. #### rule_set_ip_cidr_match_source !!! question "Since sing-box 1.10.0" -Make `ip_cidr` in rule sets match the source IP. +Make `ip_cidr` in rule-sets match the source IP. #### invert diff --git a/docs/configuration/rule-set/index.md b/docs/configuration/rule-set/index.md index ba2f741e4f..b92d80f3d7 100644 --- a/docs/configuration/rule-set/index.md +++ b/docs/configuration/rule-set/index.md @@ -1,4 +1,4 @@ -# Rule Set +# rule-set !!! question "Since sing-box 1.8.0" @@ -50,19 +50,19 @@ ==Required== -Type of Rule Set, `local` or `remote`. +Type of rule-set, `local` or `remote`. #### tag ==Required== -Tag of Rule Set. +Tag of rule-set. #### format ==Required== -Format of Rule Set, `source` or `binary`. +Format of rule-set, `source` or `binary`. ### Local Fields @@ -70,7 +70,7 @@ Format of Rule Set, `source` or `binary`. ==Required== -File path of Rule Set. +File path of rule-set. ### Remote Fields @@ -78,7 +78,7 @@ File path of Rule Set. ==Required== -Download URL of Rule Set. +Download URL of rule-set. #### download_detour @@ -88,6 +88,6 @@ Default outbound will be used if empty. #### update_interval -Update interval of Rule Set. +Update interval of rule-set. `1d` will be used if empty. diff --git a/docs/deprecated.md b/docs/deprecated.md index 249bc492a2..eb0c1925c8 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -33,7 +33,7 @@ The maxmind GeoIP National Database, as an IP classification database, is not entirely suitable for traffic bypassing, and all existing implementations suffer from high memory usage and difficult management. -sing-box 1.8.0 introduces [Rule Set](/configuration/rule-set/), which can completely replace GeoIP, +sing-box 1.8.0 introduces [rule-set](/configuration/rule-set/), which can completely replace GeoIP, check [Migration](/migration/#migrate-geoip-to-rule-sets). #### Geosite @@ -43,7 +43,7 @@ Geosite is deprecated and may be removed in the future. Geosite, the `domain-list-community` project maintained by V2Ray as an early traffic bypassing solution, suffers from a number of problems, including lack of maintenance, inaccurate rules, and difficult management. -sing-box 1.8.0 introduces [Rule Set](/configuration/rule-set/), which can completely replace Geosite, +sing-box 1.8.0 introduces [rule-set](/configuration/rule-set/), which can completely replace Geosite, check [Migration](/migration/#migrate-geosite-to-rule-sets). ## 1.6.0 diff --git a/docs/migration.md b/docs/migration.md index c696a836b1..2654ae4372 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -128,7 +128,7 @@ which will disrupt the existing `process_path` use cases in Windows. } ``` -### :material-checkbox-intermediate: Migrate GeoIP to rule sets +### :material-checkbox-intermediate: Migrate GeoIP to rule-sets !!! info "References" @@ -136,11 +136,11 @@ which will disrupt the existing `process_path` use cases in Windows. [Route](/configuration/route/) / [Route Rule](/configuration/route/rule/) / [DNS Rule](/configuration/dns/rule/) / - [Rule Set](/configuration/rule-set/) + [rule-set](/configuration/rule-set/) !!! tip - `sing-box geoip` commands can help you convert custom GeoIP into rule sets. + `sing-box geoip` commands can help you convert custom GeoIP into rule-sets. === ":material-card-remove: Deprecated" @@ -207,13 +207,13 @@ which will disrupt the existing `process_path` use cases in Windows. }, "experimental": { "cache_file": { - "enabled": true // required to save Rule Set cache + "enabled": true // required to save rule-set cache } } } ``` -### :material-checkbox-intermediate: Migrate Geosite to rule sets +### :material-checkbox-intermediate: Migrate Geosite to rule-sets !!! info "References" @@ -221,11 +221,11 @@ which will disrupt the existing `process_path` use cases in Windows. [Route](/configuration/route/) / [Route Rule](/configuration/route/rule/) / [DNS Rule](/configuration/dns/rule/) / - [Rule Set](/configuration/rule-set/) + [rule-set](/configuration/rule-set/) !!! tip - `sing-box geosite` commands can help you convert custom Geosite into rule sets. + `sing-box geosite` commands can help you convert custom Geosite into rule-sets. === ":material-card-remove: Deprecated" @@ -268,7 +268,7 @@ which will disrupt the existing `process_path` use cases in Windows. }, "experimental": { "cache_file": { - "enabled": true // required to save Rule Set cache + "enabled": true // required to save rule-set cache } } } diff --git a/docs/migration.zh.md b/docs/migration.zh.md index 9fe40cc9fd..9a275399b8 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -206,7 +206,7 @@ sing-box 1.9.0 使 QueryFullProcessImageNameW 输出 Win32 路径(如 `C:\fold }, "experimental": { "cache_file": { - "enabled": true // required to save Rule Set cache + "enabled": true // required to save rule-set cache } } } @@ -267,7 +267,7 @@ sing-box 1.9.0 使 QueryFullProcessImageNameW 输出 Win32 路径(如 `C:\fold }, "experimental": { "cache_file": { - "enabled": true // required to save Rule Set cache + "enabled": true // required to save rule-set cache } } } diff --git a/option/rule_set.go b/option/rule_set.go index 5498400f2f..ec32d0a13a 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -31,7 +31,7 @@ func (r RuleSet) MarshalJSON() ([]byte, error) { case C.RuleSetTypeRemote: v = r.RemoteOptions default: - return nil, E.New("unknown rule set type: " + r.Type) + return nil, E.New("unknown rule-set type: " + r.Type) } return MarshallObjects((_RuleSet)(r), v) } @@ -49,7 +49,7 @@ func (r *RuleSet) UnmarshalJSON(bytes []byte) error { return E.New("missing format") case C.RuleSetFormatSource, C.RuleSetFormatBinary: default: - return E.New("unknown rule set format: " + r.Format) + return E.New("unknown rule-set format: " + r.Format) } var v any switch r.Type { @@ -60,7 +60,7 @@ func (r *RuleSet) UnmarshalJSON(bytes []byte) error { case "": return E.New("missing type") default: - return E.New("unknown rule set type: " + r.Type) + return E.New("unknown rule-set type: " + r.Type) } err = UnmarshallExcluded(bytes, (*_RuleSet)(r), v) if err != nil { @@ -188,7 +188,7 @@ func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { case C.RuleSetVersion1, C.RuleSetVersion2: v = r.Options default: - return nil, E.New("unknown rule set version: ", r.Version) + return nil, E.New("unknown rule-set version: ", r.Version) } return MarshallObjects((_PlainRuleSetCompat)(r), v) } @@ -203,9 +203,9 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { case C.RuleSetVersion1, C.RuleSetVersion2: v = &r.Options case 0: - return E.New("missing rule set version") + return E.New("missing rule-set version") default: - return E.New("unknown rule set version: ", r.Version) + return E.New("unknown rule-set version: ", r.Version) } err = UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v) if err != nil { @@ -220,7 +220,7 @@ func (r PlainRuleSetCompat) Upgrade() PlainRuleSet { case C.RuleSetVersion1, C.RuleSetVersion2: result = r.Options default: - panic("unknown rule set version: " + F.ToString(r.Version)) + panic("unknown rule-set version: " + F.ToString(r.Version)) } return result } diff --git a/route/rule_set.go b/route/rule_set.go index ff28858e58..92952c51d3 100644 --- a/route/rule_set.go +++ b/route/rule_set.go @@ -25,7 +25,7 @@ func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.Contex case C.RuleSetTypeRemote: return NewRemoteRuleSet(ctx, router, logger, options), nil default: - return nil, E.New("unknown rule set type: ", options.Type) + return nil, E.New("unknown rule-set type: ", options.Type) } } diff --git a/route/rule_set_local.go b/route/rule_set_local.go index 5344618320..aa8c3ff693 100644 --- a/route/rule_set_local.go +++ b/route/rule_set_local.go @@ -51,7 +51,7 @@ func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleS return nil, err } default: - return nil, E.New("unknown rule set format: ", options.Format) + return nil, E.New("unknown rule-set format: ", options.Format) } rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) var err error diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go index bf0cfe204c..1473a494a9 100644 --- a/route/rule_set_remote.go +++ b/route/rule_set_remote.go @@ -175,7 +175,7 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error { return err } default: - return E.New("unknown rule set format: ", s.options.Format) + return E.New("unknown rule-set format: ", s.options.Format) } rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) for i, ruleOptions := range plainRuleSet.Rules { From 33ce490f73053ef1aa3308221777272a03ed9149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 26 Jun 2024 00:43:51 +0800 Subject: [PATCH 20/31] Add inline rule-set & Add reload for local rule-set --- cmd/sing-box/cmd_rule_set_compile.go | 5 +- cmd/sing-box/cmd_rule_set_match.go | 5 +- cmd/sing-box/cmd_rule_set_upgrade.go | 5 +- common/tls/ech_server.go | 185 +++++++++------------------ common/tls/std_server.go | 61 +++------ constant/rule.go | 1 + docs/configuration/rule-set/index.md | 104 +++++++++------ docs/configuration/shared/tls.md | 18 ++- docs/configuration/shared/tls.zh.md | 16 ++- go.mod | 4 +- option/rule_set.go | 33 +++-- route/rule_set.go | 4 +- route/rule_set_local.go | 127 +++++++++++++----- route/rule_set_remote.go | 5 +- 14 files changed, 302 insertions(+), 271 deletions(-) diff --git a/cmd/sing-box/cmd_rule_set_compile.go b/cmd/sing-box/cmd_rule_set_compile.go index 7e3753c928..4fae4d99e5 100644 --- a/cmd/sing-box/cmd_rule_set_compile.go +++ b/cmd/sing-box/cmd_rule_set_compile.go @@ -56,7 +56,10 @@ func compileRuleSet(sourcePath string) error { if err != nil { return err } - ruleSet := plainRuleSet.Upgrade() + ruleSet, err := plainRuleSet.Upgrade() + if err != nil { + return err + } var outputPath string if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput { if strings.HasSuffix(sourcePath, ".json") { diff --git a/cmd/sing-box/cmd_rule_set_match.go b/cmd/sing-box/cmd_rule_set_match.go index 8bf2ec7e12..937458f2c6 100644 --- a/cmd/sing-box/cmd_rule_set_match.go +++ b/cmd/sing-box/cmd_rule_set_match.go @@ -63,7 +63,10 @@ func ruleSetMatch(sourcePath string, domain string) error { if err != nil { return err } - plainRuleSet = compat.Upgrade() + plainRuleSet, err = compat.Upgrade() + if err != nil { + return err + } case C.RuleSetFormatBinary: plainRuleSet, err = srs.Read(bytes.NewReader(content), false) if err != nil { diff --git a/cmd/sing-box/cmd_rule_set_upgrade.go b/cmd/sing-box/cmd_rule_set_upgrade.go index 0ec039fd71..e885d849e4 100644 --- a/cmd/sing-box/cmd_rule_set_upgrade.go +++ b/cmd/sing-box/cmd_rule_set_upgrade.go @@ -61,7 +61,10 @@ func upgradeRuleSet(sourcePath string) error { log.Info("already up-to-date") return nil } - plainRuleSet := plainRuleSetCompat.Upgrade() + plainRuleSet, err := plainRuleSetCompat.Upgrade() + if err != nil { + return err + } buffer := new(bytes.Buffer) encoder := json.NewEncoder(buffer) encoder.SetIndent("", " ") diff --git a/common/tls/ech_server.go b/common/tls/ech_server.go index 43ddd820ec..ac3e6279c3 100644 --- a/common/tls/ech_server.go +++ b/common/tls/ech_server.go @@ -11,12 +11,11 @@ import ( "strings" cftls "github.com/sagernet/cloudflare-tls" + "github.com/sagernet/fswatch" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/ntp" - - "github.com/fsnotify/fsnotify" ) type echServerConfig struct { @@ -26,9 +25,8 @@ type echServerConfig struct { key []byte certificatePath string keyPath string - watcher *fsnotify.Watcher echKeyPath string - echWatcher *fsnotify.Watcher + watcher *fswatch.Watcher } func (c *echServerConfig) ServerName() string { @@ -66,146 +64,84 @@ func (c *echServerConfig) Clone() Config { } func (c *echServerConfig) Start() error { - if c.certificatePath != "" && c.keyPath != "" { - err := c.startWatcher() - if err != nil { - c.logger.Warn("create fsnotify watcher: ", err) - } - } - if c.echKeyPath != "" { - err := c.startECHWatcher() - if err != nil { - c.logger.Warn("create fsnotify watcher: ", err) - } + err := c.startWatcher() + if err != nil { + c.logger.Warn("create credentials watcher: ", err) } return nil } func (c *echServerConfig) startWatcher() error { - watcher, err := fsnotify.NewWatcher() - if err != nil { - return err - } + var watchPath []string if c.certificatePath != "" { - err = watcher.Add(c.certificatePath) - if err != nil { - return err - } + watchPath = append(watchPath, c.certificatePath) } if c.keyPath != "" { - err = watcher.Add(c.keyPath) - if err != nil { - return err - } + watchPath = append(watchPath, c.keyPath) + } + if c.echKeyPath != "" { + watchPath = append(watchPath, c.echKeyPath) + } + if len(watchPath) == 0 { + return nil + } + watcher, err := fswatch.NewWatcher(fswatch.Options{ + Path: watchPath, + Callback: func(path string) { + err := c.credentialsUpdated(path) + if err != nil { + c.logger.Error(E.Cause(err, "reload credentials from ", path)) + } + }, + }) + if err != nil { + return err } c.watcher = watcher - go c.loopUpdate() return nil } -func (c *echServerConfig) loopUpdate() { - for { - select { - case event, ok := <-c.watcher.Events: - if !ok { - return - } - if event.Op&fsnotify.Write != fsnotify.Write { - continue - } - err := c.reloadKeyPair() +func (c *echServerConfig) credentialsUpdated(path string) error { + if path == c.certificatePath || path == c.keyPath { + if path == c.certificatePath { + certificate, err := os.ReadFile(c.certificatePath) if err != nil { - c.logger.Error(E.Cause(err, "reload TLS key pair")) + return err } - case err, ok := <-c.watcher.Errors: - if !ok { - return + c.certificate = certificate + } else { + key, err := os.ReadFile(c.keyPath) + if err != nil { + return err } - c.logger.Error(E.Cause(err, "fsnotify error")) + c.key = key } - } -} - -func (c *echServerConfig) reloadKeyPair() error { - if c.certificatePath != "" { - certificate, err := os.ReadFile(c.certificatePath) + keyPair, err := cftls.X509KeyPair(c.certificate, c.key) if err != nil { - return E.Cause(err, "reload certificate from ", c.certificatePath) + return E.Cause(err, "parse key pair") } - c.certificate = certificate - } - if c.keyPath != "" { - key, err := os.ReadFile(c.keyPath) + c.config.Certificates = []cftls.Certificate{keyPair} + c.logger.Info("reloaded TLS certificate") + } else { + echKeyContent, err := os.ReadFile(c.echKeyPath) if err != nil { - return E.Cause(err, "reload key from ", c.keyPath) + return err } - c.key = key - } - keyPair, err := cftls.X509KeyPair(c.certificate, c.key) - if err != nil { - return E.Cause(err, "reload key pair") - } - c.config.Certificates = []cftls.Certificate{keyPair} - c.logger.Info("reloaded TLS certificate") - return nil -} - -func (c *echServerConfig) startECHWatcher() error { - watcher, err := fsnotify.NewWatcher() - if err != nil { - return err - } - err = watcher.Add(c.echKeyPath) - if err != nil { - return err - } - c.echWatcher = watcher - go c.loopECHUpdate() - return nil -} - -func (c *echServerConfig) loopECHUpdate() { - for { - select { - case event, ok := <-c.echWatcher.Events: - if !ok { - return - } - if event.Op&fsnotify.Write != fsnotify.Write { - continue - } - err := c.reloadECHKey() - if err != nil { - c.logger.Error(E.Cause(err, "reload ECH key")) - } - case err, ok := <-c.echWatcher.Errors: - if !ok { - return - } - c.logger.Error(E.Cause(err, "fsnotify error")) + block, rest := pem.Decode(echKeyContent) + if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 { + return E.New("invalid ECH keys pem") } + echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes) + if err != nil { + return E.Cause(err, "parse ECH keys") + } + echKeySet, err := cftls.EXP_NewECHKeySet(echKeys) + if err != nil { + return E.Cause(err, "create ECH key set") + } + c.config.ServerECHProvider = echKeySet + c.logger.Info("reloaded ECH keys") } -} - -func (c *echServerConfig) reloadECHKey() error { - echKeyContent, err := os.ReadFile(c.echKeyPath) - if err != nil { - return err - } - block, rest := pem.Decode(echKeyContent) - if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 { - return E.New("invalid ECH keys pem") - } - echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes) - if err != nil { - return E.Cause(err, "parse ECH keys") - } - echKeySet, err := cftls.EXP_NewECHKeySet(echKeys) - if err != nil { - return E.Cause(err, "create ECH key set") - } - c.config.ServerECHProvider = echKeySet - c.logger.Info("reloaded ECH keys") return nil } @@ -213,12 +149,7 @@ func (c *echServerConfig) Close() error { var err error if c.watcher != nil { err = E.Append(err, c.watcher.Close(), func(err error) error { - return E.Cause(err, "close certificate watcher") - }) - } - if c.echWatcher != nil { - err = E.Append(err, c.echWatcher.Close(), func(err error) error { - return E.Cause(err, "close ECH key watcher") + return E.Cause(err, "close credentials watcher") }) } return err diff --git a/common/tls/std_server.go b/common/tls/std_server.go index 7184bdb36b..7001bd3a04 100644 --- a/common/tls/std_server.go +++ b/common/tls/std_server.go @@ -7,14 +7,13 @@ import ( "os" "strings" + "github.com/sagernet/fswatch" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/ntp" - - "github.com/fsnotify/fsnotify" ) var errInsecureUnused = E.New("tls: insecure unused") @@ -27,7 +26,7 @@ type STDServerConfig struct { key []byte certificatePath string keyPath string - watcher *fsnotify.Watcher + watcher *fswatch.Watcher } func (c *STDServerConfig) ServerName() string { @@ -88,59 +87,37 @@ func (c *STDServerConfig) Start() error { } func (c *STDServerConfig) startWatcher() error { - watcher, err := fsnotify.NewWatcher() - if err != nil { - return err - } + var watchPath []string if c.certificatePath != "" { - err = watcher.Add(c.certificatePath) - if err != nil { - return err - } + watchPath = append(watchPath, c.certificatePath) } if c.keyPath != "" { - err = watcher.Add(c.keyPath) - if err != nil { - return err - } + watchPath = append(watchPath, c.keyPath) } - c.watcher = watcher - go c.loopUpdate() - return nil -} - -func (c *STDServerConfig) loopUpdate() { - for { - select { - case event, ok := <-c.watcher.Events: - if !ok { - return - } - if event.Op&fsnotify.Write != fsnotify.Write { - continue - } - err := c.reloadKeyPair() + watcher, err := fswatch.NewWatcher(fswatch.Options{ + Path: watchPath, + Callback: func(path string) { + err := c.certificateUpdated(path) if err != nil { - c.logger.Error(E.Cause(err, "reload TLS key pair")) - } - case err, ok := <-c.watcher.Errors: - if !ok { - return + c.logger.Error(err) } - c.logger.Error(E.Cause(err, "fsnotify error")) - } + }, + }) + if err != nil { + return err } + c.watcher = watcher + return nil } -func (c *STDServerConfig) reloadKeyPair() error { - if c.certificatePath != "" { +func (c *STDServerConfig) certificateUpdated(path string) error { + if path == c.certificatePath { certificate, err := os.ReadFile(c.certificatePath) if err != nil { return E.Cause(err, "reload certificate from ", c.certificatePath) } c.certificate = certificate - } - if c.keyPath != "" { + } else if path == c.keyPath { key, err := os.ReadFile(c.keyPath) if err != nil { return E.Cause(err, "reload key from ", c.keyPath) diff --git a/constant/rule.go b/constant/rule.go index fb0f39e22e..718e79a5e6 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -11,6 +11,7 @@ const ( ) const ( + RuleSetTypeInline = "inline" RuleSetTypeLocal = "local" RuleSetTypeRemote = "remote" RuleSetFormatSource = "source" diff --git a/docs/configuration/rule-set/index.md b/docs/configuration/rule-set/index.md index b92d80f3d7..dfe71d9e39 100644 --- a/docs/configuration/rule-set/index.md +++ b/docs/configuration/rule-set/index.md @@ -1,48 +1,56 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.10.0" + + :material-plus: `type: inline` + # rule-set !!! question "Since sing-box 1.8.0" ### Structure -```json -{ - "type": "", - "tag": "", - "format": "", - - ... // Typed Fields -} -``` - -#### Local Structure - -```json -{ - "type": "local", - - ... - - "path": "" -} -``` - -#### Remote Structure - -!!! info "" - - Remote rule-set will be cached if `experimental.cache_file.enabled`. - -```json -{ - "type": "remote", - - ..., - - "url": "", - "download_detour": "", - "update_interval": "" -} -``` +=== "Inline" + + !!! question "Since sing-box 1.10.0" + + ```json + { + "type": "inline", // optional + "tag": "", + "rules": [] + } + ``` + +=== "Local File" + + ```json + { + "type": "local", + "tag": "", + "format": "source", // or binary + "path": "" + } + ``` + +=== "Remote File" + + !!! info "" + + Remote rule-set will be cached if `experimental.cache_file.enabled`. + + ```json + { + "type": "remote", + "tag": "", + "format": "source", // or binary + "url": "", + "download_detour": "", // optional + "update_interval": "" // optional + } + ``` ### Fields @@ -58,11 +66,23 @@ Type of rule-set, `local` or `remote`. Tag of rule-set. +### Inline Fields + +!!! question "Since sing-box 1.10.0" + +#### rules + +==Required== + +List of [Headless Rule](./headless-rule.md/). + +### Local or Remote Fields + #### format ==Required== -Format of rule-set, `source` or `binary`. +Format of rule-set file, `source` or `binary`. ### Local Fields @@ -70,6 +90,10 @@ Format of rule-set, `source` or `binary`. ==Required== +!!! note "" + + Will be automatically reloaded if file modified since sing-box 1.10.0. + File path of rule-set. ### Remote Fields diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md index b1441a8abc..799aa0b09d 100644 --- a/docs/configuration/shared/tls.md +++ b/docs/configuration/shared/tls.md @@ -178,6 +178,10 @@ The server certificate line array, in PEM format. #### certificate_path +!!! note "" + + Will be automatically reloaded if file modified. + The path to the server certificate, in PEM format. #### key @@ -190,6 +194,10 @@ The server private key line array, in PEM format. ==Server only== +!!! note "" + + Will be automatically reloaded if file modified. + The path to the server private key, in PEM format. ## Custom TLS support @@ -266,6 +274,10 @@ ECH key line array, in PEM format. ==Server only== +!!! note "" + + Will be automatically reloaded if file modified. + The path to ECH key, in PEM format. #### config @@ -397,8 +409,4 @@ A hexadecimal string with zero to eight digits. The maximum time difference between the server and the client. -Check disabled if empty. - -### Reload - -For server configuration, certificate, key and ECH key will be automatically reloaded if modified. \ No newline at end of file +Check disabled if empty. \ No newline at end of file diff --git a/docs/configuration/shared/tls.zh.md b/docs/configuration/shared/tls.zh.md index 360c453642..68de98459f 100644 --- a/docs/configuration/shared/tls.zh.md +++ b/docs/configuration/shared/tls.zh.md @@ -176,12 +176,20 @@ TLS 版本值: #### certificate_path +!!! note "" + + 文件更改时将自动重新加载。 + 服务器 PEM 证书路径。 #### key ==仅服务器== +!!! note "" + + 文件更改时将自动重新加载。 + 服务器 PEM 私钥行数组。 #### key_path @@ -258,6 +266,10 @@ ECH PEM 密钥行数组 ==仅服务器== +!!! note "" + + 文件更改时将自动重新加载。 + ECH PEM 密钥路径 #### config @@ -384,7 +396,3 @@ ACME DNS01 验证字段。如果配置,将禁用其他验证方法。 服务器与和客户端之间允许的最大时间差。 默认禁用检查。 - -### 重载 - -对于服务器配置,如果修改,证书和密钥将自动重新加载。 \ No newline at end of file diff --git a/go.mod b/go.mod index 83dc74abe5..1f3e19dd33 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/caddyserver/certmagic v0.20.0 github.com/cloudflare/circl v1.3.7 github.com/cretz/bine v0.2.0 - github.com/fsnotify/fsnotify v1.7.0 github.com/go-chi/chi/v5 v5.0.12 github.com/go-chi/cors v1.2.1 github.com/go-chi/render v1.0.3 @@ -23,6 +22,7 @@ require ( github.com/oschwald/maxminddb-golang v1.12.0 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 + github.com/sagernet/fswatch v0.1.1 github.com/sagernet/gomobile v0.1.3 github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f github.com/sagernet/quic-go v0.46.0-beta.4 @@ -59,6 +59,7 @@ require ( github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/brotli v1.0.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gaukas/godicttls v0.0.4 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect @@ -81,7 +82,6 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect - github.com/sagernet/fswatch v0.1.1 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/nftables v0.3.0-beta.4 // indirect github.com/spf13/pflag v1.0.5 // indirect diff --git a/option/rule_set.go b/option/rule_set.go index ec32d0a13a..002cadd46e 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -17,6 +17,7 @@ type _RuleSet struct { Type string `json:"type"` Tag string `json:"tag"` Format string `json:"format"` + InlineOptions PlainRuleSet `json:"-"` LocalOptions LocalRuleSet `json:"-"` RemoteOptions RemoteRuleSet `json:"-"` } @@ -26,6 +27,9 @@ type RuleSet _RuleSet func (r RuleSet) MarshalJSON() ([]byte, error) { var v any switch r.Type { + case "", C.RuleSetTypeInline: + r.Type = "" + v = r.InlineOptions case C.RuleSetTypeLocal: v = r.LocalOptions case C.RuleSetTypeRemote: @@ -44,21 +48,26 @@ func (r *RuleSet) UnmarshalJSON(bytes []byte) error { if r.Tag == "" { return E.New("missing tag") } - switch r.Format { - case "": - return E.New("missing format") - case C.RuleSetFormatSource, C.RuleSetFormatBinary: - default: - return E.New("unknown rule-set format: " + r.Format) + if r.Type != C.RuleSetTypeInline { + switch r.Format { + case "": + return E.New("missing format") + case C.RuleSetFormatSource, C.RuleSetFormatBinary: + default: + return E.New("unknown rule-set format: " + r.Format) + } + } else { + r.Format = "" } var v any switch r.Type { + case "", C.RuleSetTypeInline: + r.Type = C.RuleSetTypeInline + v = &r.InlineOptions case C.RuleSetTypeLocal: v = &r.LocalOptions case C.RuleSetTypeRemote: v = &r.RemoteOptions - case "": - return E.New("missing type") default: return E.New("unknown rule-set type: " + r.Type) } @@ -214,15 +223,13 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { return nil } -func (r PlainRuleSetCompat) Upgrade() PlainRuleSet { - var result PlainRuleSet +func (r PlainRuleSetCompat) Upgrade() (PlainRuleSet, error) { switch r.Version { case C.RuleSetVersion1, C.RuleSetVersion2: - result = r.Options default: - panic("unknown rule-set version: " + F.ToString(r.Version)) + return PlainRuleSet{}, E.New("unknown rule-set version: " + F.ToString(r.Version)) } - return result + return r.Options, nil } type PlainRuleSet struct { diff --git a/route/rule_set.go b/route/rule_set.go index 92952c51d3..fd960b5e25 100644 --- a/route/rule_set.go +++ b/route/rule_set.go @@ -20,8 +20,8 @@ import ( func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) { switch options.Type { - case C.RuleSetTypeLocal: - return NewLocalRuleSet(router, options) + case C.RuleSetTypeInline, C.RuleSetTypeLocal, "": + return NewLocalRuleSet(router, logger, options) case C.RuleSetTypeRemote: return NewRemoteRuleSet(ctx, router, logger, options), nil default: diff --git a/route/rule_set_local.go b/route/rule_set_local.go index aa8c3ff693..cf38f16815 100644 --- a/route/rule_set_local.go +++ b/route/rule_set_local.go @@ -3,8 +3,10 @@ package route import ( "context" "os" + "path/filepath" "strings" + "github.com/sagernet/fswatch" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/srs" C "github.com/sagernet/sing-box/constant" @@ -14,6 +16,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/x/list" "go4.org/netipx" @@ -22,50 +25,55 @@ import ( var _ adapter.RuleSet = (*LocalRuleSet)(nil) type LocalRuleSet struct { - tag string - rules []adapter.HeadlessRule - metadata adapter.RuleSetMetadata - refs atomic.Int32 + router adapter.Router + logger logger.Logger + tag string + rules []adapter.HeadlessRule + metadata adapter.RuleSetMetadata + fileFormat string + watcher *fswatch.Watcher + refs atomic.Int32 } -func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) { - var plainRuleSet option.PlainRuleSet - switch options.Format { - case C.RuleSetFormatSource, "": - content, err := os.ReadFile(options.LocalOptions.Path) - if err != nil { - return nil, err - } - compat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content) - if err != nil { - return nil, err +func NewLocalRuleSet(router adapter.Router, logger logger.Logger, options option.RuleSet) (*LocalRuleSet, error) { + ruleSet := &LocalRuleSet{ + router: router, + logger: logger, + tag: options.Tag, + fileFormat: options.Format, + } + if options.Type == C.RuleSetTypeInline { + if len(options.InlineOptions.Rules) == 0 { + return nil, E.New("empty inline rule-set") } - plainRuleSet = compat.Upgrade() - case C.RuleSetFormatBinary: - setFile, err := os.Open(options.LocalOptions.Path) + err := ruleSet.reloadRules(options.InlineOptions.Rules) if err != nil { return nil, err } - plainRuleSet, err = srs.Read(setFile, false) + } else { + err := ruleSet.reloadFile(options.LocalOptions.Path) if err != nil { return nil, err } - default: - return nil, E.New("unknown rule-set format: ", options.Format) } - rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) - var err error - for i, ruleOptions := range plainRuleSet.Rules { - rules[i], err = NewHeadlessRule(router, ruleOptions) + if options.Type == C.RuleSetTypeLocal { + var watcher *fswatch.Watcher + filePath, _ := filepath.Abs(options.LocalOptions.Path) + watcher, err := fswatch.NewWatcher(fswatch.Options{ + Path: []string{filePath}, + Callback: func(path string) { + uErr := ruleSet.reloadFile(path) + if uErr != nil { + logger.Error(E.Cause(uErr, "reload rule-set ", options.Tag)) + } + }, + }) if err != nil { - return nil, E.Cause(err, "parse rule_set.rules.[", i, "]") + return nil, err } + ruleSet.watcher = watcher } - var metadata adapter.RuleSetMetadata - metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) - metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) - metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule) - return &LocalRuleSet{tag: options.Tag, rules: rules, metadata: metadata}, nil + return ruleSet, nil } func (s *LocalRuleSet) Name() string { @@ -77,6 +85,61 @@ func (s *LocalRuleSet) String() string { } func (s *LocalRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error { + if s.watcher != nil { + err := s.watcher.Start() + if err != nil { + s.logger.Error(E.Cause(err, "watch rule-set file")) + } + } + return nil +} + +func (s *LocalRuleSet) reloadFile(path string) error { + var plainRuleSet option.PlainRuleSet + switch s.fileFormat { + case C.RuleSetFormatSource, "": + content, err := os.ReadFile(path) + if err != nil { + return err + } + compat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content) + if err != nil { + return err + } + plainRuleSet, err = compat.Upgrade() + if err != nil { + return err + } + case C.RuleSetFormatBinary: + setFile, err := os.Open(path) + if err != nil { + return err + } + plainRuleSet, err = srs.Read(setFile, false) + if err != nil { + return err + } + default: + return E.New("unknown rule-set format: ", s.fileFormat) + } + return s.reloadRules(plainRuleSet.Rules) +} + +func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error { + rules := make([]adapter.HeadlessRule, len(headlessRules)) + var err error + for i, ruleOptions := range headlessRules { + rules[i], err = NewHeadlessRule(s.router, ruleOptions) + if err != nil { + return E.Cause(err, "parse rule_set.rules.[", i, "]") + } + } + var metadata adapter.RuleSetMetadata + metadata.ContainsProcessRule = hasHeadlessRule(headlessRules, isProcessHeadlessRule) + metadata.ContainsWIFIRule = hasHeadlessRule(headlessRules, isWIFIHeadlessRule) + metadata.ContainsIPCIDRRule = hasHeadlessRule(headlessRules, isIPCIDRHeadlessRule) + s.rules = rules + s.metadata = metadata return nil } @@ -117,7 +180,7 @@ func (s *LocalRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetU func (s *LocalRuleSet) Close() error { s.rules = nil - return nil + return common.Close(common.PtrOrNil(s.watcher)) } func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go index 1473a494a9..03662ee42c 100644 --- a/route/rule_set_remote.go +++ b/route/rule_set_remote.go @@ -168,7 +168,10 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error { if err != nil { return err } - plainRuleSet = compat.Upgrade() + plainRuleSet, err = compat.Upgrade() + if err != nil { + return err + } case C.RuleSetFormatBinary: plainRuleSet, err = srs.Read(bytes.NewReader(content), false) if err != nil { From 75f9b22f308e8ff8f3b9b48c38b006eb7f07be78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 7 Jul 2024 15:45:50 +0800 Subject: [PATCH 21/31] Improve QUIC sniffer --- adapter/inbound.go | 9 +- common/ja3/LICENSE | 29 +++ common/ja3/README.md | 3 + common/ja3/error.go | 31 +++ common/ja3/ja3.go | 83 +++++++ common/ja3/parser.go | 357 +++++++++++++++++++++++++++ common/sniff/bittorrent.go | 44 ++-- common/sniff/bittorrent_test.go | 11 +- common/sniff/dns.go | 20 +- common/sniff/dtls.go | 13 +- common/sniff/dtls_test.go | 7 +- common/sniff/http.go | 8 +- common/sniff/http_test.go | 7 +- common/sniff/quic.go | 214 ++++++++++------ common/sniff/quic_blacklist.go | 24 ++ common/sniff/quic_test.go | 56 ++++- common/sniff/sniff.go | 28 +-- common/sniff/stun.go | 11 +- common/sniff/stun_test.go | 7 +- common/sniff/tls.go | 8 +- constant/protocol.go | 8 + docs/configuration/route/rule.md | 15 +- docs/configuration/route/rule.zh.md | 15 +- docs/configuration/route/sniff.md | 29 ++- docs/configuration/route/sniff.zh.md | 29 ++- go.mod | 2 +- option/rule.go | 1 + route/router.go | 139 ++++++----- route/rule_default.go | 5 + route/rule_item_client.go | 37 +++ 30 files changed, 1013 insertions(+), 237 deletions(-) create mode 100644 common/ja3/LICENSE create mode 100644 common/ja3/README.md create mode 100644 common/ja3/error.go create mode 100644 common/ja3/ja3.go create mode 100644 common/ja3/parser.go create mode 100644 common/sniff/quic_blacklist.go create mode 100644 route/rule_item_client.go diff --git a/adapter/inbound.go b/adapter/inbound.go index 56eb562308..ffc4c56454 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -31,11 +31,16 @@ type InboundContext struct { Network string Source M.Socksaddr Destination M.Socksaddr - Domain string - Protocol string User string Outbound string + // sniffer + + Protocol string + Domain string + Client string + SniffContext any + // cache InboundDetour string diff --git a/common/ja3/LICENSE b/common/ja3/LICENSE new file mode 100644 index 0000000000..b42ec95db2 --- /dev/null +++ b/common/ja3/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2018, Open Systems AG +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/common/ja3/README.md b/common/ja3/README.md new file mode 100644 index 0000000000..5c0bd8ae27 --- /dev/null +++ b/common/ja3/README.md @@ -0,0 +1,3 @@ +# JA3 + +mod from: https://github.com/open-ch/ja3 \ No newline at end of file diff --git a/common/ja3/error.go b/common/ja3/error.go new file mode 100644 index 0000000000..cab8549255 --- /dev/null +++ b/common/ja3/error.go @@ -0,0 +1,31 @@ +// Copyright (c) 2018, Open Systems AG. All rights reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. + +package ja3 + +import "fmt" + +// Error types +const ( + LengthErr string = "length check %v failed" + ContentTypeErr string = "content type not matching" + VersionErr string = "version check %v failed" + HandshakeTypeErr string = "handshake type not matching" + SNITypeErr string = "SNI type not supported" +) + +// ParseError can be encountered while parsing a segment +type ParseError struct { + errType string + check int +} + +func (e *ParseError) Error() string { + if e.errType == LengthErr || e.errType == VersionErr { + return fmt.Sprintf(e.errType, e.check) + } + return fmt.Sprint(e.errType) +} diff --git a/common/ja3/ja3.go b/common/ja3/ja3.go new file mode 100644 index 0000000000..608819fe1e --- /dev/null +++ b/common/ja3/ja3.go @@ -0,0 +1,83 @@ +// Copyright (c) 2018, Open Systems AG. All rights reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. + +package ja3 + +import ( + "crypto/md5" + "encoding/hex" + + "golang.org/x/exp/slices" +) + +type ClientHello struct { + Version uint16 + CipherSuites []uint16 + Extensions []uint16 + EllipticCurves []uint16 + EllipticCurvePF []uint8 + Versions []uint16 + SignatureAlgorithms []uint16 + ServerName string + ja3ByteString []byte + ja3Hash string +} + +func (j *ClientHello) Equals(another *ClientHello, ignoreExtensionsSequence bool) bool { + if j.Version != another.Version { + return false + } + if !slices.Equal(j.CipherSuites, another.CipherSuites) { + return false + } + if !ignoreExtensionsSequence && !slices.Equal(j.Extensions, another.Extensions) { + return false + } + if ignoreExtensionsSequence && !slices.Equal(j.Extensions, another.sortedExtensions()) { + return false + } + if !slices.Equal(j.EllipticCurves, another.EllipticCurves) { + return false + } + if !slices.Equal(j.EllipticCurvePF, another.EllipticCurvePF) { + return false + } + if !slices.Equal(j.SignatureAlgorithms, another.SignatureAlgorithms) { + return false + } + return true +} + +func (j *ClientHello) sortedExtensions() []uint16 { + extensions := make([]uint16, len(j.Extensions)) + copy(extensions, j.Extensions) + slices.Sort(extensions) + return extensions +} + +func Compute(payload []byte) (*ClientHello, error) { + ja3 := ClientHello{} + err := ja3.parseSegment(payload) + return &ja3, err +} + +func (j *ClientHello) String() string { + if j.ja3ByteString == nil { + j.marshalJA3() + } + return string(j.ja3ByteString) +} + +func (j *ClientHello) Hash() string { + if j.ja3ByteString == nil { + j.marshalJA3() + } + if j.ja3Hash == "" { + h := md5.Sum(j.ja3ByteString) + j.ja3Hash = hex.EncodeToString(h[:]) + } + return j.ja3Hash +} diff --git a/common/ja3/parser.go b/common/ja3/parser.go new file mode 100644 index 0000000000..f9cca60382 --- /dev/null +++ b/common/ja3/parser.go @@ -0,0 +1,357 @@ +// Copyright (c) 2018, Open Systems AG. All rights reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. + +package ja3 + +import ( + "encoding/binary" + "strconv" +) + +const ( + // Constants used for parsing + recordLayerHeaderLen int = 5 + handshakeHeaderLen int = 6 + randomDataLen int = 32 + sessionIDHeaderLen int = 1 + cipherSuiteHeaderLen int = 2 + compressMethodHeaderLen int = 1 + extensionsHeaderLen int = 2 + extensionHeaderLen int = 4 + sniExtensionHeaderLen int = 5 + ecExtensionHeaderLen int = 2 + ecpfExtensionHeaderLen int = 1 + versionExtensionHeaderLen int = 1 + signatureAlgorithmsExtensionHeaderLen int = 2 + contentType uint8 = 22 + handshakeType uint8 = 1 + sniExtensionType uint16 = 0 + sniNameDNSHostnameType uint8 = 0 + ecExtensionType uint16 = 10 + ecpfExtensionType uint16 = 11 + versionExtensionType uint16 = 43 + signatureAlgorithmsExtensionType uint16 = 13 + + // Versions + // The bitmask covers the versions SSL3.0 to TLS1.2 + tlsVersionBitmask uint16 = 0xFFFC + tls13 uint16 = 0x0304 + + // GREASE values + // The bitmask covers all GREASE values + GreaseBitmask uint16 = 0x0F0F + + // Constants used for marshalling + dashByte = byte(45) + commaByte = byte(44) +) + +// parseSegment to populate the corresponding ClientHello object or return an error +func (j *ClientHello) parseSegment(segment []byte) error { + // Check if we can decode the next fields + if len(segment) < recordLayerHeaderLen { + return &ParseError{LengthErr, 1} + } + + // Check if we have "Content Type: Handshake (22)" + contType := uint8(segment[0]) + if contType != contentType { + return &ParseError{errType: ContentTypeErr} + } + + // Check if TLS record layer version is supported + tlsRecordVersion := uint16(segment[1])<<8 | uint16(segment[2]) + if tlsRecordVersion&tlsVersionBitmask != 0x0300 && tlsRecordVersion != tls13 { + return &ParseError{VersionErr, 1} + } + + // Check that the Handshake is as long as expected from the length field + segmentLen := uint16(segment[3])<<8 | uint16(segment[4]) + if len(segment[recordLayerHeaderLen:]) < int(segmentLen) { + return &ParseError{LengthErr, 2} + } + // Keep the Handshake messege, ignore any additional following record types + hs := segment[recordLayerHeaderLen : recordLayerHeaderLen+int(segmentLen)] + + err := j.parseHandshake(hs) + + return err +} + +// parseHandshake body +func (j *ClientHello) parseHandshake(hs []byte) error { + // Check if we can decode the next fields + if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen { + return &ParseError{LengthErr, 3} + } + + // Check if we have "Handshake Type: Client Hello (1)" + handshType := uint8(hs[0]) + if handshType != handshakeType { + return &ParseError{errType: HandshakeTypeErr} + } + + // Check if actual length of handshake matches (this is a great exclusion criterion for false positives, + // as these fields have to match the actual length of the rest of the segment) + handshakeLen := uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3]) + if len(hs[4:]) != int(handshakeLen) { + return &ParseError{LengthErr, 4} + } + + // Check if Client Hello version is supported + tlsVersion := uint16(hs[4])<<8 | uint16(hs[5]) + if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 { + return &ParseError{VersionErr, 2} + } + j.Version = tlsVersion + + // Check if we can decode the next fields + sessionIDLen := uint8(hs[38]) + if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen) { + return &ParseError{LengthErr, 5} + } + + // Cipher Suites + cs := hs[handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen):] + + // Check if we can decode the next fields + if len(cs) < cipherSuiteHeaderLen { + return &ParseError{LengthErr, 6} + } + + csLen := uint16(cs[0])<<8 | uint16(cs[1]) + numCiphers := int(csLen / 2) + cipherSuites := make([]uint16, 0, numCiphers) + + // Check if we can decode the next fields + if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen { + return &ParseError{LengthErr, 7} + } + + for i := 0; i < numCiphers; i++ { + cipherSuite := uint16(cs[2+i<<1])<<8 | uint16(cs[3+i<<1]) + cipherSuites = append(cipherSuites, cipherSuite) + } + j.CipherSuites = cipherSuites + + // Check if we can decode the next fields + compressMethodLen := uint16(cs[cipherSuiteHeaderLen+int(csLen)]) + if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen) { + return &ParseError{LengthErr, 8} + } + + // Extensions + exs := cs[cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen):] + + err := j.parseExtensions(exs) + + return err +} + +// parseExtensions of the handshake +func (j *ClientHello) parseExtensions(exs []byte) error { + // Check for no extensions, this fields header is nonexistent if no body is used + if len(exs) == 0 { + return nil + } + + // Check if we can decode the next fields + if len(exs) < extensionsHeaderLen { + return &ParseError{LengthErr, 9} + } + + exsLen := uint16(exs[0])<<8 | uint16(exs[1]) + exs = exs[extensionsHeaderLen:] + + // Check if we can decode the next fields + if len(exs) < int(exsLen) { + return &ParseError{LengthErr, 10} + } + + var sni []byte + var extensions, ellipticCurves []uint16 + var ellipticCurvePF []uint8 + var versions []uint16 + var signatureAlgorithms []uint16 + for len(exs) > 0 { + + // Check if we can decode the next fields + if len(exs) < extensionHeaderLen { + return &ParseError{LengthErr, 11} + } + + exType := uint16(exs[0])<<8 | uint16(exs[1]) + exLen := uint16(exs[2])<<8 | uint16(exs[3]) + // Ignore any GREASE extensions + extensions = append(extensions, exType) + // Check if we can decode the next fields + if len(exs) < extensionHeaderLen+int(exLen) { + return &ParseError{LengthErr, 12} + } + + sex := exs[extensionHeaderLen : extensionHeaderLen+int(exLen)] + + switch exType { + case sniExtensionType: // Extensions: server_name + + // Check if we can decode the next fields + if len(sex) < sniExtensionHeaderLen { + return &ParseError{LengthErr, 13} + } + + sniType := uint8(sex[2]) + sniLen := uint16(sex[3])<<8 | uint16(sex[4]) + sex = sex[sniExtensionHeaderLen:] + + // Check if we can decode the next fields + if len(sex) != int(sniLen) { + return &ParseError{LengthErr, 14} + } + + switch sniType { + case sniNameDNSHostnameType: + sni = sex + default: + return &ParseError{errType: SNITypeErr} + } + case ecExtensionType: // Extensions: supported_groups + + // Check if we can decode the next fields + if len(sex) < ecExtensionHeaderLen { + return &ParseError{LengthErr, 15} + } + + ecsLen := uint16(sex[0])<<8 | uint16(sex[1]) + numCurves := int(ecsLen / 2) + ellipticCurves = make([]uint16, 0, numCurves) + sex = sex[ecExtensionHeaderLen:] + + // Check if we can decode the next fields + if len(sex) != int(ecsLen) { + return &ParseError{LengthErr, 16} + } + + for i := 0; i < numCurves; i++ { + ecType := uint16(sex[i*2])<<8 | uint16(sex[1+i*2]) + ellipticCurves = append(ellipticCurves, ecType) + } + + case ecpfExtensionType: // Extensions: ec_point_formats + + // Check if we can decode the next fields + if len(sex) < ecpfExtensionHeaderLen { + return &ParseError{LengthErr, 17} + } + + ecpfsLen := uint8(sex[0]) + numPF := int(ecpfsLen) + ellipticCurvePF = make([]uint8, numPF) + sex = sex[ecpfExtensionHeaderLen:] + + // Check if we can decode the next fields + if len(sex) != numPF { + return &ParseError{LengthErr, 18} + } + + for i := 0; i < numPF; i++ { + ellipticCurvePF[i] = uint8(sex[i]) + } + case versionExtensionType: + if len(sex) < versionExtensionHeaderLen { + return &ParseError{LengthErr, 19} + } + versionsLen := int(sex[0]) + for i := 0; i < versionsLen; i += 2 { + versions = append(versions, binary.BigEndian.Uint16(sex[1:][i:])) + } + case signatureAlgorithmsExtensionType: + if len(sex) < signatureAlgorithmsExtensionHeaderLen { + return &ParseError{LengthErr, 20} + } + ssaLen := binary.BigEndian.Uint16(sex) + for i := 0; i < int(ssaLen); i += 2 { + signatureAlgorithms = append(signatureAlgorithms, binary.BigEndian.Uint16(sex[2:][i:])) + } + } + exs = exs[4+exLen:] + } + j.ServerName = string(sni) + j.Extensions = extensions + j.EllipticCurves = ellipticCurves + j.EllipticCurvePF = ellipticCurvePF + j.Versions = versions + j.SignatureAlgorithms = signatureAlgorithms + return nil +} + +// marshalJA3 into a byte string +func (j *ClientHello) marshalJA3() { + // An uint16 can contain numbers with up to 5 digits and an uint8 can contain numbers with up to 3 digits, but we + // also need a byte for each separating character, except at the end. + byteStringLen := 6*(1+len(j.CipherSuites)+len(j.Extensions)+len(j.EllipticCurves)) + 4*len(j.EllipticCurvePF) - 1 + byteString := make([]byte, 0, byteStringLen) + + // Version + byteString = strconv.AppendUint(byteString, uint64(j.Version), 10) + byteString = append(byteString, commaByte) + + // Cipher Suites + if len(j.CipherSuites) != 0 { + for _, val := range j.CipherSuites { + if val&GreaseBitmask != 0x0A0A { + continue + } + byteString = strconv.AppendUint(byteString, uint64(val), 10) + byteString = append(byteString, dashByte) + } + // Replace last dash with a comma + byteString[len(byteString)-1] = commaByte + } else { + byteString = append(byteString, commaByte) + } + + // Extensions + if len(j.Extensions) != 0 { + for _, val := range j.Extensions { + if val&GreaseBitmask != 0x0A0A { + continue + } + byteString = strconv.AppendUint(byteString, uint64(val), 10) + byteString = append(byteString, dashByte) + } + // Replace last dash with a comma + byteString[len(byteString)-1] = commaByte + } else { + byteString = append(byteString, commaByte) + } + + // Elliptic curves + if len(j.EllipticCurves) != 0 { + for _, val := range j.EllipticCurves { + if val&GreaseBitmask != 0x0A0A { + continue + } + byteString = strconv.AppendUint(byteString, uint64(val), 10) + byteString = append(byteString, dashByte) + } + // Replace last dash with a comma + byteString[len(byteString)-1] = commaByte + } else { + byteString = append(byteString, commaByte) + } + + // ECPF + if len(j.EllipticCurvePF) != 0 { + for _, val := range j.EllipticCurvePF { + byteString = strconv.AppendUint(byteString, uint64(val), 10) + byteString = append(byteString, dashByte) + } + // Remove last dash + byteString = byteString[:len(byteString)-1] + } + + j.ja3ByteString = byteString +} diff --git a/common/sniff/bittorrent.go b/common/sniff/bittorrent.go index debc35ca4f..9e123c41e9 100644 --- a/common/sniff/bittorrent.go +++ b/common/sniff/bittorrent.go @@ -19,45 +19,44 @@ const ( // BitTorrent detects if the stream is a BitTorrent connection. // For the BitTorrent protocol specification, see https://www.bittorrent.org/beps/bep_0003.html -func BitTorrent(_ context.Context, reader io.Reader) (*adapter.InboundContext, error) { +func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { var first byte err := binary.Read(reader, binary.BigEndian, &first) if err != nil { - return nil, err + return err } if first != 19 { - return nil, os.ErrInvalid + return os.ErrInvalid } var protocol [19]byte _, err = reader.Read(protocol[:]) if err != nil { - return nil, err + return err } if string(protocol[:]) != "BitTorrent protocol" { - return nil, os.ErrInvalid + return os.ErrInvalid } - return &adapter.InboundContext{ - Protocol: C.ProtocolBitTorrent, - }, nil + metadata.Protocol = C.ProtocolBitTorrent + return nil } // UTP detects if the packet is a uTP connection packet. // For the uTP protocol specification, see // 1. https://www.bittorrent.org/beps/bep_0029.html // 2. https://github.com/bittorrent/libutp/blob/2b364cbb0650bdab64a5de2abb4518f9f228ec44/utp_internal.cpp#L112 -func UTP(_ context.Context, packet []byte) (*adapter.InboundContext, error) { +func UTP(_ context.Context, metadata *adapter.InboundContext, packet []byte) error { // A valid uTP packet must be at least 20 bytes long. if len(packet) < 20 { - return nil, os.ErrInvalid + return os.ErrInvalid } version := packet[0] & 0x0F ty := packet[0] >> 4 if version != 1 || ty > 4 { - return nil, os.ErrInvalid + return os.ErrInvalid } // Validate the extensions @@ -66,36 +65,35 @@ func UTP(_ context.Context, packet []byte) (*adapter.InboundContext, error) { for extension != 0 { err := binary.Read(reader, binary.BigEndian, &extension) if err != nil { - return nil, err + return err } var length byte err = binary.Read(reader, binary.BigEndian, &length) if err != nil { - return nil, err + return err } _, err = reader.Seek(int64(length), io.SeekCurrent) if err != nil { - return nil, err + return err } } - - return &adapter.InboundContext{ - Protocol: C.ProtocolBitTorrent, - }, nil + metadata.Protocol = C.ProtocolBitTorrent + return nil } // UDPTracker detects if the packet is a UDP Tracker Protocol packet. // For the UDP Tracker Protocol specification, see https://www.bittorrent.org/beps/bep_0015.html -func UDPTracker(_ context.Context, packet []byte) (*adapter.InboundContext, error) { +func UDPTracker(_ context.Context, metadata *adapter.InboundContext, packet []byte) error { if len(packet) < trackerConnectMinSize { - return nil, os.ErrInvalid + return os.ErrInvalid } if binary.BigEndian.Uint64(packet[:8]) != trackerProtocolID { - return nil, os.ErrInvalid + return os.ErrInvalid } if binary.BigEndian.Uint32(packet[8:12]) != trackerConnectFlag { - return nil, os.ErrInvalid + return os.ErrInvalid } - return &adapter.InboundContext{Protocol: C.ProtocolBitTorrent}, nil + metadata.Protocol = C.ProtocolBitTorrent + return nil } diff --git a/common/sniff/bittorrent_test.go b/common/sniff/bittorrent_test.go index 6b3ab64e95..65f095bdac 100644 --- a/common/sniff/bittorrent_test.go +++ b/common/sniff/bittorrent_test.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "testing" + "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" @@ -24,7 +25,8 @@ func TestSniffBittorrent(t *testing.T) { for _, pkt := range packets { pkt, err := hex.DecodeString(pkt) require.NoError(t, err) - metadata, err := sniff.BitTorrent(context.TODO(), bytes.NewReader(pkt)) + var metadata adapter.InboundContext + err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt)) require.NoError(t, err) require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol) } @@ -43,8 +45,8 @@ func TestSniffUTP(t *testing.T) { for _, pkt := range packets { pkt, err := hex.DecodeString(pkt) require.NoError(t, err) - - metadata, err := sniff.UTP(context.TODO(), pkt) + var metadata adapter.InboundContext + err = sniff.UTP(context.TODO(), &metadata, pkt) require.NoError(t, err) require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol) } @@ -63,7 +65,8 @@ func TestSniffUDPTracker(t *testing.T) { pkt, err := hex.DecodeString(pkt) require.NoError(t, err) - metadata, err := sniff.UDPTracker(context.TODO(), pkt) + var metadata adapter.InboundContext + err = sniff.UDPTracker(context.TODO(), &metadata, pkt) require.NoError(t, err) require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol) } diff --git a/common/sniff/dns.go b/common/sniff/dns.go index d69f3b158c..96670eca9c 100644 --- a/common/sniff/dns.go +++ b/common/sniff/dns.go @@ -17,18 +17,17 @@ import ( mDNS "github.com/miekg/dns" ) -func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.InboundContext, error) { +func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundContext, reader io.Reader) error { var length uint16 err := binary.Read(reader, binary.BigEndian, &length) if err != nil { - return nil, err + return os.ErrInvalid } if length == 0 { - return nil, os.ErrInvalid + 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 { @@ -37,19 +36,20 @@ func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter. err = readTask.Run(readCtx) cancel() if err != nil { - return nil, err + return err } - return DomainNameQuery(readCtx, buffer.Bytes()) + return DomainNameQuery(readCtx, metadata, buffer.Bytes()) } -func DomainNameQuery(ctx context.Context, packet []byte) (*adapter.InboundContext, error) { +func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { var msg mDNS.Msg err := msg.Unpack(packet) if err != nil { - return nil, err + return err } if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) { - return nil, os.ErrInvalid + return os.ErrInvalid } - return &adapter.InboundContext{Protocol: C.ProtocolDNS}, nil + metadata.Protocol = C.ProtocolDNS + return nil } diff --git a/common/sniff/dtls.go b/common/sniff/dtls.go index 6469b09709..e2704a27ae 100644 --- a/common/sniff/dtls.go +++ b/common/sniff/dtls.go @@ -8,24 +8,25 @@ import ( C "github.com/sagernet/sing-box/constant" ) -func DTLSRecord(ctx context.Context, packet []byte) (*adapter.InboundContext, error) { +func DTLSRecord(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { const fixedHeaderSize = 13 if len(packet) < fixedHeaderSize { - return nil, os.ErrInvalid + return os.ErrInvalid } contentType := packet[0] switch contentType { case 20, 21, 22, 23, 25: default: - return nil, os.ErrInvalid + return os.ErrInvalid } versionMajor := packet[1] if versionMajor != 0xfe { - return nil, os.ErrInvalid + return os.ErrInvalid } versionMinor := packet[2] if versionMinor != 0xff && versionMinor != 0xfd { - return nil, os.ErrInvalid + return os.ErrInvalid } - return &adapter.InboundContext{Protocol: C.ProtocolDTLS}, nil + metadata.Protocol = C.ProtocolDTLS + return nil } diff --git a/common/sniff/dtls_test.go b/common/sniff/dtls_test.go index 45f7712642..48c9b42b31 100644 --- a/common/sniff/dtls_test.go +++ b/common/sniff/dtls_test.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "testing" + "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" @@ -15,7 +16,8 @@ func TestSniffDTLSClientHello(t *testing.T) { t.Parallel() packet, err := hex.DecodeString("16fefd0000000000000000007e010000720000000000000072fefd668a43523798e064bd806d0c87660de9c611a59bbdfc3892c4e072d94f2cafc40000000cc02bc02fc00ac014c02cc0300100003c000d0010000e0403050306030401050106010807ff01000100000a00080006001d00170018000b00020100000e000900060008000700010000170000") require.NoError(t, err) - metadata, err := sniff.DTLSRecord(context.Background(), packet) + var metadata adapter.InboundContext + err = sniff.DTLSRecord(context.Background(), &metadata, packet) require.NoError(t, err) require.Equal(t, metadata.Protocol, C.ProtocolDTLS) } @@ -24,7 +26,8 @@ func TestSniffDTLSClientApplicationData(t *testing.T) { t.Parallel() packet, err := hex.DecodeString("17fefd000100000000000100440001000000000001a4f682b77ecadd10f3f3a2f78d90566212366ff8209fd77314f5a49352f9bb9bd12f4daba0b4736ae29e46b9714d3b424b3e6d0234736619b5aa0d3f") require.NoError(t, err) - metadata, err := sniff.DTLSRecord(context.Background(), packet) + var metadata adapter.InboundContext + err = sniff.DTLSRecord(context.Background(), &metadata, packet) require.NoError(t, err) require.Equal(t, metadata.Protocol, C.ProtocolDTLS) } diff --git a/common/sniff/http.go b/common/sniff/http.go index faf05fd304..0e6ab406c6 100644 --- a/common/sniff/http.go +++ b/common/sniff/http.go @@ -11,10 +11,12 @@ import ( "github.com/sagernet/sing/protocol/http" ) -func HTTPHost(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error) { +func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { request, err := http.ReadRequest(std_bufio.NewReader(reader)) if err != nil { - return nil, err + return err } - return &adapter.InboundContext{Protocol: C.ProtocolHTTP, Domain: M.ParseSocksaddr(request.Host).AddrString()}, nil + metadata.Protocol = C.ProtocolHTTP + metadata.Domain = M.ParseSocksaddr(request.Host).AddrString() + return nil } diff --git a/common/sniff/http_test.go b/common/sniff/http_test.go index 98100e092a..9f64efa85e 100644 --- a/common/sniff/http_test.go +++ b/common/sniff/http_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" "github.com/stretchr/testify/require" @@ -13,7 +14,8 @@ import ( func TestSniffHTTP1(t *testing.T) { t.Parallel() pkt := "GET / HTTP/1.1\r\nHost: www.google.com\r\nAccept: */*\r\n\r\n" - metadata, err := sniff.HTTPHost(context.Background(), strings.NewReader(pkt)) + var metadata adapter.InboundContext + err := sniff.HTTPHost(context.Background(), &metadata, strings.NewReader(pkt)) require.NoError(t, err) require.Equal(t, metadata.Domain, "www.google.com") } @@ -21,7 +23,8 @@ func TestSniffHTTP1(t *testing.T) { func TestSniffHTTP1WithPort(t *testing.T) { t.Parallel() pkt := "GET / HTTP/1.1\r\nHost: www.gov.cn:8080\r\nAccept: */*\r\n\r\n" - metadata, err := sniff.HTTPHost(context.Background(), strings.NewReader(pkt)) + var metadata adapter.InboundContext + err := sniff.HTTPHost(context.Background(), &metadata, strings.NewReader(pkt)) require.NoError(t, err) require.Equal(t, metadata.Domain, "www.gov.cn") } diff --git a/common/sniff/quic.go b/common/sniff/quic.go index 55dc1c0023..adec7008b1 100644 --- a/common/sniff/quic.go +++ b/common/sniff/quic.go @@ -5,95 +5,99 @@ import ( "context" "crypto" "crypto/aes" + "crypto/tls" "encoding/binary" "io" "os" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/ja3" "github.com/sagernet/sing-box/common/sniff/internal/qtls" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" "golang.org/x/crypto/hkdf" ) -func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContext, error) { - reader := bytes.NewReader(packet) +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() if err != nil { - return nil, err + return err } if typeByte&0x40 == 0 { - return nil, E.New("bad type byte") + return E.New("bad type byte") } var versionNumber uint32 err = binary.Read(reader, binary.BigEndian, &versionNumber) if err != nil { - return nil, err + return err } if versionNumber != qtls.VersionDraft29 && versionNumber != qtls.Version1 && versionNumber != qtls.Version2 { - return nil, E.New("bad version") + return E.New("bad version") } packetType := (typeByte & 0x30) >> 4 if packetType == 0 && versionNumber == qtls.Version2 || packetType == 2 && versionNumber != qtls.Version2 || packetType > 2 { - return nil, E.New("bad packet type") + return E.New("bad packet type") } destConnIDLen, err := reader.ReadByte() if err != nil { - return nil, err + return err } if destConnIDLen == 0 || destConnIDLen > 20 { - return nil, E.New("bad destination connection id length") + return E.New("bad destination connection id length") } destConnID := make([]byte, destConnIDLen) _, err = io.ReadFull(reader, destConnID) if err != nil { - return nil, err + return err } srcConnIDLen, err := reader.ReadByte() if err != nil { - return nil, err + return err } _, err = io.CopyN(io.Discard, reader, int64(srcConnIDLen)) if err != nil { - return nil, err + return err } tokenLen, err := qtls.ReadUvarint(reader) if err != nil { - return nil, err + return err } _, err = io.CopyN(io.Discard, reader, int64(tokenLen)) if err != nil { - return nil, err + return err } packetLen, err := qtls.ReadUvarint(reader) if err != nil { - return nil, err + return err } hdrLen := int(reader.Size()) - reader.Len() if hdrLen+int(packetLen) > len(packet) { - return nil, os.ErrInvalid + return os.ErrInvalid } _, err = io.CopyN(io.Discard, reader, 4) if err != nil { - return nil, err + return err } pnBytes := make([]byte, aes.BlockSize) _, err = io.ReadFull(reader, pnBytes) if err != nil { - return nil, err + return err } var salt []byte @@ -117,7 +121,7 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex hpKey := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, hkdfHeaderProtectionLabel, 16) block, err := aes.NewCipher(hpKey) if err != nil { - return nil, err + return err } mask := make([]byte, aes.BlockSize) block.Encrypt(mask, pnBytes) @@ -129,7 +133,7 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex } packetNumberLength := newPacket[0]&0x3 + 1 if hdrLen+int(packetNumberLength) > int(packetLen)+hdrLen { - return nil, os.ErrInvalid + return os.ErrInvalid } var packetNumber uint32 switch packetNumberLength { @@ -142,7 +146,7 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex case 4: packetNumber = binary.BigEndian.Uint32(newPacket[hdrLen:]) default: - return nil, E.New("bad packet number length") + return E.New("bad packet number length") } extHdrLen := hdrLen + int(packetNumberLength) copy(newPacket[extHdrLen:hdrLen+4], packet[extHdrLen:]) @@ -166,138 +170,208 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(packetNumber)) decrypted, err := cipher.Open(newPacket[extHdrLen:extHdrLen], nonce, data, newPacket[:extHdrLen]) if err != nil { - return nil, err + return err } var frameType byte - var frameLen uint64 - var fragments []struct { - offset uint64 - length uint64 - payload []byte - } + var fragments []qCryptoFragment decryptedReader := bytes.NewReader(decrypted) + const ( + frameTypePadding = 0x00 + frameTypePing = 0x01 + frameTypeAck = 0x02 + frameTypeAck2 = 0x03 + frameTypeCrypto = 0x06 + frameTypeConnectionClose = 0x1c + ) + var frameTypeList []uint8 for { frameType, err = decryptedReader.ReadByte() if err == io.EOF { break } + frameTypeList = append(frameTypeList, frameType) switch frameType { - case 0x00: // PADDING + case frameTypePadding: continue - case 0x01: // PING + case frameTypePing: continue - case 0x02, 0x03: // ACK + case frameTypeAck, frameTypeAck2: _, err = qtls.ReadUvarint(decryptedReader) // Largest Acknowledged if err != nil { - return nil, err + return err } _, err = qtls.ReadUvarint(decryptedReader) // ACK Delay if err != nil { - return nil, err + return err } ackRangeCount, err := qtls.ReadUvarint(decryptedReader) // ACK Range Count if err != nil { - return nil, err + return err } _, err = qtls.ReadUvarint(decryptedReader) // First ACK Range if err != nil { - return nil, err + return err } for i := 0; i < int(ackRangeCount); i++ { _, err = qtls.ReadUvarint(decryptedReader) // Gap if err != nil { - return nil, err + return err } _, err = qtls.ReadUvarint(decryptedReader) // ACK Range Length if err != nil { - return nil, err + return err } } if frameType == 0x03 { _, err = qtls.ReadUvarint(decryptedReader) // ECT0 Count if err != nil { - return nil, err + return err } _, err = qtls.ReadUvarint(decryptedReader) // ECT1 Count if err != nil { - return nil, err + return err } _, err = qtls.ReadUvarint(decryptedReader) // ECN-CE Count if err != nil { - return nil, err + return err } } - case 0x06: // CRYPTO + case frameTypeCrypto: var offset uint64 offset, err = qtls.ReadUvarint(decryptedReader) if err != nil { - return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err + return err } var length uint64 length, err = qtls.ReadUvarint(decryptedReader) if err != nil { - return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err + return err } index := len(decrypted) - decryptedReader.Len() - fragments = append(fragments, struct { - offset uint64 - length uint64 - payload []byte - }{offset, length, decrypted[index : index+int(length)]}) - frameLen += length + fragments = append(fragments, qCryptoFragment{offset, length, decrypted[index : index+int(length)]}) _, err = decryptedReader.Seek(int64(length), io.SeekCurrent) if err != nil { - return nil, err + return err } - case 0x1c: // CONNECTION_CLOSE + case frameTypeConnectionClose: _, err = qtls.ReadUvarint(decryptedReader) // Error Code if err != nil { - return nil, err + return err } _, err = qtls.ReadUvarint(decryptedReader) // Frame Type if err != nil { - return nil, err + return err } var length uint64 length, err = qtls.ReadUvarint(decryptedReader) // Reason Phrase Length if err != nil { - return nil, err + return err } _, err = decryptedReader.Seek(int64(length), io.SeekCurrent) // Reason Phrase if err != nil { - return nil, err + return err } default: - return nil, os.ErrInvalid + return os.ErrInvalid } } - tlsHdr := make([]byte, 5) - tlsHdr[0] = 0x16 - binary.BigEndian.PutUint16(tlsHdr[1:], uint16(0x0303)) - binary.BigEndian.PutUint16(tlsHdr[3:], uint16(frameLen)) + if metadata.SniffContext != nil { + fragments = append(fragments, metadata.SniffContext.([]qCryptoFragment)...) + metadata.SniffContext = nil + } + var frameLen uint64 + for _, fragment := range fragments { + frameLen += fragment.length + } + buffer := buf.NewSize(5 + int(frameLen)) + defer buffer.Release() + buffer.WriteByte(0x16) + binary.Write(buffer, binary.BigEndian, uint16(0x0303)) + binary.Write(buffer, binary.BigEndian, uint16(frameLen)) var index uint64 var length int - var readers []io.Reader - readers = append(readers, bytes.NewReader(tlsHdr)) find: for { for _, fragment := range fragments { if fragment.offset == index { - readers = append(readers, bytes.NewReader(fragment.payload)) + buffer.Write(fragment.payload) index = fragment.offset + fragment.length length++ continue find } } - if length == len(fragments) { + break + } + metadata.Protocol = C.ProtocolQUIC + fingerprint, err := ja3.Compute(buffer.Bytes()) + if err != nil { + metadata.Protocol = C.ProtocolQUIC + metadata.Client = C.ClientChromium + metadata.SniffContext = fragments + return ErrClientHelloFragmented + } + metadata.Domain = fingerprint.ServerName + for metadata.Client == "" { + if len(frameTypeList) == 1 { + metadata.Client = C.ClientFirefox + break + } + if frameTypeList[0] == frameTypeCrypto && isZero(frameTypeList[1:]) { + if len(fingerprint.Versions) == 2 && fingerprint.Versions[0]&ja3.GreaseBitmask == 0x0A0A && + len(fingerprint.EllipticCurves) == 5 && fingerprint.EllipticCurves[0]&ja3.GreaseBitmask == 0x0A0A { + metadata.Client = C.ClientSafari + break + } + if len(fingerprint.CipherSuites) == 1 && fingerprint.CipherSuites[0] == tls.TLS_AES_256_GCM_SHA384 && + len(fingerprint.EllipticCurves) == 1 && fingerprint.EllipticCurves[0] == uint16(tls.X25519) && + len(fingerprint.SignatureAlgorithms) == 1 && fingerprint.SignatureAlgorithms[0] == uint16(tls.ECDSAWithP256AndSHA256) { + metadata.Client = C.ClientSafari + break + } + } + + if frameTypeList[len(frameTypeList)-1] == frameTypeCrypto && isZero(frameTypeList[:len(frameTypeList)-1]) { + metadata.Client = C.ClientQUICGo break } - return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, E.New("bad fragments") + + if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 { + if maybeUQUIC(fingerprint) { + metadata.Client = C.ClientQUICGo + } else { + metadata.Client = C.ClientChromium + } + break + } + + metadata.Client = C.ClientUnknown + //nolint:staticcheck + break } - metadata, err := TLSClientHello(ctx, io.MultiReader(readers...)) - if err != nil { - return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err + return nil +} + +func isZero(slices []uint8) bool { + for _, slice := range slices { + if slice != 0 { + return false + } } - metadata.Protocol = C.ProtocolQUIC - return metadata, nil + return true +} + +func count(slices []uint8, value uint8) int { + var times int + for _, slice := range slices { + if slice == value { + times++ + } + } + return times +} + +type qCryptoFragment struct { + offset uint64 + length uint64 + payload []byte } diff --git a/common/sniff/quic_blacklist.go b/common/sniff/quic_blacklist.go new file mode 100644 index 0000000000..7d5f91e79b --- /dev/null +++ b/common/sniff/quic_blacklist.go @@ -0,0 +1,24 @@ +package sniff + +import ( + "crypto/tls" + + "github.com/sagernet/sing-box/common/ja3" +) + +// Chromium sends separate client hello packets, but UQUIC has not yet implemented this behavior +// The cronet without this behavior does not have version 115 +var uQUICChrome115 = &ja3.ClientHello{ + Version: tls.VersionTLS12, + CipherSuites: []uint16{4865, 4866, 4867}, + Extensions: []uint16{0, 10, 13, 16, 27, 43, 45, 51, 57, 17513}, + EllipticCurves: []uint16{29, 23, 24}, + SignatureAlgorithms: []uint16{1027, 2052, 1025, 1283, 2053, 1281, 2054, 1537, 513}, +} + +func maybeUQUIC(fingerprint *ja3.ClientHello) bool { + if uQUICChrome115.Equals(fingerprint, true) { + return true + } + return false +} diff --git a/common/sniff/quic_test.go b/common/sniff/quic_test.go index 807197654b..e75e86a5d2 100644 --- a/common/sniff/quic_test.go +++ b/common/sniff/quic_test.go @@ -5,31 +5,69 @@ import ( "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 TestSniffQUICv1(t *testing.T) { +func TestSniffQUICChromium(t *testing.T) { t.Parallel() - pkt, err := hex.DecodeString("cc0000000108d2dc7bad02241f5003796e71004215a71bfcb05159416c724be418537389acdd9a4047306283dcb4d7a9cad5cc06322042d204da67a8dbaa328ab476bb428b48fd001501863afd203f8d4ef085629d664f1a734a65969a47e4a63d4e01a21f18c1d90db0c027180906dc135f9ae421bb8617314c8d54c175fef3d3383d310d0916ebcbd6eed9329befbbb109d8fd4af1d2cf9d6adce8e6c1260a7f8256e273e326da0aa7cc148d76e7a08489dc9d52ade89c027cbc3491ada46417c2c04e2ca768e9a7dd6aa00c594e48b678927325da796817693499bb727050cb3baf3d3291a397c3a8d868e8ec7b8f7295e347455c9dadbe2252ae917ac793d958c7fb8a3d2cdb34e3891eb4286f18617556ff7216dd60256aa5b1d11ff4753459fc5f9dedf11d483a26a0835dc6cd50e1c1f54f86e8f1e502821183cd874f6447a74e818bf3445c7795acf4559d1c1fac474911d2ead5c8d23e4aa4f67afb66efe305a30a0b5d825679b31ddc186cbea936535795c7e8c378c87b8c5adc065154d15bae8f85ac8fec2da40c3aa623b682a065440831555011d7647cde44446a0fb4cf5892f2c088ae1920643094be72e3c499fe8d265caf939e8ab607a5b9317917d2a32a812e8a0e6a2f84721bbb5984ffd242838f705d13f4cfb249bc6a5c80d58ac2595edf56648ec3fe21d787573c253a79805252d6d81e26d367d4ff29ef66b5fe8992086af7bada8cad10b82a7c0dc406c5b6d0c5ec3c583e767f759ce08cad6c3c8f91e5a8") + pkt, err := hex.DecodeString("c30000000108f40d654cc09b27f5000044d08a94548e57e43cc5483f129986187c432d58d46674830442988f869566a6e31e2ae37c9f7acbf61cc81621594fab0b3dfdc1635460b32389563dc8e74006315661cd22694114612973c1c45910621713a48b375854f095e8a77ccf3afa64e972f0f7f7002f50e0b014b1b146ea47c07fb20b73ad5587872b51a0b3fafdf1c4cf4fe6f8b112142392efa25d993abe2f42582be145148bdfe12edcd96c3655b65a4781b093e5594ba8e3ae5320f12e8314fc3ca374128cc43381046c322b964681ed4395c813b28534505118201459665a44b8f0abead877de322e9040631d20b05f15b81fa7ff785d4041aecc37c7e2ccdc5d1532787ce566517e8985fd5c200dbfd1e67bc255efaba94cfc07bb52fea4a90887413b134f2715b5643542aa897c6116486f428d82da64d2a2c1e1bdd40bd592558901a554b003d6966ac5a7b8b9413eddbf6ef21f28386c74981e3ce1d724c341e95494907626659692720c81114ca4acea35a14c402cfa3dc2228446e78dc1b81fa4325cf7e314a9cad6a6bdff33b3351dcba74eb15fae67f1227283aa4cdd64bcadf8f19358333f8549b596f4350297b5c65274565869d497398339947b9d3d064e5b06d39d34b436d8a41c1a3880de10bd26c3b1c5b4e2a49b0d4d07b8d90cd9e92bc611564d19ea8ec33099e92033caf21f5307dbeaa4708b99eb313bff99e2081ac25fd12d6a72e8335e0724f6718fe023cd0ad0d6e6a6309f09c9c391eec2bc08e9c3210a043c08e1759f354c121f6517fff4d6e20711a871e41285d48d930352fddffb92c96ba57df045ce99f8bfdfa8edc0969ce68a51e9fbb4f54b956d9df74a9e4af27ed2b27839bce1cffeca8333c0aaee81a570217442f9029ba8fedb84a2cf4be4d910982d891ea00e816c7fb98e8020e896a9c6fdd9106611da0a99dde18df1b7a8f6327acb1eed9ad93314451e48cb0dfb9571728521ca3db2ac0968159d5622556a55d51a422d11995b650949aaefc5d24c16080446dfc4fbc10353f9f93ce161ab513367bb89ab83988e0630b689e174e27bcfcc31996ee7b0bca909e251b82d69a28fee5a5d662e127508cd19dbbe5097b7d5b62a49203d66764197a527e472e2627e44a93d44177dace9d60e7d0e03305ddf4cfe47cdf2362e14de79ef46a6763ce696cd7854a48d9419a0817507a4713ffd4977b906d4f2b5fb6dbe1bd15bc505d5fea582190bf531a45d5ee026da8918547fd5105f15e5d061c7b0cf80a34990366ed8e91e13c2f0d85e5dad537298808d193cf54b7eaac33f10051f74cb6b75e52f81618c36f03d86aef613ba237a1a793ba1539938a38f62ccaf7bd5f6c5e0ce53cde4012fcf2b758214a0422d2faaa798e86e19d7481b42df2b36a73d287ff28c20cce01ce598771fec16a8f1f00305c06010126013a6c1de9f589b4e79d693717cd88ad1c42a2d99fa96617ba0bc6365b68e21a70ebc447904aa27979e1514433cfd83bfec09f137c747d47582cb63eb28f873fb94cf7a59ff764ddfbb687d79a58bb10f85949269f7f72c611a5e0fbb52adfa298ff060ec2eb7216fd7302ea8fb07798cbb3be25cb53ac8161aac2b5bbcfbcfb01c113d28bd1cb0333fb89ac82a95930f7abded0a2f5a623cc6a1f62bf3f38ef1b81c1e50a634f657dbb6770e4af45879e2fb1e00c742e7b52205c8015b5c0f5b1e40186ff9aa7288ab3e01a51fb87761f9bc6837082af109b39cc9f620") require.NoError(t, err) - metadata, err := sniff.QUICClientHello(context.Background(), pkt) + var metadata adapter.InboundContext + 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) + pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28") require.NoError(t, err) - require.Equal(t, metadata.Domain, "cloudflare-quic.com") + err = sniff.QUICClientHello(context.Background(), &metadata, pkt) + require.NoError(t, err) + require.Equal(t, metadata.Domain, "google.com") +} + +func TestSniffUQUICChrome115(t *testing.T) { + t.Parallel() + pkt, err := hex.DecodeString("cb0000000108181e17c387120abc000044d0705b6a3ef9ee37a8d3949a7d393ed078243c2ee2c3627fad1c3f107c117f4f071131ad61848068fcbbe5c65803c147f7f8ec5e2cd77b77beea23ba779d936dccac540f8396400e3190ea35cc2942af4171a04cb14272491920f90124959f44e80143678c0b52f5d31af319aaa589db2f940f004562724d0af40f737e1bb0002a071e6a1dbc9f52c64f070806a5010abed0298053634d9c9126bd7949ae5087998ade762c0ad06691d99c0875a38c601fc1ee77bfc3b8c11381829f2c9bdd022f4499c43ff1d6aee1a0d296861461dda217d22c568b276016ef3929e59d2f7d7ddf7809920fb7dc805641608949f3f8466ab3d37149aac501f0b107d808f3add4acfc657e4a82e2b88e97a6c74a00c419548760ab3414ba13915c78a1ca79dceee8d59fbe299f20b671ac44823218368b2a026baa55170cf549519ac21dbb6d31d248bd339438a4e663bcdca1fe3ae3f045a5dc19b122e9db9d7af9757076666dda4e9ace1c67def77fa14786f0cab3ebf7a270ea6e2b37838318c95779f80c3b8471948d0046c3614b3a13477c939a39a7855d85d13522a45ae0765739cd5eedef87237e824a929983ace27640c6495dbf5a72fa0b96893dc5d28f3988249a57bdb458d460b4a57043de3da750a76b6e5d2259247ca27cd864ea18f0d09aa62ab6eb7c014fb43179b2a1963d170b756cce83eeaebff78a828d025c811848e16ff862a8080d093478cd2208c8ab0803178325bc0d9d6bb25e62fa50c4ad15cf80916da6578796932036c72e43eb480d1e423ed812ac75a97722f8416529b82ba8ee2219c535012282bb17066bd53e78b87a71abdb7ebdb2a7c2766ff8397962e87d0f85485b64b4ee81cc84f99c47f33f2b0872716441992773f59186e38d32dbf5609a6fda94cb928cd25f5a7a3ab736b5a4236b6d5409ab18892c6a4d3480fc2350abfdf0bab1cedb55bdf0760fdb703e6688f4de596254eed4ed3e67eb03d0717b8e15b31e735214e588c87ae36bc6c310e1894b4c15143e4ccf287b2dbc707a946bf9671ae3c574f9486b2c82eec784bba4cbc76113cbe0f97ac8c13cfa38f2925ab9d06887a612ce48280a91d7e074e6caf898d88e2bbf71360899abf48a03f9a70cf2891199f2d63b116f4871af0ebb4f4906792f66cc21d1609f189138532875c129a68c73e7bcd3b5d8100beac1d8ac4b20d94a59ac8df5a5af58a9acb20413eadf97189f5f19ff889155f0c4d37514ec184eb6903967ff38a41fc087abb0f2cad3761d6e3f95f92a09a72f5c065b16e188088b87460241f27ecdb1bc6ece92c8d36b2d68b58d0fb4d4b3c928c579ade8ae5a995833aadd297c30a37f7bc35440fc97070e1b198e0fac00157452177d16d2803b4239997452b4ad3a951173bdec47a033fd7f8a7942accaa9aaa905b3c5a2175e7c3e07c48bf25331727fd69cd1e64d74d8c9d4a6f8f4491adb7bc911505cb19877083d8f21a12475e313fccf57877ff3556318e81ed9145dd9427f2b65275440893035f417481f721c69215af8ae103530cd0a1d35bf2cb5a27628f8d44d7c6f5ec12ce79d0a8333e0eb48771115d0a191304e46b8db19bbe5c40f1c346dde98e76ff5e21ff38d2c34e60cb07766ed529dd6d2cbacd7fbf1ed8a0e6e40decad0ca5021e91552be87c156d3ae2fffef41c65b14ba6d488f2c3227a1ab11ffce0e2dc47723a69da27a67a7f26e1cb13a7103af9b87a8db8e18ea") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.QUICClientHello(context.Background(), &metadata, pkt) + require.NoError(t, err) + require.Equal(t, metadata.Protocol, C.ProtocolQUIC) + require.Equal(t, metadata.Client, C.ClientQUICGo) + require.Equal(t, metadata.Domain, "www.google.com") +} + +func TestSniffQUICFirefox(t *testing.T) { + t.Parallel() + pkt, err := hex.DecodeString("c8000000010867f174d7ebfe1b0803cd9c20004286de068f7963cf1736349ee6ebe0ddcd3e4cd0041a51ced3f7ce9eea1fb595458e74bdb4b792b16449bd8cae71419862c4fcbe766eaec7d1af65cd298e1dd46f8bd94a77ab4ca28c54b8e9773de3f02d7cb2463c9f7dcacfb311f024b0266ec6ab7bfb615b4148333fb4d4ece7c4cd90029ca30c2cbae2216b428499ec873fa125797e71c5a5da85087760ad37ca610020f71b76e82651c47576e20bf33cf676cb2d400b8c09d3c8cb4e21c47d2b21f6b68732bef30c8cefd5c723fc23eb29e6f7f65a5e52aad9055c1fb3d8b1811f0380b38d7e2eee8eb37dd5bd5d4ca4b66540175d916289d88a9df7c161964d713999c5057d27edb298ef5164352568b0d4bac3c15d90456e8fd460e41b81d0ec1b1e94b87d3333cc6908b018e0914ae1f214d73e75398da3d55a0106161d3a75897b4eb66e98c59010fae75f0d367d38be48c3a5c58bc8a30773c3fff50690ac9d487822f85d4f5713d626baa92d36e858dd21259cf814bce0b90d18da88a1ade40113e5a088cdb304a2558879152a8cf15c1839e056378aa41acba6fcb9974dee54bd50b5d4eb2c475654e06c0ec06b7f18f4462c808684843a1071041b9bfb2688324e0120144944416e30e83eedbbbcbc275b1f53762d3db18f0998ce54f0e1c512946b4098f07781d49264fa148f4c8220a3b02e73d7f15554aa370aafeff73cb75c52c494edf90f0261abfdd32a4d670f729de50266162687aa8efe14b8506f313b058b02aaaab5825428f5f4510b8e49451fdcb7b5a4af4b59c831afcb89fb4f64dba78e3b38387e87e9e8cdaa1f3b700a87c7d442388863b8950296e5773b38f308d62f52548c0bbf308e40540747cca5bf99b1345bc0d70b8f0e69a83b85a8d69f795b87f93e2bfccf52b529afea4ff6fd456957000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.QUICClientHello(context.Background(), &metadata, pkt) + require.NoError(t, err) + require.Equal(t, metadata.Protocol, C.ProtocolQUIC) + require.Equal(t, metadata.Client, C.ClientFirefox) + require.Equal(t, metadata.Domain, "www.google.com") } -func TestSniffQUICFragment(t *testing.T) { +func TestSniffQUICSafari(t *testing.T) { t.Parallel() - pkt, err := hex.DecodeString("cc00000001082e3d5d1b64040c55000044d0ccea69e773f6631c1d18b04ae9ee75fcfc34ef74fa62533c93534338a86f101a05d70e0697fb483063fa85db1c59ccfbda5c35234931d8524d8aac37eaaad649470a67794cd754b23c98695238b8363452333bc8c4858376b4166e001da2006e35cf98a91e11a56419b2786775284942d0f7163982f7c248867d12dd374957481dbc564013ff785e1916195eef671f725908f761099d992d69231336ba81d9e25fe2fa3a6eff4318a6ccf10176fc841a1b315f7b35c5b292266fc869d76ca533e7d14e86d82db2e22eacd350977e47d2e012d8a5891c5aaf2a0f4c2b2dae897c161e5b68cbb4dee952472bdc1e21504b8f02534ec4366ce3f8bf86efc78e0232778fbd554457567112abdcafcf6d4d8fcf35083c25d9495679614aba21696e338c62b585046cc55ba8c09c844361d889a47c3ea703b4e23545a9ab2c0bb369693a9ddfb5daffa85cf80fdd6ad66738664e5b0a551729b4955cff7255afcb04dee88c2f072c9de7400947a1bd9327ac5d012a33000ada021d4c03d249fb017d6ac9200b2f9436beab8183ddfbe2d8aee31ffb7df9e1cc181c1af80c39a89965d18ed12da8e3ebe2ae1fbe4b348f83ba19e3e3d1c9b22bcf03ab6ad9b30fe180623faa291ebad83bcd71d7b57f2f5e2f3b8e81d24fb70b2f2159239e8f21ffafef2747aba47d97ab4081e603c018b10678cf99cab1fb42156a14486fa435153979d7279fd22cd40af7088bfc7eff41af2f4b3c0c8864d0040d74dff427f7bffdb8c278474ea00311326cf4925471a8cf596cb92119f19e0f789490ba9cb77b98015a987d93e0324cf1a38b55109f00c3e6ddc5180fb107bf468323afec9bb49fd6a86418569789d66cafe3b8253c2aebb3af3782c1c54dd560487d031d28e6a6e23e159581bb1d47efc4da3fe1d169f9ffb0ca9ba61af0a38a92fde5bc5e6ec026e8378a6315a7b95abf1d2da790a391306ce74d0baf8e2ce648ca74c487f2c0a76a28a80cdf5bd34316eb607684fe7e6d9e83824a00e07660d0b90e3cddd61ebf10748263474afa88c300549e64ce2e90560bb1a12dee7e9484f729a8a4ee7c5651adb5194b3b3ae38e501567c7dbf36e7bb37a2c20b74655f47f2d9af18e52e9d4c9c9eee8e63745779b8f0b06f3a09d846ba62eb978ad77c85de1ee2fee3fbb4c2d283c73e1ccba56a4658e48a2665d200f7f9342f8e84c2ba490094a4f94feec89e42d2f654f564c2beb2997bafa1fc2c68ad8e160b63587d49abc31b834878d52acfb05fb73d0e059b206162e3c90b40c4bc08407ffcb3c08431895b691a3fea923f1f3b48db75d3e6b91fd319ffe4d486e0e14bd5c6affc838dee63d9e0b80f169b5e6c02c7321dcb20deb2b8e707b60e345a308d505bbf26a93d8f18b39d62632e9a77cbe48b3b32eb8819d6311a49820d40f5acbf0273c91c36b2269a03e72ee64df3dfb10ddefe73c64ef60870b2b77bd99dea655f5fe791b538a929a14d99f6d69685d72431ea5f0f4b27a044f2f575ab474fcc3857895934de1ca2581798eaef2c17fe5aaf2e6add97fa32997c7026f15c1b1ad0e6043ae506027a7c0242546fdc851cca39a204e56879f2cef838be8ec66e0f2292f8c862e06f810eb9b80c7a467ce6e90155206352c7f82b1173ba3b98d35bb72c259a60db20dd1a43fe6d7aef0265e6eaa5caafd9b64b448ff745a2046acbdb65cf2a5007809808a4828dc99097feedc734c236260c584") + pkt, err := hex.DecodeString("c70000000108e4e75af2e223198a0000449ef2d83cb4473a62765eba67424cd4a5817315cbf55a9e8daaca360904b0bae60b1629cfeba11e2dfbbf5ea4c588cb134e31af36fd7a409fb0fcc0187e9b56037ac37964ed20a8c1ca19fd6cfd53398324b3d0c71537294f769db208fa998b6811234a4a7eb3b5eceb457ae92e3a2d98f7c110702db8064b5c29fa3298eb1d0529fd445a84a5fd6ff8709be90f8af4f94998d8a8f2953bb05ad08c80668eca784c6aec959114e68e5b827e7c41c79f2277c716a967e7fcc8d1b77442e6cb18329dbedb34b473516b468cba5fc20659e655fbe37f36408289b9a475fcee091bd82828d3be00367e9e5cec9423bb97854abdada1d7562a3777756eb3bddef826ddc1ef46137cb01bb504a54d410d9bcb74cd5f959050c84edf343fa6a49708c228a758ee7adbbadf260b2f1984911489712e2cb364a3d6520badba4b7e539b9c163eeddfd96c0abb0de151e47496bb9750be76ee17ccdb61d35d2c6795174037d6f9d282c3f36c4d9a90b64f3b6ddd0cf4d9ed8e6f7805e25928fa04b087e63ae02761df30720cc01dfc32b64c575c8a66ef82e9a17400ff80cd8609b93ba16d668f4aa734e71c4a5d145f14ee1151bec970214e0ff83fc3e1e85d8694f2975f9155c57c18b7b69bb6a36832a9435f1f4b346a7be188f3a75f9ad2cc6ad0a3d26d6fa7d4c1179bd49bd5989d15ba43ff602890107db96484695086627356750d7b2b3b714ba65d564654e8f60ac10f5b6d3bfb507e8eaa31bab1da2d676195046d165c7f8b32829c9f9b68d97b2af7ac04a1369357e4b65de2b2f24eaf27cc8d95e05db001adebe726f927a94e43e62ce671e6e306e16f05aafcbe6c49080e80286d7939f375023d110a5ad9069364ae928ca480454a9dcddd61bc48b7efeb716a5bd6c7cd39c486ceb20c738af6abf22ba1ddd8b4a3b781fc2f251173409e1aadccbd7514e97106d0ebfc3af6e59445f74cd733a1ba99b10fce3fb4e9f7c88f5e25b567f5ba2b8dabacd375e7faf7634bfa178cbe51aee63032c5126b196ea47b02385fc3062a000fb7e4b4d0d12e74579f8830ede20d10829496032b2cc56743287f9a9b4d5091877a82fea44deb2cffac8a379f78a151d99e28cbc74d732c083bf06d50584e3f18f254e71a48d6ababaf6fff6f425e9be001510dfbe6a32a27792c00ada036b62ddb90c706d7b882c76a7072f5dd11c69a1f49d4ba183cb0b57545419fa27b9b9706098848935ae9c9e8fbe9fac165d1339128b991a73d20e7795e8d6a8c6adfbf20bf13ada43f2aef3ba78c14697910507132623f721387dce60c4707225b84d9782d469a5d9eaa099f35d6a590ef142ddef766495cf3337815ceef5ff2b3ed352637e72b5c23a2a8ff7d7440236a19b981d47f8e519a0431ebfbc0b78d8a36798b4c060c0c6793499f1e2e818862560a5b501c8d02ba1517be1941da2af5b174e0189c62978d878eb0f9c9db3a9221c28fb94645cf6e85ff2eea8c65ba3083a7382b131b83102dd67aa5453ad7375a4eb8c69fc479fbd29dab8924f801d253f2c997120b705c6e5217fb74702e2f1038917dd5fb0eeb7ae1bf7a668fc7d50c034b4cd5a057a8482e6bc9c921297f44e76967265623a167cd9883eb6e64bc77856dc333bd605d7df3bed0e5cecb5a99fe8b62873d58530f") require.NoError(t, err) - metadata, err := sniff.QUICClientHello(context.Background(), pkt) + var metadata adapter.InboundContext + err = sniff.QUICClientHello(context.Background(), &metadata, pkt) require.NoError(t, err) - require.Equal(t, metadata.Domain, "cloudflare-quic.com") + require.Equal(t, metadata.Protocol, C.ProtocolQUIC) + require.Equal(t, metadata.Client, C.ClientSafari) + require.Equal(t, metadata.Domain, "www.google.com") } func FuzzSniffQUIC(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { - sniff.QUICClientHello(context.Background(), data) + var metadata adapter.InboundContext + err := sniff.QUICClientHello(context.Background(), &metadata, data) + require.Error(t, err) }) } diff --git a/common/sniff/sniff.go b/common/sniff/sniff.go index 424311a86e..eaea3b1018 100644 --- a/common/sniff/sniff.go +++ b/common/sniff/sniff.go @@ -14,11 +14,11 @@ import ( ) type ( - StreamSniffer = func(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error) - PacketSniffer = func(ctx context.Context, packet []byte) (*adapter.InboundContext, error) + StreamSniffer = func(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error + PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error ) -func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) (*adapter.InboundContext, error) { +func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) error { if timeout == 0 { timeout = C.ReadPayloadTimeout } @@ -28,7 +28,7 @@ func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout for i := 0; i < 3; i++ { err := conn.SetReadDeadline(deadline) if err != nil { - return nil, E.Cause(err, "set read deadline") + return E.Cause(err, "set read deadline") } _, err = buffer.ReadOnceFrom(conn) err = E.Errors(err, conn.SetReadDeadline(time.Time{})) @@ -36,27 +36,27 @@ func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout if i > 0 { break } - return nil, E.Cause(err, "read payload") + return E.Cause(err, "read payload") } for _, sniffer := range sniffers { - metadata, err := sniffer(ctx, bytes.NewReader(buffer.Bytes())) - if metadata != nil { - return metadata, nil + err = sniffer(ctx, metadata, bytes.NewReader(buffer.Bytes())) + if err == nil { + return nil } errors = append(errors, err) } } - return nil, E.Errors(errors...) + return E.Errors(errors...) } -func PeekPacket(ctx context.Context, packet []byte, sniffers ...PacketSniffer) (*adapter.InboundContext, error) { +func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error { var errors []error for _, sniffer := range sniffers { - metadata, err := sniffer(ctx, packet) - if metadata != nil { - return metadata, nil + err := sniffer(ctx, metadata, packet) + if err == nil { + return nil } errors = append(errors, err) } - return nil, E.Errors(errors...) + return E.Errors(errors...) } diff --git a/common/sniff/stun.go b/common/sniff/stun.go index 66a72d7e17..dfd1125978 100644 --- a/common/sniff/stun.go +++ b/common/sniff/stun.go @@ -9,16 +9,17 @@ import ( C "github.com/sagernet/sing-box/constant" ) -func STUNMessage(ctx context.Context, packet []byte) (*adapter.InboundContext, error) { +func STUNMessage(_ context.Context, metadata *adapter.InboundContext, packet []byte) error { pLen := len(packet) if pLen < 20 { - return nil, os.ErrInvalid + return os.ErrInvalid } if binary.BigEndian.Uint32(packet[4:8]) != 0x2112A442 { - return nil, os.ErrInvalid + return os.ErrInvalid } if len(packet) < 20+int(binary.BigEndian.Uint16(packet[2:4])) { - return nil, os.ErrInvalid + return os.ErrInvalid } - return &adapter.InboundContext{Protocol: C.ProtocolSTUN}, nil + metadata.Protocol = C.ProtocolSTUN + return nil } diff --git a/common/sniff/stun_test.go b/common/sniff/stun_test.go index 6da89162a9..f465763e1f 100644 --- a/common/sniff/stun_test.go +++ b/common/sniff/stun_test.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "testing" + "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" @@ -15,14 +16,16 @@ func TestSniffSTUN(t *testing.T) { t.Parallel() packet, err := hex.DecodeString("000100002112a44224b1a025d0c180c484341306") require.NoError(t, err) - metadata, err := sniff.STUNMessage(context.Background(), packet) + var metadata adapter.InboundContext + err = sniff.STUNMessage(context.Background(), &metadata, packet) require.NoError(t, err) require.Equal(t, metadata.Protocol, C.ProtocolSTUN) } func FuzzSniffSTUN(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { - if _, err := sniff.STUNMessage(context.Background(), data); err == nil { + var metadata adapter.InboundContext + if err := sniff.STUNMessage(context.Background(), &metadata, data); err == nil { t.Fail() } }) diff --git a/common/sniff/tls.go b/common/sniff/tls.go index dcba195afe..6fe430e2d8 100644 --- a/common/sniff/tls.go +++ b/common/sniff/tls.go @@ -10,7 +10,7 @@ import ( "github.com/sagernet/sing/common/bufio" ) -func TLSClientHello(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error) { +func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error { var clientHello *tls.ClientHelloInfo err := tls.Server(bufio.NewReadOnlyConn(reader), &tls.Config{ GetConfigForClient: func(argHello *tls.ClientHelloInfo) (*tls.Config, error) { @@ -19,7 +19,9 @@ func TLSClientHello(ctx context.Context, reader io.Reader) (*adapter.InboundCont }, }).HandshakeContext(ctx) if clientHello != nil { - return &adapter.InboundContext{Protocol: C.ProtocolTLS, Domain: clientHello.ServerName}, nil + metadata.Protocol = C.ProtocolTLS + metadata.Domain = clientHello.ServerName + return nil } - return nil, err + return err } diff --git a/constant/protocol.go b/constant/protocol.go index 915e64ca19..8b1854d419 100644 --- a/constant/protocol.go +++ b/constant/protocol.go @@ -9,3 +9,11 @@ const ( ProtocolBitTorrent = "bittorrent" ProtocolDTLS = "dtls" ) + +const ( + ClientChromium = "chromium" + ClientSafari = "safari" + ClientFirefox = "firefox" + ClientQUICGo = "quic-go" + ClientUnknown = "unknown" +) diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 23d2bf1b0c..8e61e51df2 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -4,6 +4,7 @@ icon: material/alert-decagram !!! quote "Changes in sing-box 1.10.0" + :material-plus: [client](#client) :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) @@ -40,6 +41,12 @@ icon: material/alert-decagram "http", "quic" ], + "client": [ + "chromium", + "safari", + "firefox", + "quic-go" + ], "domain": [ "test.com" ], @@ -166,7 +173,13 @@ Username, see each inbound for details. #### protocol -Sniffed protocol, see [Sniff](/configuration/route/sniff/) for details. +Sniffed protocol, see [Protocol Sniff](/configuration/route/sniff/) for details. + +#### client + +!!! question "Since sing-box 1.10.0" + +Sniffed client type, see [Protocol Sniff](/configuration/route/sniff/) for details. #### network diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index 52d334f22c..68e66cf5c9 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -4,8 +4,9 @@ icon: material/alert-decagram !!! quote "sing-box 1.10.0 中的更改" + :material-plus: [client](#client) :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) - :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) + :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) !!! quote "sing-box 1.8.0 中的更改" @@ -40,6 +41,12 @@ icon: material/alert-decagram "http", "quic" ], + "client": [ + "chromium", + "safari", + "firefox", + "quic-go" + ], "domain": [ "test.com" ], @@ -166,6 +173,12 @@ icon: material/alert-decagram 探测到的协议, 参阅 [协议探测](/zh/configuration/route/sniff/)。 +#### client + +!!! question "自 sing-box 1.10.0 起" + +探测到的客户端类型, 参阅 [协议探测](/zh/configuration/route/sniff/)。 + #### network `tcp` 或 `udp`。 diff --git a/docs/configuration/route/sniff.md b/docs/configuration/route/sniff.md index cc9a5c130d..ab96e75a6f 100644 --- a/docs/configuration/route/sniff.md +++ b/docs/configuration/route/sniff.md @@ -4,19 +4,28 @@ icon: material/new-box !!! quote "Changes in sing-box 1.10.0" - :material-plus: BitTorrent support + :material-plus: QUIC client type detect support for QUIC + :material-plus: Chromium support for QUIC + :material-plus: BitTorrent support :material-plus: DTLS support If enabled in the inbound, the protocol and domain name (if present) of by the connection can be sniffed. #### Supported Protocols -| Network | Protocol | Domain Name | -|:-------:|:------------:|:-----------:| -| TCP | `http` | Host | -| TCP | `tls` | Server Name | -| UDP | `quic` | Server Name | -| UDP | `stun` | / | -| TCP/UDP | `dns` | / | -| TCP/UDP | `bittorrent` | / | -| UDP | `dtls` | / | +| Network | Protocol | Domain Name | Client | +|:-------:|:------------:|:-----------:|:----------------:| +| TCP | `http` | Host | / | +| TCP | `tls` | Server Name | / | +| UDP | `quic` | Server Name | QUIC Client Type | +| UDP | `stun` | / | / | +| TCP/UDP | `dns` | / | / | +| TCP/UDP | `bittorrent` | / | / | +| UDP | `dtls` | / | / | + +| QUIC Client | Type | +|:------------------------:|:----------:| +| Chromium/Cronet | `chrimium` | +| Safari/Apple Network API | `safari` | +| Firefox / uquic firefox | `firefox` | +| quic-go / uquic chrome | `quic-go` | \ No newline at end of file diff --git a/docs/configuration/route/sniff.zh.md b/docs/configuration/route/sniff.zh.md index 523ed44b90..1ebc6d38d1 100644 --- a/docs/configuration/route/sniff.zh.md +++ b/docs/configuration/route/sniff.zh.md @@ -4,19 +4,28 @@ icon: material/new-box !!! quote "sing-box 1.10.0 中的更改" - :material-plus: BitTorrent 支持 + :material-plus: QUIC 的 客户端类型探测支持 + :material-plus: QUIC 的 Chromium 支持 + :material-plus: BitTorrent 支持 :material-plus: DTLS 支持 如果在入站中启用,则可以嗅探连接的协议和域名(如果存在)。 #### 支持的协议 -| 网络 | 协议 | 域名 | -|:-------:|:------------:|:-----------:| -| TCP | `http` | Host | -| TCP | `tls` | Server Name | -| UDP | `quic` | Server Name | -| UDP | `stun` | / | -| TCP/UDP | `dns` | / | -| TCP/UDP | `bittorrent` | / | -| UDP | `dtls` | / | +| 网络 | 协议 | 域名 | 客户端 | +|:-------:|:------------:|:-----------:|:----------:| +| TCP | `http` | Host | / | +| TCP | `tls` | Server Name | / | +| UDP | `quic` | Server Name | QUIC 客户端类型 | +| UDP | `stun` | / | / | +| TCP/UDP | `dns` | / | / | +| TCP/UDP | `bittorrent` | / | / | +| UDP | `dtls` | / | / | + +| QUIC 客户端 | 类型 | +|:------------------------:|:----------:| +| Chromium/Cronet | `chrimium` | +| Safari/Apple Network API | `safari` | +| Firefox / uquic firefox | `firefox` | +| quic-go / uquic chrome | `quic-go` | \ No newline at end of file diff --git a/go.mod b/go.mod index 1f3e19dd33..d4601fac0e 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( go.uber.org/zap v1.27.0 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/crypto v0.25.0 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 golang.org/x/net v0.27.0 golang.org/x/sys v0.22.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 @@ -89,7 +90,6 @@ require ( github.com/vishvananda/netns v0.0.4 // indirect github.com/zeebo/blake3 v0.2.3 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/text v0.16.0 // indirect diff --git a/option/rule.go b/option/rule.go index 74dd13c615..5c69ef00e3 100644 --- a/option/rule.go +++ b/option/rule.go @@ -70,6 +70,7 @@ type _DefaultRule struct { Network Listable[string] `json:"network,omitempty"` AuthUser Listable[string] `json:"auth_user,omitempty"` Protocol Listable[string] `json:"protocol,omitempty"` + Client Listable[string] `json:"client,omitempty"` Domain Listable[string] `json:"domain,omitempty"` DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` diff --git a/route/router.go b/route/router.go index 9a89becb34..4653e0bcf4 100644 --- a/route/router.go +++ b/route/router.go @@ -854,8 +854,9 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad if metadata.InboundOptions.SniffEnabled { buffer := buf.NewPacket() - sniffMetadata, err := sniff.PeekStream( + err := sniff.PeekStream( ctx, + &metadata, conn, buffer, time.Duration(metadata.InboundOptions.SniffTimeout), @@ -864,9 +865,7 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad sniff.HTTPHost, sniff.BitTorrent, ) - if sniffMetadata != nil { - metadata.Protocol = sniffMetadata.Protocol - metadata.Domain = sniffMetadata.Domain + if err == nil { if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) { metadata.Destination = M.Socksaddr{ Fqdn: metadata.Domain, @@ -878,8 +877,6 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad } else { r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol) } - } else if err != nil { - r.logger.TraceContext(ctx, "sniffed no protocol: ", err) } if !buffer.IsEmpty() { conn = bufio.NewCachedConn(conn, buffer) @@ -980,65 +977,89 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m }*/ if metadata.InboundOptions.SniffEnabled || metadata.Destination.Addr.IsUnspecified() { - var ( - buffer = buf.NewPacket() - destination M.Socksaddr - done = make(chan struct{}) - err error - ) - go func() { - sniffTimeout := C.ReadPayloadTimeout - if metadata.InboundOptions.SniffTimeout > 0 { - sniffTimeout = time.Duration(metadata.InboundOptions.SniffTimeout) - } - conn.SetReadDeadline(time.Now().Add(sniffTimeout)) - destination, err = conn.ReadPacket(buffer) - conn.SetReadDeadline(time.Time{}) - close(done) - }() - select { - case <-done: - case <-ctx.Done(): - conn.Close() - return ctx.Err() - } - if err != nil { - buffer.Release() - if !errors.Is(err, os.ErrDeadlineExceeded) { - return err - } - } else { - if metadata.Destination.Addr.IsUnspecified() { - metadata.Destination = destination - } - if metadata.InboundOptions.SniffEnabled { - sniffMetadata, _ := sniff.PeekPacket( - ctx, - buffer.Bytes(), - sniff.DomainNameQuery, - sniff.QUICClientHello, - sniff.STUNMessage, - sniff.UTP, - sniff.UDPTracker, - sniff.DTLSRecord, + var bufferList []*buf.Buffer + for { + var ( + buffer = buf.NewPacket() + destination M.Socksaddr + done = make(chan struct{}) + err error ) - if sniffMetadata != nil { - metadata.Protocol = sniffMetadata.Protocol - metadata.Domain = sniffMetadata.Domain - if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) { - metadata.Destination = M.Socksaddr{ - Fqdn: metadata.Domain, - Port: metadata.Destination.Port, - } - } - if metadata.Domain != "" { - r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain) + go func() { + sniffTimeout := C.ReadPayloadTimeout + if metadata.InboundOptions.SniffTimeout > 0 { + sniffTimeout = time.Duration(metadata.InboundOptions.SniffTimeout) + } + conn.SetReadDeadline(time.Now().Add(sniffTimeout)) + destination, err = conn.ReadPacket(buffer) + conn.SetReadDeadline(time.Time{}) + close(done) + }() + select { + case <-done: + case <-ctx.Done(): + conn.Close() + return ctx.Err() + } + if err != nil { + buffer.Release() + if !errors.Is(err, os.ErrDeadlineExceeded) { + return err + } + } else { + if metadata.Destination.Addr.IsUnspecified() { + metadata.Destination = destination + } + if metadata.InboundOptions.SniffEnabled { + if len(bufferList) > 0 { + err = sniff.PeekPacket( + ctx, + &metadata, + buffer.Bytes(), + sniff.QUICClientHello, + ) } else { - r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol) + err = sniff.PeekPacket( + ctx, + &metadata, + buffer.Bytes(), + sniff.DomainNameQuery, + sniff.QUICClientHello, + sniff.STUNMessage, + sniff.UTP, + sniff.UDPTracker, + sniff.DTLSRecord, + ) + } + if E.IsMulti(err, sniff.ErrClientHelloFragmented) && len(bufferList) == 0 { + bufferList = append(bufferList, buffer) + r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello") + continue + } + if metadata.Protocol != "" { + if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) { + metadata.Destination = M.Socksaddr{ + Fqdn: metadata.Domain, + Port: metadata.Destination.Port, + } + } + if metadata.Domain != "" && metadata.Client != "" { + r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain, ", client: ", metadata.Client) + } else if metadata.Domain != "" { + r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain) + } else if metadata.Client != "" { + r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", client: ", metadata.Client) + } else { + r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol) + } } } } conn = bufio.NewCachedPacketConn(conn, buffer, destination) + for _, cachedBuffer := range common.Reverse(bufferList) { + conn = bufio.NewCachedPacketConn(conn, cachedBuffer, destination) + } + break } } if r.dnsReverseMapping != nil && metadata.Domain == "" { diff --git a/route/rule_default.go b/route/rule_default.go index 53e53bdf86..4bbc2b6b2e 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -79,6 +79,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.Client) > 0 { + item := NewClientItem(options.Client) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 { item := NewDomainItem(options.Domain, options.DomainSuffix) rule.destinationAddressItems = append(rule.destinationAddressItems, item) diff --git a/route/rule_item_client.go b/route/rule_item_client.go new file mode 100644 index 0000000000..eeab440240 --- /dev/null +++ b/route/rule_item_client.go @@ -0,0 +1,37 @@ +package route + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*ClientItem)(nil) + +type ClientItem struct { + clients []string + clientMap map[string]bool +} + +func NewClientItem(clients []string) *ClientItem { + clientMap := make(map[string]bool) + for _, client := range clients { + clientMap[client] = true + } + return &ClientItem{ + clients: clients, + clientMap: clientMap, + } +} + +func (r *ClientItem) Match(metadata *adapter.InboundContext) bool { + return r.clientMap[metadata.Client] +} + +func (r *ClientItem) String() string { + if len(r.clients) == 1 { + return F.ToString("client=", r.clients[0]) + } + return F.ToString("client=[", strings.Join(r.clients, " "), "]") +} From 7f45055ed1286726f7c6cdbcfa9a46ef5db12c22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 26 Jul 2024 08:03:08 +0800 Subject: [PATCH 22/31] Add AdGuard DNS filter support --- cmd/sing-box/cmd_rule_set_convert.go | 88 +++++ .../internal/convertor/adguard/convertor.go | 346 ++++++++++++++++++ .../convertor/adguard/convertor_test.go | 140 +++++++ common/srs/binary.go | 22 ++ docs/configuration/rule-set/adguard.md | 71 ++++ mkdocs.yml | 1 + option/rule_set.go | 3 + route/rule_headless.go | 9 + route/rule_item_adguard.go | 43 +++ 9 files changed, 723 insertions(+) create mode 100644 cmd/sing-box/cmd_rule_set_convert.go create mode 100644 cmd/sing-box/internal/convertor/adguard/convertor.go create mode 100644 cmd/sing-box/internal/convertor/adguard/convertor_test.go create mode 100644 docs/configuration/rule-set/adguard.md create mode 100644 route/rule_item_adguard.go diff --git a/cmd/sing-box/cmd_rule_set_convert.go b/cmd/sing-box/cmd_rule_set_convert.go new file mode 100644 index 0000000000..8d2092ea28 --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_convert.go @@ -0,0 +1,88 @@ +package main + +import ( + "io" + "os" + "strings" + + "github.com/sagernet/sing-box/cmd/sing-box/internal/convertor/adguard" + "github.com/sagernet/sing-box/common/srs" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var ( + flagRuleSetConvertType string + flagRuleSetConvertOutput string +) + +var commandRuleSetConvert = &cobra.Command{ + Use: "convert [source-path]", + Short: "Convert adguard DNS filter to rule-set", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := convertRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSet.AddCommand(commandRuleSetConvert) + commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertType, "type", "t", "", "Source type, available: adguard") + commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file") +} + +func convertRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + var rules []option.HeadlessRule + switch flagRuleSetConvertType { + case "adguard": + rules, err = adguard.Convert(reader) + case "": + return E.New("source type is required") + default: + return E.New("unsupported source type: ", flagRuleSetConvertType) + } + if err != nil { + return err + } + var outputPath string + if flagRuleSetConvertOutput == flagRuleSetCompileDefaultOutput { + if strings.HasSuffix(sourcePath, ".txt") { + outputPath = sourcePath[:len(sourcePath)-4] + ".srs" + } else { + outputPath = sourcePath + ".srs" + } + } else { + outputPath = flagRuleSetConvertOutput + } + outputFile, err := os.Create(outputPath) + if err != nil { + return err + } + defer outputFile.Close() + err = srs.Write(outputFile, option.PlainRuleSet{Rules: rules}, true) + if err != nil { + outputFile.Close() + os.Remove(outputPath) + return err + } + outputFile.Close() + return nil +} diff --git a/cmd/sing-box/internal/convertor/adguard/convertor.go b/cmd/sing-box/internal/convertor/adguard/convertor.go new file mode 100644 index 0000000000..ff60929b8d --- /dev/null +++ b/cmd/sing-box/internal/convertor/adguard/convertor.go @@ -0,0 +1,346 @@ +package adguard + +import ( + "bufio" + "io" + "net/netip" + "os" + "strconv" + "strings" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" +) + +type agdguardRuleLine struct { + ruleLine string + isRawDomain bool + isExclude bool + isSuffix bool + hasStart bool + hasEnd bool + isRegexp bool + isImportant bool +} + +func Convert(reader io.Reader) ([]option.HeadlessRule, error) { + scanner := bufio.NewScanner(reader) + var ( + ruleLines []agdguardRuleLine + ignoredLines int + ) +parseLine: + for scanner.Scan() { + ruleLine := scanner.Text() + if ruleLine == "" || ruleLine[0] == '!' || ruleLine[0] == '#' { + continue + } + originRuleLine := ruleLine + if M.IsDomainName(ruleLine) { + ruleLines = append(ruleLines, agdguardRuleLine{ + ruleLine: ruleLine, + isRawDomain: true, + }) + continue + } + hostLine, err := parseAdGuardHostLine(ruleLine) + if err == nil { + if hostLine != "" { + ruleLines = append(ruleLines, agdguardRuleLine{ + ruleLine: hostLine, + isRawDomain: true, + hasStart: true, + hasEnd: true, + }) + } + continue + } + if strings.HasSuffix(ruleLine, "|") { + ruleLine = ruleLine[:len(ruleLine)-1] + } + var ( + isExclude bool + isSuffix bool + hasStart bool + hasEnd bool + isRegexp bool + isImportant bool + ) + if !strings.HasPrefix(ruleLine, "/") && strings.Contains(ruleLine, "$") { + params := common.SubstringAfter(ruleLine, "$") + for _, param := range strings.Split(params, ",") { + paramParts := strings.Split(param, "=") + var ignored bool + if len(paramParts) > 0 && len(paramParts) <= 2 { + switch paramParts[0] { + case "app", "network": + // maybe support by package_name/process_name + case "dnstype": + // maybe support by query_type + case "important": + ignored = true + isImportant = true + case "dnsrewrite": + if len(paramParts) == 2 && M.ParseAddr(paramParts[1]).IsUnspecified() { + ignored = true + } + } + } + if !ignored { + ignoredLines++ + log.Debug("ignored unsupported rule with modifier: ", paramParts[0], ": ", ruleLine) + continue parseLine + } + } + ruleLine = common.SubstringBefore(ruleLine, "$") + } + if strings.HasPrefix(ruleLine, "@@") { + ruleLine = ruleLine[2:] + isExclude = true + } + if strings.HasSuffix(ruleLine, "|") { + ruleLine = ruleLine[:len(ruleLine)-1] + } + if strings.HasPrefix(ruleLine, "||") { + ruleLine = ruleLine[2:] + isSuffix = true + } else if strings.HasPrefix(ruleLine, "|") { + ruleLine = ruleLine[1:] + hasStart = true + } + if strings.HasSuffix(ruleLine, "^") { + ruleLine = ruleLine[:len(ruleLine)-1] + hasEnd = true + } + if strings.HasPrefix(ruleLine, "/") && strings.HasSuffix(ruleLine, "/") { + ruleLine = ruleLine[1 : len(ruleLine)-1] + if ignoreIPCIDRRegexp(ruleLine) { + ignoredLines++ + log.Debug("ignored unsupported rule with IPCIDR regexp: ", ruleLine) + continue + } + isRegexp = true + } else { + if strings.Contains(ruleLine, "://") { + ruleLine = common.SubstringAfter(ruleLine, "://") + } + if strings.Contains(ruleLine, "/") { + ignoredLines++ + log.Debug("ignored unsupported rule with path: ", ruleLine) + continue + } + if strings.Contains(ruleLine, "##") { + ignoredLines++ + log.Debug("ignored unsupported rule with element hiding: ", ruleLine) + continue + } + if strings.Contains(ruleLine, "#$#") { + ignoredLines++ + log.Debug("ignored unsupported rule with element hiding: ", ruleLine) + continue + } + var domainCheck string + if strings.HasPrefix(ruleLine, ".") || strings.HasPrefix(ruleLine, "-") { + domainCheck = "r" + ruleLine + } else { + domainCheck = ruleLine + } + if ruleLine == "" { + ignoredLines++ + log.Debug("ignored unsupported rule with empty domain", originRuleLine) + continue + } else { + domainCheck = strings.ReplaceAll(domainCheck, "*", "x") + if !M.IsDomainName(domainCheck) { + _, ipErr := parseADGuardIPCIDRLine(ruleLine) + if ipErr == nil { + ignoredLines++ + log.Debug("ignored unsupported rule with IPCIDR: ", ruleLine) + continue + } + if M.ParseSocksaddr(domainCheck).Port != 0 { + log.Debug("ignored unsupported rule with port: ", ruleLine) + } else { + log.Debug("ignored unsupported rule with invalid domain: ", ruleLine) + } + ignoredLines++ + continue + } + } + } + ruleLines = append(ruleLines, agdguardRuleLine{ + ruleLine: ruleLine, + isExclude: isExclude, + isSuffix: isSuffix, + hasStart: hasStart, + hasEnd: hasEnd, + isRegexp: isRegexp, + isImportant: isImportant, + }) + } + if len(ruleLines) == 0 { + return nil, E.New("AdGuard rule-set is empty or all rules are unsupported") + } + if common.All(ruleLines, func(it agdguardRuleLine) bool { + return it.isRawDomain + }) { + return []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + Domain: common.Map(ruleLines, func(it agdguardRuleLine) string { + return it.ruleLine + }), + }, + }, + }, nil + } + mapDomain := func(it agdguardRuleLine) string { + ruleLine := it.ruleLine + if it.isSuffix { + ruleLine = "||" + ruleLine + } else if it.hasStart { + ruleLine = "|" + ruleLine + } + if it.hasEnd { + ruleLine += "^" + } + return ruleLine + } + + importantDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && !it.isRegexp && !it.isExclude }), mapDomain) + importantDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && it.isRegexp && !it.isExclude }), mapDomain) + importantExcludeDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && !it.isRegexp && it.isExclude }), mapDomain) + importantExcludeDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && it.isRegexp && it.isExclude }), mapDomain) + domain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && !it.isRegexp && !it.isExclude }), mapDomain) + domainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && it.isRegexp && !it.isExclude }), mapDomain) + excludeDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && !it.isRegexp && it.isExclude }), mapDomain) + excludeDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && it.isRegexp && it.isExclude }), mapDomain) + currentRule := option.HeadlessRule{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + AdGuardDomain: domain, + DomainRegex: domainRegex, + }, + } + if len(excludeDomain) > 0 || len(excludeDomainRegex) > 0 { + currentRule = option.HeadlessRule{ + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalHeadlessRule{ + Mode: C.LogicalTypeAnd, + Rules: []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + AdGuardDomain: excludeDomain, + DomainRegex: excludeDomainRegex, + Invert: true, + }, + }, + currentRule, + }, + }, + } + } + if len(importantDomain) > 0 || len(importantDomainRegex) > 0 { + currentRule = option.HeadlessRule{ + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalHeadlessRule{ + Mode: C.LogicalTypeOr, + Rules: []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + AdGuardDomain: importantDomain, + DomainRegex: importantDomainRegex, + }, + }, + currentRule, + }, + }, + } + } + if len(importantExcludeDomain) > 0 || len(importantExcludeDomainRegex) > 0 { + currentRule = option.HeadlessRule{ + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalHeadlessRule{ + Mode: C.LogicalTypeAnd, + Rules: []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + AdGuardDomain: importantExcludeDomain, + DomainRegex: importantExcludeDomainRegex, + Invert: true, + }, + }, + currentRule, + }, + }, + } + } + log.Info("parsed rules: ", len(ruleLines), "/", len(ruleLines)+ignoredLines) + return []option.HeadlessRule{currentRule}, nil +} + +func ignoreIPCIDRRegexp(ruleLine string) bool { + if strings.HasPrefix(ruleLine, "(http?:\\/\\/)") { + ruleLine = ruleLine[12:] + } else if strings.HasPrefix(ruleLine, "(https?:\\/\\/)") { + ruleLine = ruleLine[13:] + } else if strings.HasPrefix(ruleLine, "^") { + ruleLine = ruleLine[1:] + } else { + return false + } + _, parseErr := strconv.ParseUint(common.SubstringBefore(ruleLine, "\\."), 10, 8) + return parseErr == nil +} + +func parseAdGuardHostLine(ruleLine string) (string, error) { + idx := strings.Index(ruleLine, " ") + if idx == -1 { + return "", os.ErrInvalid + } + address, err := netip.ParseAddr(ruleLine[:idx]) + if err != nil { + return "", err + } + if !address.IsUnspecified() { + return "", nil + } + domain := ruleLine[idx+1:] + if !M.IsDomainName(domain) { + return "", E.New("invalid domain name: ", domain) + } + return domain, nil +} + +func parseADGuardIPCIDRLine(ruleLine string) (netip.Prefix, error) { + var isPrefix bool + if strings.HasSuffix(ruleLine, ".") { + isPrefix = true + ruleLine = ruleLine[:len(ruleLine)-1] + } + ruleStringParts := strings.Split(ruleLine, ".") + if len(ruleStringParts) > 4 || len(ruleStringParts) < 4 && !isPrefix { + return netip.Prefix{}, os.ErrInvalid + } + ruleParts := make([]uint8, 0, len(ruleStringParts)) + for _, part := range ruleStringParts { + rulePart, err := strconv.ParseUint(part, 10, 8) + if err != nil { + return netip.Prefix{}, err + } + ruleParts = append(ruleParts, uint8(rulePart)) + } + bitLen := len(ruleParts) * 8 + for len(ruleParts) < 4 { + ruleParts = append(ruleParts, 0) + } + return netip.PrefixFrom(netip.AddrFrom4(*(*[4]byte)(ruleParts)), bitLen), nil +} diff --git a/cmd/sing-box/internal/convertor/adguard/convertor_test.go b/cmd/sing-box/internal/convertor/adguard/convertor_test.go new file mode 100644 index 0000000000..7da8a22841 --- /dev/null +++ b/cmd/sing-box/internal/convertor/adguard/convertor_test.go @@ -0,0 +1,140 @@ +package adguard + +import ( + "strings" + "testing" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/route" + + "github.com/stretchr/testify/require" +) + +func TestConverter(t *testing.T) { + t.Parallel() + rules, err := Convert(strings.NewReader(` +||example.org^ +|example.com^ +example.net^ +||example.edu +||example.edu.tw^ +|example.gov +example.arpa +@@|sagernet.example.org| +||sagernet.org^$important +@@|sing-box.sagernet.org^$important +`)) + require.NoError(t, err) + require.Len(t, rules, 1) + rule, err := route.NewHeadlessRule(nil, rules[0]) + require.NoError(t, err) + matchDomain := []string{ + "example.org", + "www.example.org", + "example.com", + "example.net", + "isexample.net", + "www.example.net", + "example.edu", + "example.edu.cn", + "example.edu.tw", + "www.example.edu", + "www.example.edu.cn", + "example.gov", + "example.gov.cn", + "example.arpa", + "www.example.arpa", + "isexample.arpa", + "example.arpa.cn", + "www.example.arpa.cn", + "isexample.arpa.cn", + "sagernet.org", + "www.sagernet.org", + } + notMatchDomain := []string{ + "example.org.cn", + "notexample.org", + "example.com.cn", + "www.example.com.cn", + "example.net.cn", + "notexample.edu", + "notexample.edu.cn", + "www.example.gov", + "notexample.gov", + "sagernet.example.org", + "sing-box.sagernet.org", + } + for _, domain := range matchDomain { + require.True(t, rule.Match(&adapter.InboundContext{ + Domain: domain, + }), domain) + } + for _, domain := range notMatchDomain { + require.False(t, rule.Match(&adapter.InboundContext{ + Domain: domain, + }), domain) + } +} + +func TestHosts(t *testing.T) { + t.Parallel() + rules, err := Convert(strings.NewReader(` +127.0.0.1 localhost +::1 localhost #[IPv6] +0.0.0.0 google.com +`)) + require.NoError(t, err) + require.Len(t, rules, 1) + rule, err := route.NewHeadlessRule(nil, rules[0]) + require.NoError(t, err) + matchDomain := []string{ + "google.com", + } + notMatchDomain := []string{ + "www.google.com", + "notgoogle.com", + "localhost", + } + for _, domain := range matchDomain { + require.True(t, rule.Match(&adapter.InboundContext{ + Domain: domain, + }), domain) + } + for _, domain := range notMatchDomain { + require.False(t, rule.Match(&adapter.InboundContext{ + Domain: domain, + }), domain) + } +} + +func TestSimpleHosts(t *testing.T) { + t.Parallel() + rules, err := Convert(strings.NewReader(` +example.com +www.example.org +`)) + require.NoError(t, err) + require.Len(t, rules, 1) + rule, err := route.NewHeadlessRule(nil, rules[0]) + require.NoError(t, err) + matchDomain := []string{ + "example.com", + "www.example.org", + } + notMatchDomain := []string{ + "example.com.cn", + "www.example.com", + "notexample.com", + "example.org", + } + for _, domain := range matchDomain { + require.True(t, rule.Match(&adapter.InboundContext{ + Domain: domain, + }), domain) + } + for _, domain := range notMatchDomain { + require.False(t, rule.Match(&adapter.InboundContext{ + Domain: domain, + }), domain) + } +} diff --git a/common/srs/binary.go b/common/srs/binary.go index 69075f7881..7e4f402b39 100644 --- a/common/srs/binary.go +++ b/common/srs/binary.go @@ -36,6 +36,7 @@ const ( ruleItemPackageName ruleItemWIFISSID ruleItemWIFIBSSID + ruleItemAdGuardDomain ruleItemFinal uint8 = 0xFF ) @@ -212,6 +213,17 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea rule.WIFISSID, err = readRuleItemString(reader) case ruleItemWIFIBSSID: rule.WIFIBSSID, err = readRuleItemString(reader) + case ruleItemAdGuardDomain: + if recover { + err = E.New("unable to decompile binary AdGuard rules to rule-set") + return + } + var matcher *domain.AdGuardMatcher + matcher, err = domain.ReadAdGuardMatcher(reader) + if err != nil { + return + } + rule.AdGuardDomainMatcher = matcher case ruleItemFinal: err = binary.Read(reader, binary.BigEndian, &rule.Invert) return @@ -332,6 +344,16 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen return err } } + if len(rule.AdGuardDomain) > 0 { + err = binary.Write(writer, binary.BigEndian, ruleItemAdGuardDomain) + if err != nil { + return err + } + err = domain.NewAdGuardMatcher(rule.AdGuardDomain).Write(writer) + if err != nil { + return err + } + } err = binary.Write(writer, binary.BigEndian, ruleItemFinal) if err != nil { return err diff --git a/docs/configuration/rule-set/adguard.md b/docs/configuration/rule-set/adguard.md new file mode 100644 index 0000000000..870972bf2f --- /dev/null +++ b/docs/configuration/rule-set/adguard.md @@ -0,0 +1,71 @@ +--- +icon: material/new-box +--- + +# AdGuard DNS Filter + +!!! question "Since sing-box 1.10.0" + +sing-box supports some rule-set formats from other projects which cannot be fully translated to sing-box, +currently only AdGuard DNS Filter. + +These formats are not directly supported as source formats, +instead you need to convert them to binary rule-set. + +## Convert + +Use `sing-box rule-set convert --type adguard [--output .srs] .txt` to convert to binary rule-set. + +## Performance + +AdGuard keeps all rules in memory and matches them sequentially, +while sing-box chooses high performance and smaller memory usage. +As a trade-off, you cannot know which rule item is matched. + +## Compatibility + +Almost all rules in [AdGuardSDNSFilter](https://github.com/AdguardTeam/AdGuardSDNSFilter) +and rules in rule-sets listed in [adguard-filter-list](https://github.com/ppfeufer/adguard-filter-list) +are supported. + +## Supported formats + +### AdGuard Filter + +#### Basic rule syntax + +| Syntax | Supported | +|--------|------------------| +| `@@` | :material-check: | +| `\|\|` | :material-check: | +| `\|` | :material-check: | +| `^` | :material-check: | +| `*` | :material-check: | + +#### Host syntax + +| Syntax | Example | Supported | +|-------------|--------------------------|--------------------------| +| Scheme | `https://` | :material-alert: Ignored | +| Domain Host | `example.org` | :material-check: | +| IP Host | `1.1.1.1`, `10.0.0.` | :material-close: | +| Regexp | `/regexp/` | :material-check: | +| Port | `example.org:80` | :material-close: | +| Path | `example.org/path/ad.js` | :material-close: | + +#### Modifier syntax + +| Modifier | Supported | +|-----------------------|--------------------------| +| `$important` | :material-check: | +| `$dnsrewrite=0.0.0.0` | :material-alert: Ignored | +| Any other modifiers | :material-close: | + +### Hosts + +Only items with `0.0.0.0` IP addresses will be accepted. + +### Simple + +When all rule lines are valid domains, they are treated as simple line-by-line domain rules which, +like hosts, only match the exact same domain. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index d5218f4d53..63b339fa8b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -90,6 +90,7 @@ nav: - configuration/rule-set/index.md - Source Format: configuration/rule-set/source-format.md - Headless Rule: configuration/rule-set/headless-rule.md + - AdGuard DNS Filer: configuration/rule-set/adguard.md - Experimental: - configuration/experimental/index.md - Cache File: configuration/experimental/cache-file.md diff --git a/option/rule_set.go b/option/rule_set.go index 002cadd46e..8470324b02 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -166,6 +166,9 @@ type DefaultHeadlessRule struct { DomainMatcher *domain.Matcher `json:"-"` SourceIPSet *netipx.IPSet `json:"-"` IPSet *netipx.IPSet `json:"-"` + + AdGuardDomain Listable[string] `json:"-"` + AdGuardDomainMatcher *domain.AdGuardMatcher `json:"-"` } func (r DefaultHeadlessRule) IsValid() bool { diff --git a/route/rule_headless.go b/route/rule_headless.go index 67ac3a1e44..d537df572c 100644 --- a/route/rule_headless.go +++ b/route/rule_headless.go @@ -142,6 +142,15 @@ func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadles rule.allItems = append(rule.allItems, item) } } + if len(options.AdGuardDomain) > 0 { + item := NewAdGuardDomainItem(options.AdGuardDomain) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.AdGuardDomainMatcher != nil { + item := NewRawAdGuardDomainItem(options.AdGuardDomainMatcher) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } diff --git a/route/rule_item_adguard.go b/route/rule_item_adguard.go new file mode 100644 index 0000000000..bdbb3b75fd --- /dev/null +++ b/route/rule_item_adguard.go @@ -0,0 +1,43 @@ +package route + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/domain" +) + +var _ RuleItem = (*AdGuardDomainItem)(nil) + +type AdGuardDomainItem struct { + matcher *domain.AdGuardMatcher +} + +func NewAdGuardDomainItem(ruleLines []string) *AdGuardDomainItem { + return &AdGuardDomainItem{ + domain.NewAdGuardMatcher(ruleLines), + } +} + +func NewRawAdGuardDomainItem(matcher *domain.AdGuardMatcher) *AdGuardDomainItem { + return &AdGuardDomainItem{ + matcher, + } +} + +func (r *AdGuardDomainItem) Match(metadata *adapter.InboundContext) bool { + var domainHost string + if metadata.Domain != "" { + domainHost = metadata.Domain + } else { + domainHost = metadata.Destination.Fqdn + } + if domainHost == "" { + return false + } + return r.matcher.Match(strings.ToLower(domainHost)) +} + +func (r *AdGuardDomainItem) String() string { + return "!adguard_domain_rules=" +} From 15f66ef7cfd80ba7d819e9ddc5646146ae985f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 10 Aug 2024 16:47:44 +0800 Subject: [PATCH 23/31] Write close error to log --- cmd/sing-box/cmd_run.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/sing-box/cmd_run.go b/cmd/sing-box/cmd_run.go index e717c5945d..6850cd19e3 100644 --- a/cmd/sing-box/cmd_run.go +++ b/cmd/sing-box/cmd_run.go @@ -188,9 +188,12 @@ func run() error { cancel() closeCtx, closed := context.WithCancel(context.Background()) go closeMonitor(closeCtx) - instance.Close() + err = instance.Close() closed() if osSignal != syscall.SIGHUP { + if err != nil { + log.Error(E.Cause(err, "sing-box did not closed properly")) + } return nil } break From f6a01949cbedfe1c9b2a142246c655acd6f1c370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 20 Aug 2024 18:56:50 +0800 Subject: [PATCH 24/31] Introduce SSH sniffer --- common/sniff/ssh.go | 26 ++++++++++++++++++++++++++ common/sniff/ssh_test.go | 26 ++++++++++++++++++++++++++ constant/protocol.go | 1 + docs/configuration/route/sniff.md | 8 +++++--- docs/configuration/route/sniff.zh.md | 24 +++++++++++++----------- route/router.go | 3 ++- 6 files changed, 73 insertions(+), 15 deletions(-) create mode 100644 common/sniff/ssh.go create mode 100644 common/sniff/ssh_test.go diff --git a/common/sniff/ssh.go b/common/sniff/ssh.go new file mode 100644 index 0000000000..194d0bda8c --- /dev/null +++ b/common/sniff/ssh.go @@ -0,0 +1,26 @@ +package sniff + +import ( + "bufio" + "context" + "io" + "os" + "strings" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" +) + +func SSH(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { + scanner := bufio.NewScanner(reader) + if !scanner.Scan() { + return os.ErrInvalid + } + fistLine := scanner.Text() + if !strings.HasPrefix(fistLine, "SSH-2.0-") { + return os.ErrInvalid + } + metadata.Protocol = C.ProtocolSSH + metadata.Client = fistLine[8:] + return nil +} diff --git a/common/sniff/ssh_test.go b/common/sniff/ssh_test.go new file mode 100644 index 0000000000..be53098079 --- /dev/null +++ b/common/sniff/ssh_test.go @@ -0,0 +1,26 @@ +package sniff_test + +import ( + "bytes" + "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 TestSniffSSH(t *testing.T) { + t.Parallel() + + pkt, err := hex.DecodeString("5353482d322e302d64726f70626561720d0a000001a40a1492892570d1223aef61b0d647972c8bd30000009f637572766532353531392d7368613235362c637572766532353531392d736861323536406c69627373682e6f72672c6469666669652d68656c6c6d616e2d67726f757031342d7368613235362c6469666669652d68656c6c6d616e2d67726f757031342d736861312c6b6578677565737332406d6174742e7563632e61736e2e61752c6b65782d7374726963742d732d763030406f70656e7373682e636f6d000000207373682d656432353531392c7273612d736861322d3235362c7373682d7273610000003363686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d6374722c6165733235362d6374720000003363686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d6374722c6165733235362d63747200000017686d61632d736861312c686d61632d736861322d32353600000017686d61632d736861312c686d61632d736861322d323536000000046e6f6e65000000046e6f6e65000000000000000000000000002aa6ed090585b7d635b6") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt)) + require.NoError(t, err) + require.Equal(t, C.ProtocolSSH, metadata.Protocol) + require.Equal(t, "dropbear", metadata.Client) +} diff --git a/constant/protocol.go b/constant/protocol.go index 8b1854d419..f867f428f6 100644 --- a/constant/protocol.go +++ b/constant/protocol.go @@ -8,6 +8,7 @@ const ( ProtocolSTUN = "stun" ProtocolBitTorrent = "bittorrent" ProtocolDTLS = "dtls" + ProtocolSSH = "ssh" ) const ( diff --git a/docs/configuration/route/sniff.md b/docs/configuration/route/sniff.md index ab96e75a6f..70e2acc85a 100644 --- a/docs/configuration/route/sniff.md +++ b/docs/configuration/route/sniff.md @@ -7,14 +7,15 @@ icon: material/new-box :material-plus: QUIC client type detect support for QUIC :material-plus: Chromium support for QUIC :material-plus: BitTorrent support - :material-plus: DTLS support + :material-plus: DTLS support + :material-plus: SSH support If enabled in the inbound, the protocol and domain name (if present) of by the connection can be sniffed. #### Supported Protocols | Network | Protocol | Domain Name | Client | -|:-------:|:------------:|:-----------:|:----------------:| +| :-----: | :----------: | :---------: | :--------------: | | TCP | `http` | Host | / | | TCP | `tls` | Server Name | / | | UDP | `quic` | Server Name | QUIC Client Type | @@ -22,9 +23,10 @@ If enabled in the inbound, the protocol and domain name (if present) of by the c | TCP/UDP | `dns` | / | / | | TCP/UDP | `bittorrent` | / | / | | UDP | `dtls` | / | / | +| TCP | `ssh` | / | SSH Client Name | | QUIC Client | Type | -|:------------------------:|:----------:| +| :----------------------: | :--------: | | Chromium/Cronet | `chrimium` | | Safari/Apple Network API | `safari` | | Firefox / uquic firefox | `firefox` | diff --git a/docs/configuration/route/sniff.zh.md b/docs/configuration/route/sniff.zh.md index 1ebc6d38d1..7421fd07e1 100644 --- a/docs/configuration/route/sniff.zh.md +++ b/docs/configuration/route/sniff.zh.md @@ -7,24 +7,26 @@ icon: material/new-box :material-plus: QUIC 的 客户端类型探测支持 :material-plus: QUIC 的 Chromium 支持 :material-plus: BitTorrent 支持 - :material-plus: DTLS 支持 + :material-plus: DTLS 支持 + :material-plus: SSH 支持 如果在入站中启用,则可以嗅探连接的协议和域名(如果存在)。 #### 支持的协议 -| 网络 | 协议 | 域名 | 客户端 | -|:-------:|:------------:|:-----------:|:----------:| -| TCP | `http` | Host | / | -| TCP | `tls` | Server Name | / | +| 网络 | 协议 | 域名 | 客户端 | +| :-----: | :----------: | :---------: | :-------------: | +| TCP | `http` | Host | / | +| TCP | `tls` | Server Name | / | | UDP | `quic` | Server Name | QUIC 客户端类型 | -| UDP | `stun` | / | / | -| TCP/UDP | `dns` | / | / | -| TCP/UDP | `bittorrent` | / | / | -| UDP | `dtls` | / | / | +| UDP | `stun` | / | / | +| TCP/UDP | `dns` | / | / | +| TCP/UDP | `bittorrent` | / | / | +| UDP | `dtls` | / | / | +| TCP | `SSH` | / | SSH 客户端名称 | -| QUIC 客户端 | 类型 | -|:------------------------:|:----------:| +| QUIC 客户端 | 类型 | +| :----------------------: | :--------: | | Chromium/Cronet | `chrimium` | | Safari/Apple Network API | `safari` | | Firefox / uquic firefox | `firefox` | diff --git a/route/router.go b/route/router.go index 4653e0bcf4..742f5bd2ee 100644 --- a/route/router.go +++ b/route/router.go @@ -860,9 +860,10 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad conn, buffer, time.Duration(metadata.InboundOptions.SniffTimeout), - sniff.StreamDomainNameQuery, sniff.TLSClientHello, sniff.HTTPHost, + sniff.StreamDomainNameQuery, + sniff.SSH, sniff.BitTorrent, ) if err == nil { From 7007fec9cc594ba60fb0225dcacdc5dcacf66d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 23 Aug 2024 13:38:27 +0800 Subject: [PATCH 25/31] clash-api: Fix bad redirect --- experimental/clashapi/server.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 1e7804ce44..3428bbb771 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -277,10 +277,11 @@ func authentication(serverSecret string) func(next http.Handler) http.Handler { func hello(redirect bool) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - if redirect { - http.Redirect(w, r, "/ui/", http.StatusTemporaryRedirect) - } else { + contentType := r.Header.Get("Content-Type") + if !redirect || contentType == "application/json" { render.JSON(w, r, render.M{"hello": "clash"}) + } else { + http.Redirect(w, r, "/ui/", http.StatusTemporaryRedirect) } } } From 6dfa5e84a12a9e4e1788886482b242393d3b4de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 23 Aug 2024 13:38:38 +0800 Subject: [PATCH 26/31] clash-api: Add PNA support --- docs/configuration/experimental/clash-api.md | 96 +++++++++++++++---- .../experimental/clash-api.zh.md | 96 +++++++++++++++---- experimental/clashapi/server.go | 15 ++- go.mod | 2 +- go.sum | 4 +- option/experimental.go | 16 ++-- 6 files changed, 178 insertions(+), 51 deletions(-) diff --git a/docs/configuration/experimental/clash-api.md b/docs/configuration/experimental/clash-api.md index e1ca981521..7425143eb3 100644 --- a/docs/configuration/experimental/clash-api.md +++ b/docs/configuration/experimental/clash-api.md @@ -1,3 +1,12 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.10.0" + + :material-plus: [access_control_allow_origin](#access_control_allow_origin) + :material-plus: [access_control_allow_private_network](#access_control_allow_private_network) + !!! quote "Changes in sing-box 1.8.0" :material-delete-alert: [store_mode](#store_mode) @@ -8,24 +17,59 @@ ### Structure -```json -{ - "external_controller": "127.0.0.1:9090", - "external_ui": "", - "external_ui_download_url": "", - "external_ui_download_detour": "", - "secret": "", - "default_mode": "", - - // Deprecated - - "store_mode": false, - "store_selected": false, - "store_fakeip": false, - "cache_file": "", - "cache_id": "" -} -``` +=== "Structure" + + ```json + { + "external_controller": "127.0.0.1:9090", + "external_ui": "", + "external_ui_download_url": "", + "external_ui_download_detour": "", + "secret": "", + "default_mode": "", + "access_control_allow_origin": [], + "access_control_allow_private_network": false, + + // Deprecated + + "store_mode": false, + "store_selected": false, + "store_fakeip": false, + "cache_file": "", + "cache_id": "" + } + ``` + +=== "Example (online)" + + !!! question "Since sing-box 1.10.0" + + ```json + { + "external_controller": "127.0.0.1:9090", + "access_control_allow_origin": [ + "http://127.0.0.1", + "http://yacd.haishan.me" + ], + "access_control_allow_private_network": true + } + ``` + +=== "Example (download)" + + !!! question "Since sing-box 1.10.0" + + ```json + { + "external_controller": "0.0.0.0:9090", + "external_ui": "dashboard" + // external_ui_download_detour: "direct" + } + ``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item ### Fields @@ -63,6 +107,22 @@ Default mode in clash, `Rule` will be used if empty. This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item. +#### access_control_allow_origin + +!!! question "Since sing-box 1.10.0" + +CORS allowed origins, `*` will be used if empty. + +To access the Clash API on a private network from a public website, you must explicitly specify it in `access_control_allow_origin` instead of using `*`. + +#### access_control_allow_private_network + +!!! question "Since sing-box 1.10.0" + +Allow access from private network. + +To access the Clash API on a private network from a public website, `access_control_allow_private_network` must be enabled. + #### store_mode !!! failure "Deprecated in sing-box 1.8.0" diff --git a/docs/configuration/experimental/clash-api.zh.md b/docs/configuration/experimental/clash-api.zh.md index 092769ac93..b3d8aeaf99 100644 --- a/docs/configuration/experimental/clash-api.zh.md +++ b/docs/configuration/experimental/clash-api.zh.md @@ -1,3 +1,12 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.10.0 中的更改" + + :material-plus: [access_control_allow_origin](#access_control_allow_origin) + :material-plus: [access_control_allow_private_network](#access_control_allow_private_network) + !!! quote "sing-box 1.8.0 中的更改" :material-delete-alert: [store_mode](#store_mode) @@ -8,24 +17,59 @@ ### 结构 -```json -{ - "external_controller": "127.0.0.1:9090", - "external_ui": "", - "external_ui_download_url": "", - "external_ui_download_detour": "", - "secret": "", - "default_mode": "", - - // Deprecated - - "store_mode": false, - "store_selected": false, - "store_fakeip": false, - "cache_file": "", - "cache_id": "" -} -``` +=== "结构" + + ```json + { + "external_controller": "127.0.0.1:9090", + "external_ui": "", + "external_ui_download_url": "", + "external_ui_download_detour": "", + "secret": "", + "default_mode": "", + "access_control_allow_origin": [], + "access_control_allow_private_network": false, + + // Deprecated + + "store_mode": false, + "store_selected": false, + "store_fakeip": false, + "cache_file": "", + "cache_id": "" + } + ``` + +=== "示例 (在线)" + + !!! question "自 sing-box 1.10.0 起" + + ```json + { + "external_controller": "127.0.0.1:9090", + "access_control_allow_origin": [ + "http://127.0.0.1", + "http://yacd.haishan.me" + ], + "access_control_allow_private_network": true + } + ``` + +=== "示例 (下载)" + + !!! question "自 sing-box 1.10.0 起" + + ```json + { + "external_controller": "0.0.0.0:9090", + "external_ui": "dashboard" + // external_ui_download_detour: "direct" + } + ``` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签 ### Fields @@ -61,6 +105,22 @@ Clash 中的默认模式,默认使用 `Rule`。 此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。 +#### access_control_allow_origin + +!!! question "自 sing-box 1.10.0 起" + +允许的 CORS 来源,默认使用 `*`。 + +要从公共网站访问私有网络上的 Clash API,必须在 `access_control_allow_origin` 中明确指定它而不是使用 `*`。 + +#### access_control_allow_private_network + +!!! question "自 sing-box 1.10.0 起" + +允许从私有网络访问。 + +要从公共网站访问私有网络上的 Clash API,必须启用 `access_control_allow_private_network`。 + #### store_mode !!! failure "已在 sing-box 1.8.0 废弃" diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 3428bbb771..889d191e0e 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -12,6 +12,7 @@ import ( "syscall" "time" + "github.com/sagernet/cors" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/urltest" C "github.com/sagernet/sing-box/constant" @@ -29,7 +30,6 @@ import ( "github.com/sagernet/ws/wsutil" "github.com/go-chi/chi/v5" - "github.com/go-chi/cors" "github.com/go-chi/render" ) @@ -90,11 +90,16 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" { return nil, E.New("cache_file and related fields in Clash API is deprecated in sing-box 1.8.0, use experimental.cache_file instead.") } + allowedOrigins := options.AccessControlAllowOrigin + if len(allowedOrigins) == 0 { + allowedOrigins = []string{"*"} + } cors := cors.New(cors.Options{ - AllowedOrigins: []string{"*"}, - AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, - AllowedHeaders: []string{"Content-Type", "Authorization"}, - MaxAge: 300, + AllowedOrigins: allowedOrigins, + AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, + AllowedHeaders: []string{"Content-Type", "Authorization"}, + AllowPrivateNetwork: options.AccessControlAllowPrivateNetwork, + MaxAge: 300, }) chiRouter.Use(cors.Handler) chiRouter.Group(func(r chi.Router) { diff --git a/go.mod b/go.mod index d4601fac0e..c259463ce9 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/cloudflare/circl v1.3.7 github.com/cretz/bine v0.2.0 github.com/go-chi/chi/v5 v5.0.12 - github.com/go-chi/cors v1.2.1 github.com/go-chi/render v1.0.3 github.com/gofrs/uuid/v5 v5.2.0 github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 @@ -22,6 +21,7 @@ require ( github.com/oschwald/maxminddb-golang v1.12.0 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 + github.com/sagernet/cors v1.2.1 github.com/sagernet/fswatch v0.1.1 github.com/sagernet/gomobile v0.1.3 github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f diff --git a/go.sum b/go.sum index 38faa8847d..ee79e87592 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,6 @@ github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXb github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= -github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= @@ -102,6 +100,8 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 h1:YbmpqPQEMdlk9oFSKYWRqVuu9qzNiOayIonKmv1gCXY= github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1/go.mod h1:J2yAxTFPDjrDPhuAi9aWFz2L3ox9it4qAluBBbN0H5k= +github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= +github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= github.com/sagernet/gomobile v0.1.3 h1:ohjIb1Ou2+1558PnZour3od69suSuvkdSVOlO1tC4B8= diff --git a/option/experimental.go b/option/experimental.go index 9f6071baee..6ab6638550 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -17,13 +17,15 @@ type CacheFileOptions struct { } type ClashAPIOptions struct { - ExternalController string `json:"external_controller,omitempty"` - ExternalUI string `json:"external_ui,omitempty"` - ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` - ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` - Secret string `json:"secret,omitempty"` - DefaultMode string `json:"default_mode,omitempty"` - ModeList []string `json:"-"` + ExternalController string `json:"external_controller,omitempty"` + ExternalUI string `json:"external_ui,omitempty"` + ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` + ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` + Secret string `json:"secret,omitempty"` + DefaultMode string `json:"default_mode,omitempty"` + ModeList []string `json:"-"` + AccessControlAllowOrigin Listable[string] `json:"access_control_allow_origin,omitempty"` + AccessControlAllowPrivateNetwork bool `json:"access_control_allow_private_network,omitempty"` // Deprecated: migrated to global cache file CacheFile string `json:"cache_file,omitempty"` From 05c4f94982abc10d149335dca4326ca199125b0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 7 Jun 2024 16:08:07 +0800 Subject: [PATCH 27/31] documentation: Bump version --- docs/changelog.md | 218 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 6dbef0204c..a37d241a78 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -8,6 +8,21 @@ icon: material/alert-decagram If your company or organization is willing to help us return to the App Store, please [contact us](mailto:contact@sagernet.org). +#### 1.10.0-beta.5 + +* Add PNA support for [Clash API](/configuration/experimental/clash-api/) +* Fixes and improvements + +#### 1.10.0-beta.3 + +* Add SSH sniffer +* Fixes and improvements + +#### 1.10.0-beta.2 + +* Build with go1.23 +* Fixes and improvements + ### 1.9.4 * Update quic-go to v0.46.0 @@ -23,18 +38,221 @@ icon: material/alert-decagram * Fix UDP connnection leak when sniffing * Fixes and improvements +#### 1.10.0-alpha.29 + +* Update quic-go to v0.46.0 +* Fixes and improvements + +#### 1.10.0-alpha.25 + +* Add AdGuard DNS Filter support **1** + +**1**: + +The new feature allows you to use AdGuard DNS Filter lists in a sing-box without AdGuard Home. + +See [AdGuard DNS Filter](/configuration/rule-set/adguard/). + +#### 1.10.0-alpha.23 + +* Add Chromium support for QUIC sniffer +* Add client type detect support for QUIC sniffer **1** +* Fixes and improvements + +**1**: + +Now the QUIC sniffer can correctly extract the server name from Chromium requests and +can identify common QUIC clients, including +Chromium, Safari, Firefox, quic-go (including uquic disguised as Chrome). + +See [Protocol Sniff](/configuration/route/sniff/) and [Route Rule](/configuration/route/rule/#client). + +#### 1.10.0-alpha.22 + +* Optimize memory usages of rule-sets **1** +* Fixes and improvements + +**1**: + +See [Source Format](/configuration/rule-set/source-format/#version). + +#### 1.10.0-alpha.20 + +* Add DTLS sniffer +* Fixes and improvements + +#### 1.10.0-alpha.19 + +* Add `rule-set decompile` command +* Add IP address support for `rule-set match` command +* Fixes and improvements + +#### 1.10.0-alpha.18 + +* Add new `inline` rule-set type **1** +* Add auto reload support for local rule-set +* Update fsnotify usages **2** +* Fixes and improvements + +**1**: + +The new [rule-set] type inline (which also becomes the default type) +allows you to write headless rules directly without creating a rule-set file. + +[rule-set]: /configuration/rule-set/ + +**2**: + +sing-box now uses fsnotify correctly and will not cancel watching +if the target file is deleted or recreated via rename (e.g. `mv`). + +This affects all path options that support reload, including +`tls.certificate_path`, `tls.key_path`, `tls.ech.key_path` and `rule_set.path`. + +#### 1.10.0-alpha.17 + +* Some chaotic changes **1** +* `rule_set_ipcidr_match_source` rule items are renamed **2** +* Add `rule_set_ip_cidr_accept_empty` DNS address filter rule item **3** +* Update quic-go to v0.45.1 +* Fixes and improvements + +**1**: + +Something may be broken, please actively report problems with this version. + +**2**: + +`rule_set_ipcidr_match_source` route and DNS rule items are renamed to +`rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0. + +**3**: + +See [DNS Rule](/configuration/dns/rule/#rule_set_ip_cidr_accept_empty). + +#### 1.10.0-alpha.16 + +* Add custom options for `auto-route` and `auto-redirect` **1** +* Fixes and improvements + +**1**: + +See [iproute2_table_index](/configuration/inbound/tun/#iproute2_table_index), +[iproute2_rule_index](/configuration/inbound/tun/#iproute2_rule_index), +[auto_redirect_input_mark](/configuration/inbound/tun/#auto_redirect_input_mark) and +[auto_redirect_output_mark](/configuration/inbound/tun/#auto_redirect_output_mark). + +#### 1.10.0-alpha.13 + +* TUN address fields are merged **1** +* Add route address set support for auto-redirect **2** + +**1**: + +See [Migration](/migration/#tun-address-fields-are-merged). + +**2**: + +The new feature will allow you to configure the destination IP CIDR rules +in the specified rule-sets to the firewall automatically. + +Specified or unspecified destinations will bypass the sing-box routes to get better performance +(for example, keep hardware offloading of direct traffics on the router). + +See [route_address_set](/configuration/inbound/tun/#route_address_set) +and [route_exclude_address_set](/configuration/inbound/tun/#route_exclude_address_set). + +#### 1.10.0-alpha.12 + +* Fix auto-redirect not configuring nftables forward chain correctly +* Fixes and improvements + ### 1.9.3 * Fixes and improvements +#### 1.10.0-alpha.10 + +* Fixes and improvements + ### 1.9.2 * Fixes and improvements +#### 1.10.0-alpha.8 + +* Drop support for go1.18 and go1.19 **1** +* Update quic-go to v0.45.0 +* Update Hysteria2 BBR congestion control +* Fixes and improvements + +**1**: + +Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile. + ### 1.9.1 * Fixes and improvements +#### 1.10.0-alpha.7 + +* Fixes and improvements + +#### 1.10.0-alpha.5 + +* Improve auto-redirect **1** + +**1**: + +nftables support and DNS hijacking has been added. + +Tun inbounds with `auto_route` and `auto_redirect` now works as expected on routers **without intervention**. + +#### 1.10.0-alpha.4 + +* Fix auto-redirect **1** +* Improve auto-route on linux **2** + +**1**: + +Tun inbounds with `auto_route` and `auto_redirect` now works as expected on routers. + +**2**: + +Tun inbounds with `auto_route` and `strict_route` now works as expected on routers and servers, +but the usages of [exclude_interface](/configuration/inbound/tun/#exclude_interface) need to be updated. + +#### 1.10.0-alpha.2 + +* Move auto-redirect to Tun **1** +* Fixes and improvements + +**1**: + +Linux support are added. + +See [Tun](/configuration/inbound/tun/#auto_redirect). + +#### 1.10.0-alpha.1 + +* Add tailing comma support in JSON configuration +* Add simple auto-redirect for Android **1** +* Add BitTorrent sniffer **2** + +**1**: + +It allows you to use redirect inbound in the sing-box Android client +and automatically configures IPv4 TCP redirection via su. + +This may alleviate the symptoms of some OCD patients who think that +redirect can effectively save power compared to the system HTTP Proxy. + +See [Redirect](/configuration/inbound/redirect/). + +**2**: + +See [Protocol Sniff](/configuration/route/sniff/). + ### 1.9.0 * Fixes and improvements From 08e72b12da1f0afb48de38bf1afb94a6041d67b1 Mon Sep 17 00:00:00 2001 From: Juvenn Woo Date: Sat, 24 Aug 2024 12:47:08 +0800 Subject: [PATCH 28/31] metric-api: Add prometheus metrics --- Dockerfile | 2 +- Makefile | 4 +- adapter/experimental.go | 18 +++-- box.go | 12 ++++ cmd/internal/build_libbox/main.go | 2 +- docs/configuration/experimental/metric-api.md | 19 +++++ .../experimental/metric-api.zh.md | 19 +++++ docs/installation/build-from-source.md | 1 + docs/installation/build-from-source.zh.md | 1 + experimental/metricapi.go | 24 +++++++ experimental/metrics/server.go | 72 +++++++++++++++++++ experimental/metrics/tracker.go | 52 ++++++++++++++ experimental/v2rayapi/server.go | 2 +- experimental/v2rayapi/stats.go | 50 ++++++------- go.mod | 5 +- go.sum | 4 ++ include/metricapi.go | 5 ++ include/metricapi_stub.go | 17 +++++ option/experimental.go | 10 +++ route/router.go | 15 +++- 20 files changed, 294 insertions(+), 40 deletions(-) create mode 100644 docs/configuration/experimental/metric-api.md create mode 100644 docs/configuration/experimental/metric-api.zh.md create mode 100644 experimental/metricapi.go create mode 100644 experimental/metrics/server.go create mode 100644 experimental/metrics/tracker.go create mode 100644 include/metricapi.go create mode 100644 include/metricapi_stub.go diff --git a/Dockerfile b/Dockerfile index 0b1ac735e2..15bee88b5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN set -ex \ && export COMMIT=$(git rev-parse --short HEAD) \ && export VERSION=$(go run ./cmd/internal/read_tag) \ && go build -v -trimpath -tags \ - "with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \ + "with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api,with_metric_api" \ -o /go/bin/sing-box \ -ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid= -checklinkname=0" \ ./cmd/sing-box diff --git a/Makefile b/Makefile index 78721f1d18..73a65baaa6 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ NAME = sing-box COMMIT = $(shell git rev-parse --short HEAD) -TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls +TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_metric_api,with_quic,with_utls TAGS_GO121 = with_ech TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121) TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server @@ -208,4 +208,4 @@ clean: update: git fetch git reset FETCH_HEAD --hard - git clean -fdx \ No newline at end of file + git clean -fdx diff --git a/adapter/experimental.go b/adapter/experimental.go index 0cab5ed5a8..863fa8b48d 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -118,10 +118,20 @@ func OutboundTag(detour Outbound) string { type V2RayServer interface { Service - StatsService() V2RayStatsService + StatsService() PacketTracking } -type V2RayStatsService interface { - RoutedConnection(inbound string, outbound string, user string, conn net.Conn) net.Conn - RoutedPacketConnection(inbound string, outbound string, user string, conn N.PacketConn) N.PacketConn +type MetricService interface { + Service + PacketTracking +} + +type PacketTracking interface { + WithConnCounters(inbound, outbound, user string) ConnAdapter[net.Conn] + WithPacketConnCounters(inbound, outbound, user string) ConnAdapter[N.PacketConn] } + +// ConnAdapter +// Transform a connection to another connection. The T should be either of +// net.Conn or N.PacketConn. +type ConnAdapter[T any] func(T) T diff --git a/box.go b/box.go index 716b1b093c..4a7295f5af 100644 --- a/box.go +++ b/box.go @@ -61,6 +61,7 @@ func New(options Options) (*Box, error) { var needCacheFile bool var needClashAPI bool var needV2RayAPI bool + var needMetricAPI bool if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil { needCacheFile = true } @@ -70,6 +71,9 @@ func New(options Options) (*Box, error) { if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" { needV2RayAPI = true } + if experimentalOptions.MetricAPI.Enabled() { + needMetricAPI = true + } var defaultLogWriter io.Writer if options.PlatformInterface != nil { defaultLogWriter = io.Discard @@ -183,6 +187,14 @@ func New(options Options) (*Box, error) { router.SetV2RayServer(v2rayServer) preServices2["v2ray api"] = v2rayServer } + if needMetricAPI { + metricServer, err := experimental.NewMetricServer(logFactory.NewLogger("metric-api"), common.PtrValueOrDefault(experimentalOptions.MetricAPI)) + if err != nil { + return nil, E.Cause(err, "create metric api server") + } + router.SetMetricServer(metricServer) + preServices2["metric api"] = metricServer + } return &Box{ router: router, inbounds: inbounds, diff --git a/cmd/internal/build_libbox/main.go b/cmd/internal/build_libbox/main.go index fc9308ff40..ab269904a9 100644 --- a/cmd/internal/build_libbox/main.go +++ b/cmd/internal/build_libbox/main.go @@ -54,7 +54,7 @@ func init() { sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=") debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag) - sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api") + sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api", "with_metric_api") iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack") debugTags = append(debugTags, "debug") } diff --git a/docs/configuration/experimental/metric-api.md b/docs/configuration/experimental/metric-api.md new file mode 100644 index 0000000000..1573cf1035 --- /dev/null +++ b/docs/configuration/experimental/metric-api.md @@ -0,0 +1,19 @@ + +### Structure + +```json +{ + "listen": ":8080", + "path": "/metrics" +} +``` + +### Fields + +#### listen + +Prometheus metrics API listening address, disabled if empty. + +#### path + +Prometheus scrape path, `/metrics` will be used if empty. diff --git a/docs/configuration/experimental/metric-api.zh.md b/docs/configuration/experimental/metric-api.zh.md new file mode 100644 index 0000000000..67e8c31234 --- /dev/null +++ b/docs/configuration/experimental/metric-api.zh.md @@ -0,0 +1,19 @@ + +### Structure + +```json +{ + "listen": ":8080", + "path": "/metrics" +} +``` + +### Fields + +#### listen + +Prometheus 指标监听地址,如果为空则禁用。 + +#### path + +HTTP 路径,如果为空则使用 `/metrics`。本路径可用于 Prometheus exporter 进行抓取。 diff --git a/docs/installation/build-from-source.md b/docs/installation/build-from-source.md index 3ee5b6c55f..5281ee4249 100644 --- a/docs/installation/build-from-source.md +++ b/docs/installation/build-from-source.md @@ -65,6 +65,7 @@ go build -tags "tag_a tag_b" ./cmd/sing-box | `with_reality_server` | :material-check: | Build with reality TLS server support, see [TLS](/configuration/shared/tls/). | | `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls/). | | `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). | +| `with_metric_api` | :material-check: | Build with Prometheus metric API support, see [Experimental](/configuration/experimental#metric-api-fields). | | `with_v2ray_api` | :material-close:️ | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). | | `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). | | `with_embedded_tor` (CGO required) | :material-close:️ | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). | diff --git a/docs/installation/build-from-source.zh.md b/docs/installation/build-from-source.zh.md index e5da3e3379..308b8cadb1 100644 --- a/docs/installation/build-from-source.zh.md +++ b/docs/installation/build-from-source.zh.md @@ -65,6 +65,7 @@ go build -tags "tag_a tag_b" ./cmd/sing-box | `with_reality_server` | :material-check: | Build with reality TLS server support, see [TLS](/configuration/shared/tls/). | | `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls/). | | `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). | +| `with_metric_api` | :material-check: | Build with Prometheus metric API support, see [Experimental](/configuration/experimental#metric-api-fields). | | `with_v2ray_api` | :material-close:️ | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). | | `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). | | `with_embedded_tor` (CGO required) | :material-close:️ | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). | diff --git a/experimental/metricapi.go b/experimental/metricapi.go new file mode 100644 index 0000000000..ceeeaff946 --- /dev/null +++ b/experimental/metricapi.go @@ -0,0 +1,24 @@ +package experimental + +import ( + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" +) + +type MetricServerConstructor = func(logger log.Logger, options option.MetricOptions) (adapter.MetricService, error) + +var metricServerConstructor MetricServerConstructor + +func RegisterMetricServerConstructor(constructor MetricServerConstructor) { + metricServerConstructor = constructor +} + +func NewMetricServer(logger log.Logger, options option.MetricOptions) (adapter.MetricService, error) { + if metricServerConstructor == nil { + return nil, os.ErrInvalid + } + return metricServerConstructor(logger, options) +} diff --git a/experimental/metrics/server.go b/experimental/metrics/server.go new file mode 100644 index 0000000000..36e4083d48 --- /dev/null +++ b/experimental/metrics/server.go @@ -0,0 +1,72 @@ +package metrics + +import ( + "net/http" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/experimental" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + + "github.com/go-chi/chi/v5" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +var _ adapter.MetricService = (*metricServer)(nil) + +func init() { + experimental.RegisterMetricServerConstructor(NewServer) +} + +type metricServer struct { + http *http.Server + + logger log.Logger + opts option.MetricOptions + + packetCountersInbound *prometheus.CounterVec + packetCountersOutbound *prometheus.CounterVec +} + +func NewServer(logger log.Logger, opts option.MetricOptions) (adapter.MetricService, error) { + r := chi.NewRouter() + _server := &http.Server{ + Addr: opts.Listen, + Handler: r, + } + if opts.Path == "" { + opts.Path = "/metrics" + } + r.Get(opts.Path, promhttp.Handler().ServeHTTP) + server := &metricServer{ + http: _server, + logger: logger, + opts: opts, + } + err := server.registerMetrics() + return server, err +} + +func (s *metricServer) Start() error { + if !s.opts.Enabled() { + return nil + } + go func() { + err := s.http.ListenAndServe() + if err != nil { + s.logger.Error("metrics api listen error", err) + } else { + s.logger.Info("metrics api listening at ", s.http.Addr, s.opts.Path) + } + }() + return nil +} + +func (s *metricServer) Close() error { + if !s.opts.Enabled() { + return nil + } + return common.Close(common.PtrOrNil(s.http)) +} diff --git a/experimental/metrics/tracker.go b/experimental/metrics/tracker.go new file mode 100644 index 0000000000..a5614dcff9 --- /dev/null +++ b/experimental/metrics/tracker.go @@ -0,0 +1,52 @@ +package metrics + +import ( + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/bufio" + N "github.com/sagernet/sing/common/network" + + "github.com/prometheus/client_golang/prometheus" +) + +func (s *metricServer) registerMetrics() error { + s.packetCountersInbound = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "inbound_packet_bytes", + Help: "Total bytes of inbound packets", + }, []string{"inbound", "user"}) + + s.packetCountersOutbound = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "outbound_packet_bytes", + Help: "Total bytes of outbound packets", + }, []string{"outbound", "user"}) + var err error + err = prometheus.Register(s.packetCountersInbound) + err = prometheus.Register(s.packetCountersOutbound) + return err +} + +func (s *metricServer) WithConnCounters(inbound, outbound, user string) adapter.ConnAdapter[net.Conn] { + incRead, incWrite := s.getPacketCounters(inbound, outbound, user) + return func(conn net.Conn) net.Conn { + return bufio.NewCounterConn(conn, []N.CountFunc{incRead}, []N.CountFunc{incWrite}) + } +} + +func (s *metricServer) WithPacketConnCounters(inbound, outbound, user string) adapter.ConnAdapter[N.PacketConn] { + incRead, incWrite := s.getPacketCounters(inbound, outbound, user) + return func(conn N.PacketConn) N.PacketConn { + return bufio.NewCounterPacketConn(conn, []N.CountFunc{incRead}, []N.CountFunc{incWrite}) + } +} + +func (s *metricServer) getPacketCounters(inbound, outbound, user string) ( + readCounters N.CountFunc, + writeCounters N.CountFunc, +) { + return func(n int64) { + s.packetCountersInbound.WithLabelValues(inbound, user).Add(float64(n)) + }, func(n int64) { + s.packetCountersOutbound.WithLabelValues(outbound, user).Add(float64(n)) + } +} diff --git a/experimental/v2rayapi/server.go b/experimental/v2rayapi/server.go index 8b4b43855e..2c9dc16161 100644 --- a/experimental/v2rayapi/server.go +++ b/experimental/v2rayapi/server.go @@ -70,6 +70,6 @@ func (s *Server) Close() error { ) } -func (s *Server) StatsService() adapter.V2RayStatsService { +func (s *Server) StatsService() adapter.PacketTracking { return s.statsService } diff --git a/experimental/v2rayapi/stats.go b/experimental/v2rayapi/stats.go index 38b9a301fd..3031b130b4 100644 --- a/experimental/v2rayapi/stats.go +++ b/experimental/v2rayapi/stats.go @@ -22,8 +22,8 @@ func init() { } var ( - _ adapter.V2RayStatsService = (*StatsService)(nil) - _ StatsServiceServer = (*StatsService)(nil) + _ adapter.PacketTracking = (*StatsService)(nil) + _ StatsServiceServer = (*StatsService)(nil) ) type StatsService struct { @@ -60,14 +60,14 @@ func NewStatsService(options option.V2RayStatsServiceOptions) *StatsService { } } -func (s *StatsService) RoutedConnection(inbound string, outbound string, user string, conn net.Conn) net.Conn { +func (s *StatsService) getPacketCounters(inbound, outbound, user string) (incRead N.CountFunc, inWrite N.CountFunc) { var readCounter []*atomic.Int64 var writeCounter []*atomic.Int64 countInbound := inbound != "" && s.inbounds[inbound] countOutbound := outbound != "" && s.outbounds[outbound] countUser := user != "" && s.users[user] if !countInbound && !countOutbound && !countUser { - return conn + return func(n int64) {}, func(n int64) {} } s.access.Lock() if countInbound { @@ -83,33 +83,29 @@ func (s *StatsService) RoutedConnection(inbound string, outbound string, user st writeCounter = append(writeCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>downlink")) } s.access.Unlock() - return bufio.NewInt64CounterConn(conn, readCounter, writeCounter) + return func(n int64) { + for _, c := range readCounter { + c.Add(n) + } + }, func(n int64) { + for _, c := range writeCounter { + c.Add(n) + } + } } -func (s *StatsService) RoutedPacketConnection(inbound string, outbound string, user string, conn N.PacketConn) N.PacketConn { - var readCounter []*atomic.Int64 - var writeCounter []*atomic.Int64 - countInbound := inbound != "" && s.inbounds[inbound] - countOutbound := outbound != "" && s.outbounds[outbound] - countUser := user != "" && s.users[user] - if !countInbound && !countOutbound && !countUser { - return conn - } - s.access.Lock() - if countInbound { - readCounter = append(readCounter, s.loadOrCreateCounter("inbound>>>"+inbound+">>>traffic>>>uplink")) - writeCounter = append(writeCounter, s.loadOrCreateCounter("inbound>>>"+inbound+">>>traffic>>>downlink")) - } - if countOutbound { - readCounter = append(readCounter, s.loadOrCreateCounter("outbound>>>"+outbound+">>>traffic>>>uplink")) - writeCounter = append(writeCounter, s.loadOrCreateCounter("outbound>>>"+outbound+">>>traffic>>>downlink")) +func (s *StatsService) WithConnCounters(inbound, outbound, user string) adapter.ConnAdapter[net.Conn] { + rd, wr := s.getPacketCounters(inbound, outbound, user) + return func(conn net.Conn) net.Conn { + return bufio.NewCounterConn(conn, []N.CountFunc{rd}, []N.CountFunc{wr}) } - if countUser { - readCounter = append(readCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>uplink")) - writeCounter = append(writeCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>downlink")) +} + +func (s *StatsService) WithPacketConnCounters(inbound, outbound, user string) adapter.ConnAdapter[N.PacketConn] { + rd, wr := s.getPacketCounters(inbound, outbound, user) + return func(conn N.PacketConn) N.PacketConn { + return bufio.NewCounterPacketConn(conn, []N.CountFunc{rd}, []N.CountFunc{wr}) } - s.access.Unlock() - return bufio.NewInt64CounterPacketConn(conn, readCounter, writeCounter) } func (s *StatsService) GetStats(ctx context.Context, request *GetStatsRequest) (*GetStatsResponse, error) { diff --git a/go.mod b/go.mod index c259463ce9..e1699fa127 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,7 @@ require ( golang.org/x/sys v0.22.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 google.golang.org/grpc v1.63.2 - google.golang.org/protobuf v1.33.0 + google.golang.org/protobuf v1.34.2 howett.net/plist v1.0.1 ) @@ -72,7 +72,7 @@ require ( github.com/hashicorp/yamux v0.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect - github.com/klauspost/compress v1.17.4 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/libdns/libdns v0.2.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect @@ -81,6 +81,7 @@ require ( github.com/onsi/ginkgo/v2 v2.9.7 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.20.2 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect diff --git a/go.sum b/go.sum index ee79e87592..d9a1e55541 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,7 @@ github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtL github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -91,6 +92,8 @@ github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= +github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= @@ -213,6 +216,7 @@ google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/include/metricapi.go b/include/metricapi.go new file mode 100644 index 0000000000..5325622566 --- /dev/null +++ b/include/metricapi.go @@ -0,0 +1,5 @@ +//go:build with_metric_api + +package include + +import _ "github.com/sagernet/sing-box/experimental/v2rayapi" diff --git a/include/metricapi_stub.go b/include/metricapi_stub.go new file mode 100644 index 0000000000..a70ca61f4d --- /dev/null +++ b/include/metricapi_stub.go @@ -0,0 +1,17 @@ +//go:build !with_metric_api + +package include + +import ( + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/experimental" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func init() { + experimental.RegisterMetricServerConstructor(func(logger log.Logger, options option.MetricOptions) (adapter.MetricService, error) { + return nil, E.New(`metric api is not included in this build, rebuild with -tags with_metric_api`) + }) +} diff --git a/option/experimental.go b/option/experimental.go index 6ab6638550..fda17c5b0a 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -5,6 +5,7 @@ type ExperimentalOptions struct { ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` Debug *DebugOptions `json:"debug,omitempty"` + MetricAPI *MetricOptions `json:"metrics,omitempty"` } type CacheFileOptions struct { @@ -50,3 +51,12 @@ type V2RayStatsServiceOptions struct { Outbounds []string `json:"outbounds,omitempty"` Users []string `json:"users,omitempty"` } + +type MetricOptions struct { + Listen string `json:"listen,omitempty"` + Path string `json:"path,omitempty"` +} + +func (m *MetricOptions) Enabled() bool { + return m != nil && m.Listen != "" +} diff --git a/route/router.go b/route/router.go index 742f5bd2ee..7117725522 100644 --- a/route/router.go +++ b/route/router.go @@ -93,6 +93,7 @@ type Router struct { pauseManager pause.Manager clashServer adapter.ClashServer v2rayServer adapter.V2RayServer + metricServer adapter.MetricService platformInterface platform.Interface needWIFIState bool needPackageManager bool @@ -921,9 +922,12 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad } if r.v2rayServer != nil { if statsService := r.v2rayServer.StatsService(); statsService != nil { - conn = statsService.RoutedConnection(metadata.Inbound, detour.Tag(), metadata.User, conn) + conn = statsService.WithConnCounters(metadata.Inbound, detour.Tag(), metadata.User)(conn) } } + if r.metricServer != nil { + conn = r.metricServer.WithConnCounters(metadata.Inbound, detour.Tag(), metadata.User)(conn) + } return detour.NewConnection(ctx, conn, metadata) } @@ -1097,9 +1101,12 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m } if r.v2rayServer != nil { if statsService := r.v2rayServer.StatsService(); statsService != nil { - conn = statsService.RoutedPacketConnection(metadata.Inbound, detour.Tag(), metadata.User, conn) + conn = statsService.WithPacketConnCounters(metadata.Inbound, detour.Tag(), metadata.User)(conn) } } + if r.metricServer != nil { + conn = r.metricServer.WithPacketConnCounters(metadata.Inbound, detour.Tag(), metadata.User)(conn) + } if metadata.FakeIP { conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination) } @@ -1265,6 +1272,10 @@ func (r *Router) SetV2RayServer(server adapter.V2RayServer) { r.v2rayServer = server } +func (r *Router) SetMetricServer(server adapter.MetricService) { + r.metricServer = server +} + func (r *Router) OnPackagesUpdated(packages int, sharedUsers int) { r.logger.Info("updated packages list: ", packages, " packages, ", sharedUsers, " shared users") } From 3c6173d7f24349292be96c2920c0bedf9de9702b Mon Sep 17 00:00:00 2001 From: Juvenn Woo Date: Sat, 24 Aug 2024 13:07:04 +0800 Subject: [PATCH 29/31] metric-api: Fix include --- go.mod | 8 +++++++- go.sum | 16 ++++++++++++++++ include/metricapi.go | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index e1699fa127..47fd8897d6 100644 --- a/go.mod +++ b/go.mod @@ -59,6 +59,8 @@ require ( require ( github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/brotli v1.0.6 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gaukas/godicttls v0.0.4 // indirect @@ -77,11 +79,15 @@ require ( github.com/libdns/libdns v0.2.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.4.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/onsi/ginkgo/v2 v2.9.7 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.20.2 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect @@ -97,7 +103,7 @@ require ( golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.23.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect - gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index d9a1e55541..8f0bfc8f9a 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,12 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc= github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg= +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.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -53,10 +57,12 @@ github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtL github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -79,6 +85,8 @@ github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30= github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE= github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss= @@ -94,6 +102,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= @@ -216,10 +230,12 @@ google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/include/metricapi.go b/include/metricapi.go index 5325622566..577e567079 100644 --- a/include/metricapi.go +++ b/include/metricapi.go @@ -2,4 +2,4 @@ package include -import _ "github.com/sagernet/sing-box/experimental/v2rayapi" +import _ "github.com/sagernet/sing-box/experimental/metrics" From 7363f49ae34121b54de81d40214046dba4902586 Mon Sep 17 00:00:00 2001 From: Juvenn Woo Date: Sat, 24 Aug 2024 16:44:50 +0800 Subject: [PATCH 30/31] metric-api: http server error handling --- experimental/metrics/server.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/experimental/metrics/server.go b/experimental/metrics/server.go index 36e4083d48..41215672b7 100644 --- a/experimental/metrics/server.go +++ b/experimental/metrics/server.go @@ -1,6 +1,8 @@ package metrics import ( + "errors" + "net" "net/http" "github.com/sagernet/sing-box/adapter" @@ -53,12 +55,15 @@ func (s *metricServer) Start() error { if !s.opts.Enabled() { return nil } + listener, err := net.Listen("tcp", s.opts.Listen) + if err != nil { + return err + } + s.logger.Info("metrics api listening at ", s.http.Addr, s.opts.Path) go func() { - err := s.http.ListenAndServe() - if err != nil { - s.logger.Error("metrics api listen error", err) - } else { - s.logger.Info("metrics api listening at ", s.http.Addr, s.opts.Path) + err := s.http.Serve(listener) + if err != nil && !errors.Is(err, http.ErrServerClosed) { + s.logger.Error("metrics api serve error: ", err) } }() return nil From d98115dd9adcda306395f61a31bda62e83f672f8 Mon Sep 17 00:00:00 2001 From: Juvenn Woo Date: Sat, 24 Aug 2024 16:55:44 +0800 Subject: [PATCH 31/31] metric-api: Add config options doc --- docs/configuration/experimental/index.md | 5 +++-- docs/configuration/experimental/index.zh.md | 12 +++++++----- docs/configuration/experimental/metric-api.md | 6 ++++-- docs/configuration/experimental/metric-api.zh.md | 6 ++++-- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/configuration/experimental/index.md b/docs/configuration/experimental/index.md index a1a515cf85..e4ce8a47a3 100644 --- a/docs/configuration/experimental/index.md +++ b/docs/configuration/experimental/index.md @@ -2,7 +2,7 @@ !!! quote "Changes in sing-box 1.8.0" - :material-plus: [cache_file](#cache_file) + :material-plus: [cache_file](#cache_file) :material-alert-decagram: [clash_api](#clash_api) ### Structure @@ -23,4 +23,5 @@ |--------------|----------------------------| | `cache_file` | [Cache File](./cache-file/) | | `clash_api` | [Clash API](./clash-api/) | -| `v2ray_api` | [V2Ray API](./v2ray-api/) | \ No newline at end of file +| `metric_api` | [Metric API](./metric-api/) | +| `v2ray_api` | [V2Ray API](./v2ray-api/) | diff --git a/docs/configuration/experimental/index.zh.md b/docs/configuration/experimental/index.zh.md index 01246c44ef..908a9541ae 100644 --- a/docs/configuration/experimental/index.zh.md +++ b/docs/configuration/experimental/index.zh.md @@ -2,7 +2,7 @@ !!! quote "sing-box 1.8.0 中的更改" - :material-plus: [cache_file](#cache_file) + :material-plus: [cache_file](#cache_file) :material-alert-decagram: [clash_api](#clash_api) ### 结构 @@ -12,6 +12,7 @@ "experimental": { "cache_file": {}, "clash_api": {}, + "metrics": {}, "v2ray_api": {} } } @@ -19,8 +20,9 @@ ### 字段 -| 键 | 格式 | -|--------------|--------------------------| +| 键 | 格式 | +|--------------|------------------------------| | `cache_file` | [缓存文件](./cache-file/) | -| `clash_api` | [Clash API](./clash-api/) | -| `v2ray_api` | [V2Ray API](./v2ray-api/) | \ No newline at end of file +| `clash_api` | [Clash API](./clash-api/) | +| `metric_api` | [Metric API](./metric-api/) | +| `v2ray_api` | [V2Ray API](./v2ray-api/) | diff --git a/docs/configuration/experimental/metric-api.md b/docs/configuration/experimental/metric-api.md index 1573cf1035..f8d9c131da 100644 --- a/docs/configuration/experimental/metric-api.md +++ b/docs/configuration/experimental/metric-api.md @@ -3,8 +3,10 @@ ```json { - "listen": ":8080", - "path": "/metrics" + "metrics": { + "listen": ":8080", + "path": "/metrics" + } } ``` diff --git a/docs/configuration/experimental/metric-api.zh.md b/docs/configuration/experimental/metric-api.zh.md index 67e8c31234..6ff2cf612a 100644 --- a/docs/configuration/experimental/metric-api.zh.md +++ b/docs/configuration/experimental/metric-api.zh.md @@ -3,8 +3,10 @@ ```json { - "listen": ":8080", - "path": "/metrics" + "metrics": { + "listen": ":8080", + "path": "/metrics" + } } ```