diff --git a/VERSION b/VERSION index cfcde50a..7f397268 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.14.0-rr-edition-2.7.0-r1 +1.14.1-rr-edition-2.7.2-r1 diff --git a/build-apk.sh b/build-apk.sh index dc8f4d10..40c4ada6 100755 --- a/build-apk.sh +++ b/build-apk.sh @@ -8,6 +8,8 @@ GO_CACHE_DIR="${GO_CACHE_DIR:-/tmp/sing-box-rr-go-build}" GO_MODCACHE_DIR="${GO_MODCACHE_DIR:-/tmp/sing-box-rr-go-mod}" GO_PROXY_VALUE="${GOPROXY:-https://proxy.golang.org|direct}" LD_LLD_BIN="${LD_LLD_BIN:-}" +UPX_BIN="${UPX_BIN:-upx}" +ENABLE_UPX="${ENABLE_UPX:-0}" SDK_ARM64="/home/fresa/sdk/openwrt-sdk-25.12.5-mediatek-filogic_gcc-14.3.0_musl.Linux-x86_64" SDK_X86="/home/fresa/sdk/openwrt-sdk-25.12.5-x86-64_gcc-14.3.0_musl.Linux-x86_64" PACKAGES_DIR="$SCRIPT_DIR/packages/25.12" @@ -268,6 +270,9 @@ go_toolchain_value() { require_tools() { [ -x "$GO_BIN" ] || { echo "go not found at $GO_BIN" | tee -a "$LOG_FILE"; exit 1; } + if [ "$ENABLE_UPX" = 1 ]; then + command -v "$UPX_BIN" >/dev/null || { echo "upx not found: $UPX_BIN" | tee -a "$LOG_FILE"; exit 1; } + fi } reset_go_cache_if_needed() { @@ -321,6 +326,12 @@ build_binary() { -o "$output" \ ./cmd/sing-box ) + + if [ "$ENABLE_UPX" = 1 ]; then + # Experimental UPX compression for evaluating flash savings on routers. + echo "=== Experimental UPX compression (default) ===" | tee -a "$LOG_FILE" + run "$UPX_BIN" "$output" + fi } stage_common_tree() { diff --git a/build-ipk.sh b/build-ipk.sh index 3f19b491..8c970852 100755 --- a/build-ipk.sh +++ b/build-ipk.sh @@ -8,6 +8,8 @@ GO_CACHE_DIR="${GO_CACHE_DIR:-/tmp/sing-box-rr-go-build}" GO_MODCACHE_DIR="${GO_MODCACHE_DIR:-/tmp/sing-box-rr-go-mod}" GO_PROXY_VALUE="${GOPROXY:-https://proxy.golang.org|direct}" LD_LLD_BIN="${LD_LLD_BIN:-}" +UPX_BIN="${UPX_BIN:-upx}" +ENABLE_UPX="${ENABLE_UPX:-0}" SDK_ARM64="/home/fresa/sdk/openwrt-sdk-24.10.8-mediatek-filogic_gcc-13.3.0_musl.Linux-x86_64" PACKAGES_DIR="$SCRIPT_DIR/packages/24.10" PKG_NAME="sing-box-rr" @@ -231,6 +233,9 @@ go_toolchain_value() { require_tools() { [ -x "$GO_BIN" ] || { echo "go not found at $GO_BIN" | tee -a "$LOG_FILE"; exit 1; } command -v fakeroot >/dev/null || { echo "fakeroot not found in PATH" | tee -a "$LOG_FILE"; exit 1; } + if [ "$ENABLE_UPX" = 1 ]; then + command -v "$UPX_BIN" >/dev/null || { echo "upx not found: $UPX_BIN" | tee -a "$LOG_FILE"; exit 1; } + fi } reset_go_cache_if_needed() { @@ -284,6 +289,12 @@ build_binary() { -o "$output" \ ./cmd/sing-box ) + + if [ "$ENABLE_UPX" = 1 ]; then + # Experimental UPX compression for evaluating flash savings on routers. + echo "=== Experimental UPX compression (default) ===" | tee -a "$LOG_FILE" + run "$UPX_BIN" "$output" + fi } stage_common_tree() { diff --git a/packages/24.10/arm64/sing-box-rr_1.14.0-rr-edition-2.7.0-r1_aarch64_cortex-a53.ipk b/packages/24.10/arm64/sing-box-rr_1.14.1-rr-edition-2.7.2-r1_aarch64_cortex-a53.ipk similarity index 60% rename from packages/24.10/arm64/sing-box-rr_1.14.0-rr-edition-2.7.0-r1_aarch64_cortex-a53.ipk rename to packages/24.10/arm64/sing-box-rr_1.14.1-rr-edition-2.7.2-r1_aarch64_cortex-a53.ipk index dd99c181..4e0cd220 100644 Binary files a/packages/24.10/arm64/sing-box-rr_1.14.0-rr-edition-2.7.0-r1_aarch64_cortex-a53.ipk and b/packages/24.10/arm64/sing-box-rr_1.14.1-rr-edition-2.7.2-r1_aarch64_cortex-a53.ipk differ diff --git a/packages/25.12/arm64/sing-box-rr-1.14.0.2.7.0-r1.apk b/packages/25.12/arm64/sing-box-rr-1.14.1.2.7.2-r1.apk similarity index 60% rename from packages/25.12/arm64/sing-box-rr-1.14.0.2.7.0-r1.apk rename to packages/25.12/arm64/sing-box-rr-1.14.1.2.7.2-r1.apk index 582c057b..c8fbe401 100644 Binary files a/packages/25.12/arm64/sing-box-rr-1.14.0.2.7.0-r1.apk and b/packages/25.12/arm64/sing-box-rr-1.14.1.2.7.2-r1.apk differ diff --git a/packages/25.12/x86_64/sing-box-rr-1.14.0.2.7.0-r1.apk b/packages/25.12/x86_64/sing-box-rr-1.14.1.2.7.2-r1.apk similarity index 62% rename from packages/25.12/x86_64/sing-box-rr-1.14.0.2.7.0-r1.apk rename to packages/25.12/x86_64/sing-box-rr-1.14.1.2.7.2-r1.apk index b7130e51..3e3f435d 100644 Binary files a/packages/25.12/x86_64/sing-box-rr-1.14.0.2.7.0-r1.apk and b/packages/25.12/x86_64/sing-box-rr-1.14.1.2.7.2-r1.apk differ diff --git a/sing-box-rr/adapter/router.go b/sing-box-rr/adapter/router.go index 1de092b1..be626741 100644 --- a/sing-box-rr/adapter/router.go +++ b/sing-box-rr/adapter/router.go @@ -93,7 +93,11 @@ func JudgeFlow(router Router, inbound string, inboundType string, network uint8, case PreMatchDrop: return tun.FlowVerdict{Action: tun.ActionDrop} case PreMatchBypass: - return tun.FlowVerdict{Action: tun.ActionBypass} + port, isPort := result.Outbound.(tun.Port) + if !isPort { + return tun.FlowVerdict{Action: tun.ActionBypass} + } + return tun.FlowVerdict{Action: tun.ActionBypass, Port: port, UDPTimeout: result.UDPTimeout, NewTracker: result.NewTracker} case PreMatchHijackDNS: return tun.FlowVerdict{Action: tun.ActionHijackDNS} default: diff --git a/sing-box-rr/transport/v2rayhttp/force_close.go b/sing-box-rr/common/force_close/force_close.go similarity index 77% rename from sing-box-rr/transport/v2rayhttp/force_close.go rename to sing-box-rr/common/force_close/force_close.go index ef89990c..6f187c1e 100644 --- a/sing-box-rr/transport/v2rayhttp/force_close.go +++ b/sing-box-rr/common/force_close/force_close.go @@ -1,29 +1,33 @@ -package v2rayhttp +package force_close import ( "net/http" "reflect" "unsafe" + "github.com/sagernet/quic-go/http3" E "github.com/sagernet/sing/common/exceptions" "golang.org/x/net/http2" ) -type efaceWords struct { - typ unsafe.Pointer - data unsafe.Pointer -} - func ResetTransport(rawTransport http.RoundTripper) http.RoundTripper { switch transport := rawTransport.(type) { case *http.Transport: transport.CloseIdleConnections() return transport.Clone() case *http2.Transport: - closeHTTP2Connections(transport) + CloseHTTP2Connections(transport) + return transport + case *http3.Transport: + transport.Close() return transport default: panic(E.New("unknown transport type: ", reflect.TypeOf(transport))) } } + +type efaceWords struct { + typ unsafe.Pointer + data unsafe.Pointer +} diff --git a/sing-box-rr/transport/v2rayhttp/force_close_go127.go b/sing-box-rr/common/force_close/force_close_go127.go similarity index 95% rename from sing-box-rr/transport/v2rayhttp/force_close_go127.go rename to sing-box-rr/common/force_close/force_close_go127.go index 76ffc70e..0bda843d 100644 --- a/sing-box-rr/transport/v2rayhttp/force_close_go127.go +++ b/sing-box-rr/common/force_close/force_close_go127.go @@ -1,6 +1,6 @@ //go:build go1.27 && badlinkname -package v2rayhttp +package force_close import ( "net/http" @@ -16,20 +16,7 @@ import ( // -checklinkname does not inspect. This declaration must precede the linkname declarations. var _ *http.Transport -// net/http/internal/http2.Transport -type internalTransport struct { - t1 [2]uintptr // TransportConfig - connPool *clientConnPool -} - -// net/http/internal/http2.clientConnPool -type clientConnPool struct { - t *internalTransport - mu sync.Mutex - conns map[string][]unsafe.Pointer // key is host:port, value is []*ClientConn -} - -func closeHTTP2Connections(transport *http2.Transport) { +func CloseHTTP2Connections(transport *http2.Transport) { h2Transport := transportFromH1Transport(transportInit(transport)) t := (*internalTransport)((*efaceWords)(unsafe.Pointer(&h2Transport)).data) if t == nil { @@ -45,6 +32,19 @@ func closeHTTP2Connections(transport *http2.Transport) { } } +// net/http/internal/http2.Transport +type internalTransport struct { + t1 [2]uintptr // TransportConfig + connPool *clientConnPool +} + +// net/http/internal/http2.clientConnPool +type clientConnPool struct { + t *internalTransport + mu sync.Mutex + conns map[string][]unsafe.Pointer // key is host:port, value is []*ClientConn +} + //go:linkname transportInit golang.org/x/net/http2.(*Transport).init func transportInit(t *http2.Transport) *http.Transport diff --git a/sing-box-rr/transport/v2rayhttp/force_close_go127_stub.go b/sing-box-rr/common/force_close/force_close_go127_stub.go similarity index 57% rename from sing-box-rr/transport/v2rayhttp/force_close_go127_stub.go rename to sing-box-rr/common/force_close/force_close_go127_stub.go index dc675636..1faf9715 100644 --- a/sing-box-rr/transport/v2rayhttp/force_close_go127_stub.go +++ b/sing-box-rr/common/force_close/force_close_go127_stub.go @@ -1,9 +1,9 @@ //go:build go1.27 && !badlinkname -package v2rayhttp +package force_close import "golang.org/x/net/http2" -func closeHTTP2Connections(transport *http2.Transport) { +func CloseHTTP2Connections(transport *http2.Transport) { transport.CloseIdleConnections() } diff --git a/sing-box-rr/transport/v2rayhttp/force_close_legacy.go b/sing-box-rr/common/force_close/force_close_legacy.go similarity index 88% rename from sing-box-rr/transport/v2rayhttp/force_close_legacy.go rename to sing-box-rr/common/force_close/force_close_legacy.go index d8807d7e..59cab39c 100644 --- a/sing-box-rr/transport/v2rayhttp/force_close_legacy.go +++ b/sing-box-rr/common/force_close/force_close_legacy.go @@ -1,6 +1,6 @@ //go:build !go1.27 -package v2rayhttp +package force_close import ( "sync" @@ -9,13 +9,7 @@ import ( "golang.org/x/net/http2" ) -type clientConnPool struct { - t *http2.Transport - mu sync.Mutex - conns map[string][]*http2.ClientConn // key is host:port -} - -func closeHTTP2Connections(transport *http2.Transport) { +func CloseHTTP2Connections(transport *http2.Transport) { connPool := transportConnPool(transport) p := (*clientConnPool)((*efaceWords)(unsafe.Pointer(&connPool)).data) p.mu.Lock() @@ -27,5 +21,11 @@ func closeHTTP2Connections(transport *http2.Transport) { } } +type clientConnPool struct { + t *http2.Transport + mu sync.Mutex + conns map[string][]*http2.ClientConn // key is host:port +} + //go:linkname transportConnPool golang.org/x/net/http2.(*Transport).connPool func transportConnPool(t *http2.Transport) http2.ClientConnPool diff --git a/sing-box-rr/common/process/searcher_linux.go b/sing-box-rr/common/process/searcher_linux.go index d226c0e4..570e23d3 100644 --- a/sing-box-rr/common/process/searcher_linux.go +++ b/sing-box-rr/common/process/searcher_linux.go @@ -22,7 +22,11 @@ import ( "github.com/sagernet/sing/contrab/maphash" ) -const pathProc = "/proc" +const ( + pathProc = "/proc" + + processPathsAllUsers = ^uint32(0) +) var _ Searcher = (*linuxSearcher)(nil) @@ -101,6 +105,8 @@ func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, sou } func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) { + source = netip.AddrPortFrom(source.Addr().Unmap(), source.Port()) + destination = netip.AddrPortFrom(destination.Addr().Unmap(), destination.Port()) family, protocol, err := socketDiagSettings(network, source) if err != nil { return 0, 0, err @@ -118,28 +124,33 @@ func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.Addr return 0, 0, err } } - return querySocketDiagOnce(family, protocol, source) + return dumpSocketDiag(family, protocol, source, destination) } +// The socket keeps the uid it was created with, while /proc reflects the +// current uid of the process, so a socket created before a privilege drop +// only appears under a scan of all users. func (s *linuxSearcher) findProcessPath(targetInode, uid uint32) (string, error) { - if cached, ok := s.processPathCache.Get(uid); ok { - if processPath, found := cached.entries[targetInode]; found { + for _, scanUID := range []uint32{uid, processPathsAllUsers} { + if cached, ok := s.processPathCache.Get(scanUID); ok { + if processPath, found := cached.entries[targetInode]; found { + return processPath, nil + } + } + processPaths, err := buildProcessPaths(scanUID) + if err != nil { + return "", err + } + s.processPathCache.Add(scanUID, &uidProcessPaths{entries: processPaths}) + processPath, found := processPaths[targetInode] + if found { return processPath, nil } } - processPaths, err := buildProcessPathsByUID(uid) - if err != nil { - return "", err - } - s.processPathCache.Add(uid, &uidProcessPaths{entries: processPaths}) - processPath, found := processPaths[targetInode] - if !found { - return "", E.New("process of uid(", uid, "), inode(", targetInode, ") not found") - } - return processPath, nil + return "", E.New("process of uid(", uid, "), inode(", targetInode, ") not found") } -func buildProcessPathsByUID(uid uint32) (map[uint32]string, error) { +func buildProcessPaths(uid uint32) (map[uint32]string, error) { files, err := os.ReadDir(pathProc) if err != nil { return nil, err @@ -157,7 +168,7 @@ func buildProcessPathsByUID(uid uint32) (map[uint32]string, error) { } return nil, err } - if info.Sys().(*syscall.Stat_t).Uid != uid { + if uid != processPathsAllUsers && info.Sys().(*syscall.Stat_t).Uid != uid { continue } processPath := filepath.Join(pathProc, file.Name()) diff --git a/sing-box-rr/common/process/socket_diag_linux.go b/sing-box-rr/common/process/socket_diag_linux.go index 0adb3d13..b4769b85 100644 --- a/sing-box-rr/common/process/socket_diag_linux.go +++ b/sing-box-rr/common/process/socket_diag_linux.go @@ -9,6 +9,7 @@ import ( "os" "sync" "syscall" + "time" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" @@ -19,6 +20,7 @@ const ( sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + sizeOfSocketDiagRequestData socketDiagResponseMinSize = 72 socketDiagByFamily = 20 + socketDiagTimeout = 100 * time.Millisecond ) type socketDiagConn struct { @@ -28,6 +30,13 @@ type socketDiagConn struct { fd int } +type socketDiagEntry struct { + source netip.AddrPort + destination netip.AddrPort + uid uint32 + inode uint32 +} + func socketDiagConnIndex(family, protocol uint8) int { index := 0 if protocol == syscall.IPPROTO_UDP { @@ -83,15 +92,6 @@ func (c *socketDiagConn) query(source netip.AddrPort, destination netip.AddrPort return 0, 0, err } -func querySocketDiagOnce(family, protocol uint8, source netip.AddrPort) (inode, uid uint32, err error) { - fd, err := openSocketDiag() - if err != nil { - return 0, 0, E.Cause(err, "dial netlink") - } - defer syscall.Close(fd) - return querySocketDiag(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true)) -} - func (c *socketDiagConn) ensureOpenLocked() error { if c.fd != -1 { return nil @@ -109,12 +109,12 @@ func openSocketDiag() (int, error) { if err != nil { return -1, err } - timeout := &syscall.Timeval{Usec: 100} - if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, timeout); err != nil { + timeout := syscall.NsecToTimeval(socketDiagTimeout.Nanoseconds()) + if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &timeout); err != nil { syscall.Close(fd) return -1, err } - if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, timeout); err != nil { + if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &timeout); err != nil { syscall.Close(fd) return -1, err } @@ -196,36 +196,132 @@ func querySocketDiag(fd int, request []byte) (inode, uid uint32, err error) { if err != nil { return 0, 0, E.Cause(err, "parse netlink message") } - return unpackSocketDiagMessages(messages) -} - -func unpackSocketDiagMessages(messages []syscall.NetlinkMessage) (inode, uid uint32, err error) { for _, message := range messages { switch message.Header.Type { - case syscall.NLMSG_DONE: - continue case syscall.NLMSG_ERROR: err = unpackSocketDiagError(&message) if err != nil { return 0, 0, err } case socketDiagByFamily: - inode, uid = unpackSocketDiagResponse(&message) - if inode != 0 || uid != 0 { - return inode, uid, nil + entry, valid := unpackSocketDiagEntry(&message) + if valid && (entry.inode != 0 || entry.uid != 0) { + return entry.inode, entry.uid, nil } } } return 0, 0, ErrNotFound } -func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) { - if len(msg.Data) < socketDiagResponseMinSize { - return 0, 0 +// The dump only filters by port (inet_diag_dump_icsk, udp_dump), so every socket +// in the namespace with the requested local port is returned regardless of address. +// A dual-stack socket carrying a v4-mapped address is only listed under AF_INET6. +func dumpSocketDiag(family, protocol uint8, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) { + families := []uint8{family} + if family == syscall.AF_INET { + families = append(families, syscall.AF_INET6) } - uid = binary.NativeEndian.Uint32(msg.Data[64:68]) - inode = binary.NativeEndian.Uint32(msg.Data[68:72]) - return inode, uid + for _, dumpFamily := range families { + inode, uid, err = dumpSocketDiagFamily(dumpFamily, protocol, source, destination) + if err == nil || !errors.Is(err, ErrNotFound) { + return inode, uid, err + } + } + return 0, 0, ErrNotFound +} + +func dumpSocketDiagFamily(family, protocol uint8, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) { + fd, err := openSocketDiag() + if err != nil { + return 0, 0, E.Cause(err, "dial netlink") + } + defer syscall.Close(fd) + _, err = syscall.Write(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true)) + if err != nil { + return 0, 0, E.Cause(err, "write netlink request") + } + var ( + localMatch socketDiagEntry + hasLocalMatch bool + wildcardMatch socketDiagEntry + hasWildcardMatch bool + buffer = make([]byte, 64<<10) + n int + messages []syscall.NetlinkMessage + ) + for { + n, err = syscall.Read(fd, buffer) + if err != nil { + return 0, 0, E.Cause(err, "read netlink response") + } + messages, err = syscall.ParseNetlinkMessage(buffer[:n]) + if err != nil { + return 0, 0, E.Cause(err, "parse netlink message") + } + if len(messages) == 0 { + return 0, 0, E.New("empty netlink response") + } + for _, message := range messages { + switch message.Header.Type { + case syscall.NLMSG_DONE: + if hasLocalMatch { + return localMatch.inode, localMatch.uid, nil + } + if hasWildcardMatch { + return wildcardMatch.inode, wildcardMatch.uid, nil + } + return 0, 0, ErrNotFound + case syscall.NLMSG_ERROR: + err = unpackSocketDiagError(&message) + if err != nil { + return 0, 0, err + } + case socketDiagByFamily: + entry, valid := unpackSocketDiagEntry(&message) + if !valid || (entry.inode == 0 && entry.uid == 0) || entry.source.Port() != source.Port() { + continue + } + if entry.source.Addr() == source.Addr() && (!destination.IsValid() || entry.destination == destination) { + return entry.inode, entry.uid, nil + } + if protocol != syscall.IPPROTO_UDP { + continue + } + if !hasLocalMatch && entry.source.Addr() == source.Addr() { + hasLocalMatch = true + localMatch = entry + } + if !hasWildcardMatch && entry.source.Addr().IsUnspecified() { + hasWildcardMatch = true + wildcardMatch = entry + } + } + } + } +} + +func unpackSocketDiagEntry(msg *syscall.NetlinkMessage) (socketDiagEntry, bool) { + if len(msg.Data) < socketDiagResponseMinSize { + return socketDiagEntry{}, false + } + data := msg.Data + var sourceAddr, destinationAddr netip.Addr + switch data[0] { + case syscall.AF_INET: + sourceAddr = netip.AddrFrom4([4]byte(data[8:12])) + destinationAddr = netip.AddrFrom4([4]byte(data[24:28])) + case syscall.AF_INET6: + sourceAddr = netip.AddrFrom16([16]byte(data[8:24])).Unmap() + destinationAddr = netip.AddrFrom16([16]byte(data[24:40])).Unmap() + default: + return socketDiagEntry{}, false + } + return socketDiagEntry{ + source: netip.AddrPortFrom(sourceAddr, binary.BigEndian.Uint16(data[4:6])), + destination: netip.AddrPortFrom(destinationAddr, binary.BigEndian.Uint16(data[6:8])), + uid: binary.NativeEndian.Uint32(data[64:68]), + inode: binary.NativeEndian.Uint32(data[68:72]), + }, true } func unpackSocketDiagError(msg *syscall.NetlinkMessage) error { diff --git a/sing-box-rr/common/process/socket_diag_linux_test.go b/sing-box-rr/common/process/socket_diag_linux_test.go new file mode 100644 index 00000000..1aec6f20 --- /dev/null +++ b/sing-box-rr/common/process/socket_diag_linux_test.go @@ -0,0 +1,108 @@ +//go:build linux + +package process + +import ( + "context" + "net" + "net/netip" + "os" + "syscall" + "testing" + + "github.com/sagernet/sing-box/log" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "github.com/stretchr/testify/require" +) + +func socketInode(t *testing.T, conn syscall.Conn) uint32 { + rawConn, err := conn.SyscallConn() + require.NoError(t, err) + var inode uint32 + err = rawConn.Control(func(fd uintptr) { + var stat syscall.Stat_t + require.NoError(t, syscall.Fstat(int(fd), &stat)) + inode = uint32(stat.Ino) + }) + require.NoError(t, err) + return inode +} + +func TestDumpSocketDiagTCP(t *testing.T) { + t.Parallel() + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer listener.Close() + first, err := (&net.Dialer{LocalAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2)}}).Dial("tcp", listener.Addr().String()) + require.NoError(t, err) + defer first.Close() + second, err := (&net.Dialer{LocalAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 3), Port: int(M.AddrPortFromNet(first.LocalAddr()).Port())}}).Dial("tcp", listener.Addr().String()) + require.NoError(t, err) + defer second.Close() + destination := M.AddrPortFromNet(listener.Addr()) + for _, conn := range []net.Conn{second, first} { + inode, uid, err := dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_TCP, M.AddrPortFromNet(conn.LocalAddr()), destination) + require.NoError(t, err) + require.Equal(t, socketInode(t, conn.(syscall.Conn)), inode) + require.Equal(t, uint32(os.Getuid()), uid) + inode, _, err = dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_TCP, M.AddrPortFromNet(conn.LocalAddr()), netip.AddrPort{}) + require.NoError(t, err) + require.Equal(t, socketInode(t, conn.(syscall.Conn)), inode) + } + _, _, err = dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_TCP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 4}), M.AddrPortFromNet(first.LocalAddr()).Port()), destination) + require.ErrorIs(t, err, ErrNotFound) +} + +func TestDumpSocketDiagUDP(t *testing.T) { + t.Parallel() + first, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 2)}) + require.NoError(t, err) + defer first.Close() + port := M.AddrPortFromNet(first.LocalAddr()).Port() + second, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 3), Port: int(port)}) + require.NoError(t, err) + defer second.Close() + for _, conn := range []*net.UDPConn{second, first} { + inode, _, err := dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_UDP, M.AddrPortFromNet(conn.LocalAddr()), netip.AddrPort{}) + require.NoError(t, err) + require.Equal(t, socketInode(t, conn), inode) + } + _, _, err = dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_UDP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 4}), port), netip.AddrPort{}) + require.ErrorIs(t, err, ErrNotFound) + + wildcard, err := net.ListenUDP("udp4", &net.UDPAddr{}) + require.NoError(t, err) + defer wildcard.Close() + inode, _, err := dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_UDP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 4}), M.AddrPortFromNet(wildcard.LocalAddr()).Port()), netip.AddrPort{}) + require.NoError(t, err) + require.Equal(t, socketInode(t, wildcard), inode) +} + +func TestLinuxSearcherFindProcessInfo(t *testing.T) { + t.Parallel() + searcher, err := NewSearcher(Config{Logger: log.NewNOPFactory().NewLogger("test")}) + require.NoError(t, err) + defer searcher.Close() + executable, err := os.Executable() + require.NoError(t, err) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer listener.Close() + tcpConn, err := net.Dial("tcp", listener.Addr().String()) + require.NoError(t, err) + defer tcpConn.Close() + info, err := searcher.FindProcessInfo(context.Background(), N.NetworkTCP, M.AddrPortFromNet(tcpConn.LocalAddr()), M.AddrPortFromNet(listener.Addr())) + require.NoError(t, err) + require.Equal(t, executable, info.ProcessPath) + require.Equal(t, int32(os.Getuid()), info.UserId) + + udpConn, err := net.ListenUDP("udp4", &net.UDPAddr{}) + require.NoError(t, err) + defer udpConn.Close() + info, err = searcher.FindProcessInfo(context.Background(), N.NetworkUDP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), M.AddrPortFromNet(udpConn.LocalAddr()).Port()), netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), 53)) + require.NoError(t, err) + require.Equal(t, executable, info.ProcessPath) +} diff --git a/sing-box-rr/common/tls/reality_client.go b/sing-box-rr/common/tls/reality_client.go index 6753e1db..98d1c823 100644 --- a/sing-box-rr/common/tls/reality_client.go +++ b/sing-box-rr/common/tls/reality_client.go @@ -45,10 +45,11 @@ import ( var _ ConfigCompat = (*RealityClientConfig)(nil) type RealityClientConfig struct { - ctx context.Context - uClient *UTLSClientConfig - publicKey []byte - shortID [8]byte + ctx context.Context + uClient *UTLSClientConfig + publicKey []byte + shortID [8]byte + supportX25519MLKEM768 bool } func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { @@ -84,7 +85,7 @@ func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAd return nil, E.New("invalid short_id") } - var config Config = &RealityClientConfig{ctx, uClient.(*UTLSClientConfig), publicKey, shortID} + var config Config = &RealityClientConfig{ctx, uClient.(*UTLSClientConfig), publicKey, shortID, options.Reality.SupportX25519MLKEM768} if options.KernelRx || options.KernelTx { if !C.IsLinux { return nil, E.New("kTLS is only supported on Linux") @@ -145,22 +146,24 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn if err != nil { return nil, err } - for _, extension := range uConn.Extensions { - if ce, ok := extension.(*utls.SupportedCurvesExtension); ok { - ce.Curves = common.Filter(ce.Curves, func(curveID utls.CurveID) bool { - return curveID != utls.X25519MLKEM768 - }) + if !e.supportX25519MLKEM768 { + for _, extension := range uConn.Extensions { + if ce, ok := extension.(*utls.SupportedCurvesExtension); ok { + ce.Curves = common.Filter(ce.Curves, func(curveID utls.CurveID) bool { + return curveID != utls.X25519MLKEM768 + }) + } + if ks, ok := extension.(*utls.KeyShareExtension); ok { + ks.KeyShares = common.Filter(ks.KeyShares, func(share utls.KeyShare) bool { + return share.Group != utls.X25519MLKEM768 + }) + } } - if ks, ok := extension.(*utls.KeyShareExtension); ok { - ks.KeyShares = common.Filter(ks.KeyShares, func(share utls.KeyShare) bool { - return share.Group != utls.X25519MLKEM768 - }) + err = uConn.BuildHandshakeState() + if err != nil { + return nil, err } } - err = uConn.BuildHandshakeState() - if err != nil { - return nil, err - } if len(uConfig.NextProtos) > 0 { for _, extension := range uConn.Extensions { @@ -271,6 +274,7 @@ func (e *RealityClientConfig) Clone() Config { e.uClient.Clone().(*UTLSClientConfig), e.publicKey, e.shortID, + e.supportX25519MLKEM768, } } diff --git a/sing-box-rr/daemon/log_ring.go b/sing-box-rr/daemon/log_ring.go new file mode 100644 index 00000000..f59e9cc2 --- /dev/null +++ b/sing-box-rr/daemon/log_ring.go @@ -0,0 +1,39 @@ +package daemon + +import ( + "github.com/sagernet/sing-box/log" +) + +type logRing struct { + entries []*log.Entry + maxLines int + start int +} + +func (r *logRing) push(entry *log.Entry) { + if r.maxLines <= 0 { + return + } + if len(r.entries) < r.maxLines { + r.entries = append(r.entries, entry) + return + } + r.entries[r.start] = entry + r.start++ + if r.start == len(r.entries) { + r.start = 0 + } +} + +func (r *logRing) array() []*log.Entry { + result := make([]*log.Entry, 0, len(r.entries)) + result = append(result, r.entries[r.start:]...) + result = append(result, r.entries[:r.start]...) + return result +} + +func (r *logRing) reset() { + clear(r.entries) + r.entries = r.entries[:0] + r.start = 0 +} diff --git a/sing-box-rr/daemon/started_service.go b/sing-box-rr/daemon/started_service.go index 0ca8e4bb..599d5442 100644 --- a/sing-box-rr/daemon/started_service.go +++ b/sing-box-rr/daemon/started_service.go @@ -23,7 +23,6 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/memory" "github.com/sagernet/sing/common/observable" - "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" "github.com/gofrs/uuid/v5" @@ -47,7 +46,6 @@ type StartedService struct { // platform adapter.PlatformInterface handler PlatformHandler debug bool - logMaxLines int oomKillerEnabled bool oomKillerDisabled bool oomMemoryLimit uint64 @@ -64,7 +62,7 @@ type StartedService struct { serviceStatusSubscriber *observable.Subscriber[*ServiceStatus] serviceStatusObserver *observable.Observer[*ServiceStatus] logAccess sync.RWMutex - logLines list.List[*log.Entry] + logLines logRing logSubscriber *observable.Subscriber[*log.Entry] logObserver *observable.Observer[*log.Entry] instance *Instance @@ -99,7 +97,7 @@ func NewStartedService(options ServiceOptions) *StartedService { // platform: options.Platform, handler: options.Handler, debug: options.Debug, - logMaxLines: options.LogMaxLines, + logLines: logRing{maxLines: options.LogMaxLines}, oomKillerEnabled: options.OOMKillerEnabled, oomKillerDisabled: options.OOMKillerDisabled, oomMemoryLimit: options.OOMMemoryLimit, @@ -140,7 +138,7 @@ func (s *StartedService) GetVersion(ctx context.Context, empty *emptypb.Empty) ( func (s *StartedService) resetLogs() { s.logAccess.Lock() - s.logLines = list.List[*log.Entry]{} + s.logLines.reset() s.logAccess.Unlock() s.logSubscriber.Emit(nil) } @@ -378,12 +376,8 @@ func (s *StartedService) SubscribeServiceStatus(empty *emptypb.Empty, server grp } func (s *StartedService) SubscribeLog(empty *emptypb.Empty, server grpc.ServerStreamingServer[Log]) error { - var savedLines []*log.Entry s.logAccess.Lock() - savedLines = make([]*log.Entry, 0, s.logLines.Len()) - for element := s.logLines.Front(); element != nil; element = element.Next() { - savedLines = append(savedLines, element.Value) - } + savedLines := s.logLines.array() subscription, done, err := s.logObserver.Subscribe() s.logAccess.Unlock() if err != nil { @@ -2092,10 +2086,7 @@ func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() { func (s *StartedService) WriteMessage(level log.Level, message string) { item := &log.Entry{Level: level, Message: message} s.logAccess.Lock() - s.logLines.PushBack(item) - if s.logLines.Len() > s.logMaxLines { - s.logLines.Remove(s.logLines.Front()) - } + s.logLines.push(item) s.logAccess.Unlock() s.logSubscriber.Emit(item) if s.debug { @@ -2106,7 +2097,7 @@ func (s *StartedService) WriteMessage(level log.Level, message string) { func (s *StartedService) SavedLog() []*log.Entry { s.logAccess.RLock() defer s.logAccess.RUnlock() - return s.logLines.Array() + return s.logLines.array() } func (s *StartedService) Instance() *Instance { diff --git a/sing-box-rr/dns/router.go b/sing-box-rr/dns/router.go index 46355c3f..e7c0e964 100644 --- a/sing-box-rr/dns/router.go +++ b/sing-box-rr/dns/router.go @@ -1073,6 +1073,19 @@ func (r *Router) prepareExchange(ctx context.Context, message *mDNS.Msg) (*dnsEx Question: message.Question, }, nil } + if isResolverDiscoveryQuery(message.Question[0]) { + r.logger.DebugContext(ctx, "rejected resolver discovery query ", FormatQuestion(message.Question[0].String())) + return nil, &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Id: message.Id, + Response: true, + RecursionDesired: message.RecursionDesired, + RecursionAvailable: true, + Rcode: mDNS.RcodeSuccess, + }, + Question: message.Question, + }, nil + } r.rulesAccess.RLock() if r.closing { r.rulesAccess.RUnlock() @@ -1335,6 +1348,10 @@ response: return responseAddrs, err } +func isResolverDiscoveryQuery(question mDNS.Question) bool { + return question.Qtype == mDNS.TypeSVCB && len(question.Name) > 5 && strings.EqualFold(question.Name[:5], "_dns.") +} + func isAddressQuery(message *mDNS.Msg) bool { for _, question := range message.Question { if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA || question.Qtype == mDNS.TypeHTTPS { diff --git a/sing-box-rr/dns/transport/dhcp/dhcp.go b/sing-box-rr/dns/transport/dhcp/dhcp.go index b247f544..239a4663 100644 --- a/sing-box-rr/dns/transport/dhcp/dhcp.go +++ b/sing-box-rr/dns/transport/dhcp/dhcp.go @@ -109,7 +109,11 @@ func (t *Transport) Start(stage adapter.StartStage) error { return nil } if t.interfaceName == "" { - t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated) + interfaceMonitor := t.networkManager.InterfaceMonitor() + if interfaceMonitor == nil { + return E.New("missing monitor for auto DHCP, set route.auto_detect_interface") + } + t.interfaceCallback = interfaceMonitor.RegisterCallback(t.interfaceUpdated) } go func() { err := t.fetch() diff --git a/sing-box-rr/docs/configuration/shared/tls.md b/sing-box-rr/docs/configuration/shared/tls.md index 5929962c..960539c6 100644 --- a/sing-box-rr/docs/configuration/shared/tls.md +++ b/sing-box-rr/docs/configuration/shared/tls.md @@ -152,7 +152,8 @@ icon: material/new-box "reality": { "enabled": false, "public_key": "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", - "short_id": "0123456789abcdef" + "short_id": "0123456789abcdef", + "support_x25519mlkem768": false } } ``` @@ -825,6 +826,16 @@ Public key, generated by `sing-box generate reality-keypair`. A hexadecimal string with zero to eight digits. +#### support_x25519mlkem768 + +==Client only== + +Keep the `X25519MLKEM768` post-quantum hybrid key share in the ClientHello instead of stripping it. + +Required by REALITY servers running `xtls/reality` built after 2026-09-08 (Xray-core >= v26.9.8), which reject +ClientHellos that don't offer it before any plain `X25519` share. Older REALITY servers may fail the handshake +if this key share is present, so it is disabled by default. + #### max_time_difference ==Server only== diff --git a/sing-box-rr/go.mod b/sing-box-rr/go.mod index 0d6a799d..e69593f7 100644 --- a/sing-box-rr/go.mod +++ b/sing-box-rr/go.mod @@ -61,7 +61,7 @@ require ( github.com/sagernet/netlink v0.0.0-20260814022025-64455d367bbf github.com/sagernet/nftables v0.3.0-mod.4 github.com/sagernet/quic-go v0.61.0-sing-box-mod.7 - github.com/sagernet/sing v0.9.0-beta.4 + github.com/sagernet/sing v0.9.4 github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 github.com/sagernet/sing-mux v0.3.5 github.com/sagernet/sing-openconnect v0.0.0-20260810065514-53aa8058f8df @@ -71,7 +71,7 @@ require ( github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1 github.com/sagernet/sing-snell v0.0.0-20260829071736-20f2eaec77c3 - github.com/sagernet/sing-tun v0.9.0-beta.4 + github.com/sagernet/sing-tun v0.9.3 github.com/sagernet/sing-usbip v0.0.0-20260817040617-28bd42667eca github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 diff --git a/sing-box-rr/go.sum b/sing-box-rr/go.sum index 5218ddc6..bb8a7d81 100644 --- a/sing-box-rr/go.sum +++ b/sing-box-rr/go.sum @@ -489,8 +489,8 @@ github.com/sagernet/sing-shadowtls v0.2.1 h1:ZiHZdnEnP+YS73NMsxiZmIFCwNd0M4k7PkG github.com/sagernet/sing-shadowtls v0.2.1/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= github.com/sagernet/sing-snell v0.0.0-20260829071736-20f2eaec77c3 h1:wjEw0lmCSC+oqh7GD4rbMB/dk7+hZ2VopUj4xM5/Jrc= github.com/sagernet/sing-snell v0.0.0-20260829071736-20f2eaec77c3/go.mod h1:et8Lws4f5QbOrY65DmjevHGup3mijJkhswkto6cwciM= -github.com/sagernet/sing-tun v0.9.0-beta.4 h1:gIIZU4HevhtTQubZiOdMD0/RnECD6csl2kG6666KYmQ= -github.com/sagernet/sing-tun v0.9.0-beta.4/go.mod h1:3EgPst7agntRO7D6GOsiZ1l9FoqdLeuWmKT5TnWkmf0= +github.com/sagernet/sing-tun v0.9.3 h1:fCXbGezGMUXi00X0MmG3iX5QR91sPApZwzfr0MAVHv4= +github.com/sagernet/sing-tun v0.9.3/go.mod h1:3EgPst7agntRO7D6GOsiZ1l9FoqdLeuWmKT5TnWkmf0= github.com/sagernet/sing-usbip v0.0.0-20260817040617-28bd42667eca h1:5wA+IE0Fq1CGVLOgSpm0gKKZ03HzugcJ2JyhmDqeX6A= github.com/sagernet/sing-usbip v0.0.0-20260817040617-28bd42667eca/go.mod h1:ADAZZU85MnM91XNhd2TdZRTaIbA7RjTZuQ1KCHcUbNg= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= diff --git a/sing-box-rr/log/format.go b/sing-box-rr/log/format.go index 9d8176f7..4ffa9e54 100644 --- a/sing-box-rr/log/format.go +++ b/sing-box-rr/log/format.go @@ -6,8 +6,6 @@ import ( "strings" "time" - F "github.com/sagernet/sing/common/format" - "github.com/logrusorgru/aurora" ) @@ -20,159 +18,171 @@ type Formatter struct { DisableLineBreak bool } -func (f Formatter) Format(ctx context.Context, level Level, tag string, message string, timestamp time.Time) string { - levelString := strings.ToUpper(FormatLevel(level)) - if !f.DisableColors { +var ( + levelLabels [LevelTrace + 1]string + coloredLevelLabels [LevelTrace + 1]string +) + +func init() { + for level := LevelPanic; level <= LevelTrace; level++ { + label := strings.ToUpper(FormatLevel(level)) + levelLabels[level] = label switch level { case LevelDebug, LevelTrace: - levelString = aurora.White(levelString).String() + coloredLevelLabels[level] = aurora.White(label).String() case LevelInfo: - levelString = aurora.Cyan(levelString).String() - case LevelNotice: - levelString = aurora.Green(levelString).String() + coloredLevelLabels[level] = aurora.Cyan(label).String() case LevelWarn: - levelString = aurora.Yellow(levelString).String() + coloredLevelLabels[level] = aurora.Yellow(label).String() case LevelError, LevelFatal, LevelPanic: - levelString = aurora.Red(levelString).String() + coloredLevelLabels[level] = aurora.Red(label).String() } } - if tag != "" { - message = tag + ": " + message - } +} + +func (f Formatter) Format(ctx context.Context, level Level, tag string, message string, timestamp time.Time) string { var id ID var hasId bool if ctx != nil { id, hasId = IDFromContext(ctx) } + var builder strings.Builder + builder.Grow(len(tag) + len(message) + 64) + f.writePrefix(&builder, level, timestamp) if hasId { - activeDuration := FormatDuration(time.Since(id.CreatedAt)) - if !f.DisableColors { - var color aurora.Color - color = aurora.Color(uint8(id.ID)) - color %= 215 - row := uint(color / 36) - column := uint(color % 36) - - var r, g, b float32 - r = float32(row * 51) - g = float32(column / 6 * 51) - b = float32((column % 6) * 51) - luma := 0.2126*r + 0.7152*g + 0.0722*b - if luma < 60 { - row = 5 - row - column = 35 - column - color = aurora.Color(row*36 + column) - } - color += 16 - color = color << 16 - color |= 1 << 14 - message = F.ToString("[", aurora.Colorize(id.ID, color).String(), " ", activeDuration, "] ", message) - } else { - message = F.ToString("[", id.ID, " ", activeDuration, "] ", message) - } + f.writeIdPrefix(&builder, id) } - switch { - case f.DisableTimestamp: - message = levelString + " " + message - case f.FullTimestamp: - message = timestamp.Format(f.TimestampFormat) + " " + levelString + " " + message - default: - message = levelString + "[" + xd(int(timestamp.Sub(f.BaseTime)/time.Second), 4) + "] " + message + if tag != "" { + builder.WriteString(tag) + builder.WriteString(": ") } if f.DisableLineBreak { - if message[len(message)-1] == '\n' { - message = message[:len(message)-1] - } + builder.WriteString(strings.TrimSuffix(message, "\n")) } else { - if message[len(message)-1] != '\n' { - message += "\n" + builder.WriteString(message) + if !strings.HasSuffix(message, "\n") { + builder.WriteByte('\n') } } - return message + return builder.String() } -func (f Formatter) FormatWithSimple(ctx context.Context, level Level, tag string, message string, timestamp time.Time) (string, string) { - levelString := strings.ToUpper(FormatLevel(level)) - if !f.DisableColors { - switch level { - case LevelDebug, LevelTrace: - levelString = aurora.White(levelString).String() - case LevelInfo: - levelString = aurora.Cyan(levelString).String() - case LevelNotice: - levelString = aurora.Green(levelString).String() - case LevelWarn: - levelString = aurora.Yellow(levelString).String() - case LevelError, LevelFatal, LevelPanic: - levelString = aurora.Red(levelString).String() - } - } - if tag != "" { - message = tag + ": " + message - } - messageSimple := message +func (f Formatter) FormatSimple(ctx context.Context, tag string, message string) string { var id ID var hasId bool if ctx != nil { id, hasId = IDFromContext(ctx) } + if !hasId && tag == "" { + return message + } + var builder strings.Builder + builder.Grow(len(tag) + len(message) + 32) if hasId { - activeDuration := FormatDuration(time.Since(id.CreatedAt)) - if !f.DisableColors { - var color aurora.Color - color = aurora.Color(uint8(id.ID)) - color %= 215 - row := uint(color / 36) - column := uint(color % 36) - - var r, g, b float32 - r = float32(row * 51) - g = float32(column / 6 * 51) - b = float32((column % 6) * 51) - luma := 0.2126*r + 0.7152*g + 0.0722*b - if luma < 60 { - row = 5 - row - column = 35 - column - color = aurora.Color(row*36 + column) - } - color += 16 - color = color << 16 - color |= 1 << 14 - message = F.ToString("[", aurora.Colorize(id.ID, color).String(), " ", activeDuration, "] ", message) - } else { - message = F.ToString("[", id.ID, " ", activeDuration, "] ", message) - } - messageSimple = F.ToString("[", id.ID, " ", activeDuration, "] ", messageSimple) + builder.WriteByte('[') + writeUint(&builder, uint64(id.ID)) + builder.WriteByte(' ') + writeDuration(&builder, time.Since(id.CreatedAt)) + builder.WriteString("] ") + } + if tag != "" { + builder.WriteString(tag) + builder.WriteString(": ") + } + builder.WriteString(message) + return builder.String() +} +func (f Formatter) writePrefix(builder *strings.Builder, level Level, timestamp time.Time) { + var levelString string + if int(level) >= len(levelLabels) { + levelString = "UNKNOWN" + } else if f.DisableColors { + levelString = levelLabels[level] + } else { + levelString = coloredLevelLabels[level] } switch { case f.DisableTimestamp: - message = levelString + " " + message + builder.WriteString(levelString) + builder.WriteByte(' ') case f.FullTimestamp: - message = timestamp.Format(f.TimestampFormat) + " " + levelString + " " + message + var timeBuffer [64]byte + builder.Write(timestamp.AppendFormat(timeBuffer[:0], f.TimestampFormat)) + builder.WriteByte(' ') + builder.WriteString(levelString) + builder.WriteByte(' ') default: - message = levelString + "[" + xd(int(timestamp.Sub(f.BaseTime)/time.Second), 4) + "] " + message + builder.WriteString(levelString) + builder.WriteByte('[') + seconds := strconv.AppendInt(make([]byte, 0, 20), int64(timestamp.Sub(f.BaseTime)/time.Second), 10) + for pad := 4 - len(seconds); pad > 0; pad-- { + builder.WriteByte('0') + } + builder.Write(seconds) + builder.WriteString("] ") } - if message[len(message)-1] != '\n' { - message += "\n" - } - return message, messageSimple } -func xd(value int, x int) string { - message := strconv.Itoa(value) - for len(message) < x { - message = "0" + message +func (f Formatter) writeIdPrefix(builder *strings.Builder, id ID) { + builder.WriteByte('[') + if f.DisableColors { + writeUint(builder, uint64(id.ID)) + } else { + builder.WriteString("\x1b[38;5;") + writeUint(builder, uint64(colorForID(id.ID))) + builder.WriteByte('m') + writeUint(builder, uint64(id.ID)) + builder.WriteString("\x1b[0m") + } + builder.WriteByte(' ') + writeDuration(builder, time.Since(id.CreatedAt)) + builder.WriteString("] ") +} + +func colorForID(value uint32) uint8 { + color := uint8(value) % 215 + row := uint(color / 36) + column := uint(color % 36) + r := float32(row * 51) + g := float32(column / 6 * 51) + b := float32((column % 6) * 51) + luma := 0.2126*r + 0.7152*g + 0.0722*b + if luma < 60 { + row = 5 - row + column = 35 - column + color = uint8(row*36 + column) + } + return color + 16 +} + +func writeUint(builder *strings.Builder, value uint64) { + builder.Write(strconv.AppendUint(make([]byte, 0, 20), value, 10)) +} + +func writeInt(builder *strings.Builder, value int64) { + builder.Write(strconv.AppendInt(make([]byte, 0, 20), value, 10)) +} + +func writeDuration(builder *strings.Builder, duration time.Duration) { + if duration < time.Second { + writeInt(builder, duration.Milliseconds()) + builder.WriteString("ms") + } else if duration < time.Minute { + writeInt(builder, int64(duration.Seconds())) + builder.WriteByte('.') + writeInt(builder, int64(duration.Seconds()*100)%100) + builder.WriteByte('s') + } else { + writeInt(builder, int64(duration.Minutes())) + builder.WriteByte('m') + writeInt(builder, int64(duration.Seconds())%60) + builder.WriteByte('s') } - return message } func FormatDuration(duration time.Duration) string { - if duration < time.Second { - return F.ToString(duration.Milliseconds(), "ms") - } else if duration < time.Minute { - return F.ToString(int64(duration.Seconds()), ".", int64(duration.Seconds()*100)%100, "s") - } else { - return F.ToString(int64(duration.Minutes()), "m", int64(duration.Seconds())%60, "s") - } + var builder strings.Builder + writeDuration(&builder, duration) + return builder.String() } diff --git a/sing-box-rr/log/level.go b/sing-box-rr/log/level.go index d9714c52..123627fb 100644 --- a/sing-box-rr/log/level.go +++ b/sing-box-rr/log/level.go @@ -11,7 +11,6 @@ const ( LevelFatal LevelError LevelWarn - LevelNotice LevelInfo LevelDebug LevelTrace @@ -25,8 +24,6 @@ func FormatLevel(level Level) string { return "debug" case LevelInfo: return "info" - case LevelNotice: - return "notice" case LevelWarn: return "warn" case LevelError: @@ -49,7 +46,7 @@ func ParseLevel(level string) (Level, error) { case "info": return LevelInfo, nil case "notice": - return LevelNotice, nil + return LevelInfo, nil case "warn", "warning": return LevelWarn, nil case "error": diff --git a/sing-box-rr/log/observable.go b/sing-box-rr/log/observable.go index e5167cff..db1e2225 100644 --- a/sing-box-rr/log/observable.go +++ b/sing-box-rr/log/observable.go @@ -24,6 +24,7 @@ type defaultFactory struct { file *os.File filePath string platformWriters atomic.Pointer[[]PlatformWriter] + needConsole bool needObservable bool level Level subscriber *observable.Subscriber[Entry] @@ -58,6 +59,7 @@ func NewDefaultFactory( }, writer: writer, filePath: filePath, + needConsole: writer != io.Discard || filePath != "", needObservable: needObservable, level: LevelTrace, subscriber: observable.NewSubscriber[Entry](128), @@ -81,6 +83,7 @@ func (f *defaultFactory) Start() error { f.writer = logFile f.file = logFile } + f.needConsole = f.writer != io.Discard } if f.needObservable { f.observer = observable.NewObserver[Entry](f.subscriber, 64) @@ -144,19 +147,7 @@ func (f *defaultFactory) UnSubscribe(sub observable.Subscription[Entry]) { } func (f *defaultFactory) output(ctx context.Context, level Level, tag string, message string, timestamp time.Time) { - if f.needObservable { - formatted, formattedSimple := f.formatter.FormatWithSimple(ctx, level, tag, message, timestamp) - if level <= f.level { - if level == LevelPanic { - panic(formatted) - } - f.writer.Write([]byte(formatted)) - if level == LevelFatal { - os.Exit(1) - } - } - f.subscriber.Emit(Entry{level, formattedSimple}) - } else if level <= f.level { + if level <= f.level && (f.needConsole || level == LevelPanic || level == LevelFatal) { formatted := f.formatter.Format(ctx, level, tag, message, timestamp) if level == LevelPanic { panic(formatted) @@ -166,6 +157,9 @@ func (f *defaultFactory) output(ctx context.Context, level Level, tag string, me os.Exit(1) } } + if f.needObservable { + f.subscriber.Emit(Entry{level, f.formatter.FormatSimple(ctx, tag, message)}) + } platformWriters := f.loadPlatformWriters() if len(platformWriters) > 0 { platformMessage := f.platformFormatter.Format(ctx, level, tag, message, timestamp) @@ -247,7 +241,7 @@ func (l *observableLogger) InfoContext(ctx context.Context, args ...any) { } func (l *observableLogger) NoticeContext(ctx context.Context, args ...any) { - l.Log(ctx, LevelNotice, args) + l.Log(ctx, LevelInfo, args) } func (l *observableLogger) WarnContext(ctx context.Context, args ...any) { diff --git a/sing-box-rr/option/tls.go b/sing-box-rr/option/tls.go index 5c807d67..d73bb5b1 100644 --- a/sing-box-rr/option/tls.go +++ b/sing-box-rr/option/tls.go @@ -250,7 +250,8 @@ type OutboundUTLSOptions struct { } type OutboundRealityOptions struct { - Enabled bool `json:"enabled,omitempty"` - PublicKey string `json:"public_key,omitempty"` - ShortID string `json:"short_id,omitempty"` + Enabled bool `json:"enabled,omitempty"` + PublicKey string `json:"public_key,omitempty"` + ShortID string `json:"short_id,omitempty"` + SupportX25519MLKEM768 bool `json:"support_x25519mlkem768,omitempty"` } diff --git a/sing-box-rr/protocol/direct/outbound.go b/sing-box-rr/protocol/direct/outbound.go index 1ca35af0..020b3d62 100644 --- a/sing-box-rr/protocol/direct/outbound.go +++ b/sing-box-rr/protocol/direct/outbound.go @@ -99,7 +99,11 @@ func (h *Outbound) Start(stage adapter.StartStage) error { } func (h *Outbound) fetchMyAddresses() { - myInterfaceNames := h.network.InterfaceMonitor().MyInterfaces() + interfaceMonitor := h.network.InterfaceMonitor() + if interfaceMonitor == nil { + return + } + myInterfaceNames := interfaceMonitor.MyInterfaces() if len(myInterfaceNames) == 0 { return } diff --git a/sing-box-rr/protocol/group/selector.go b/sing-box-rr/protocol/group/selector.go index 0a344103..584e4f3f 100644 --- a/sing-box-rr/protocol/group/selector.go +++ b/sing-box-rr/protocol/group/selector.go @@ -190,7 +190,7 @@ func (s *Selector) NewConnection(ctx context.Context, conn net.Conn, metadata ad if outboundHandler, isHandler := selected.(adapter.ConnectionHandler); isHandler { outboundHandler.NewConnection(ctx, conn, metadata, onClose) } else { - s.connection.NewConnection(ctx, selected, conn, metadata, onClose) + s.connection.NewConnection(ctx, s, conn, metadata, onClose) } } @@ -201,7 +201,7 @@ func (s *Selector) NewPacketConnection(ctx context.Context, conn N.PacketConn, m if outboundHandler, isHandler := selected.(adapter.PacketConnectionHandler); isHandler { outboundHandler.NewPacketConnection(ctx, conn, metadata, onClose) } else { - s.connection.NewPacketConnection(ctx, selected, conn, metadata, onClose) + s.connection.NewPacketConnection(ctx, s, conn, metadata, onClose) } } diff --git a/sing-box-rr/route/route.go b/sing-box-rr/route/route.go index ec981670..14873089 100644 --- a/sing-box-rr/route/route.go +++ b/sing-box-rr/route/route.go @@ -404,9 +404,20 @@ func (r *Router) PreMatch(metadata adapter.InboundContext, firstPacket []byte) a } return adapter.PreMatchResult{Action: adapter.PreMatchBypass} } - return r.preMatchFlow(ctx, &metadata, packetDestination, currentRule, action.Outbound) + if metadata.Destination.IsDomain() || metadata.Destination != packetDestination { + return r.preMatchFlow(ctx, &metadata, packetDestination, currentRule, action.Outbound) + } + result := r.preMatchFlow(ctx, &metadata, packetDestination, currentRule, action.Outbound) + if result.Action != adapter.PreMatchFlow { + return adapter.PreMatchResult{Action: adapter.PreMatchBypass} + } + result.Action = adapter.PreMatchBypass + return result case *R.RuleActionReject: rejectErr := action.Error(r.ctx) + if rejectErr == nil && metadata.Network == N.NetworkICMP { + return continueResult + } if errors.Is(rejectErr, R.ErrDrop) { return adapter.PreMatchResult{Action: adapter.PreMatchDrop} } @@ -524,9 +535,9 @@ func (r *Router) preMatchFlow(ctx context.Context, metadata *adapter.InboundCont } else if metadata.Destination != packetDestination { result.Destination = metadata.Destination.AddrPort() } - r.logger.InfoContext(ctx, "pre-match: forward ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString(), " via outbound/", outbound.Type(), "[", outbound.Tag(), "]") metadataCopy := *metadata result.NewTracker = func() tun.FlowTracker { + r.logger.InfoContext(ctx, "pre-match: forward ", metadataCopy.Network, " connection from ", metadataCopy.Source.AddrString(), " to ", metadataCopy.Destination.AddrString(), " via outbound/", outbound.Type(), "[", outbound.Tag(), "]") flowTrackers := make([]tun.FlowTracker, 0, len(r.trackers)+1) flowTrackers = append(flowTrackers, newFlowLogger(ctx, r.logger, metadataCopy, outbound)) for _, tracker := range r.trackers { diff --git a/sing-box-rr/route/rule/match_state.go b/sing-box-rr/route/rule/match_state.go index f7f937f8..e6b4fd84 100644 --- a/sing-box-rr/route/rule/match_state.go +++ b/sing-box-rr/route/rule/match_state.go @@ -1,5 +1,7 @@ package rule +import "github.com/sagernet/sing-box/adapter" + type ruleMatchState uint8 const ( @@ -24,3 +26,23 @@ func (g ruleGroupMatch) mergeWith(other ruleGroupMatch) ruleGroupMatch { satisfied: g.satisfied | other.satisfied, } } + +type ruleMatchSnapshot struct { + ipCidrMatchSource bool + ipCidrAcceptEmpty bool + deferredIPCIDRMatchGroups uint8 +} + +func snapshotRuleMatch(metadata *adapter.InboundContext) ruleMatchSnapshot { + return ruleMatchSnapshot{ + ipCidrMatchSource: metadata.IPCIDRMatchSource, + ipCidrAcceptEmpty: metadata.IPCIDRAcceptEmpty, + deferredIPCIDRMatchGroups: metadata.DeferredIPCIDRMatchGroups, + } +} + +func (s ruleMatchSnapshot) restore(metadata *adapter.InboundContext) { + metadata.IPCIDRMatchSource = s.ipCidrMatchSource + metadata.IPCIDRAcceptEmpty = s.ipCidrAcceptEmpty + metadata.DeferredIPCIDRMatchGroups = s.deferredIPCIDRMatchGroups +} diff --git a/sing-box-rr/route/rule/rule_abstract.go b/sing-box-rr/route/rule/rule_abstract.go index f21df788..cb7d5060 100644 --- a/sing-box-rr/route/rule/rule_abstract.go +++ b/sing-box-rr/route/rule/rule_abstract.go @@ -206,32 +206,32 @@ func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool { matched bool deferredGroups uint8 ) + snapshot := snapshotRuleMatch(metadata) if r.mode == C.LogicalTypeAnd { matched = true for _, rule := range r.rules { - nestedMetadata := *metadata - nestedMetadata.ResetRuleCache() - if !rule.Match(&nestedMetadata) { + metadata.ResetRuleCache() + if !rule.Match(metadata) { matched = false deferredGroups = 0 break } - deferredGroups |= nestedMetadata.DeferredIPCIDRMatchGroups + deferredGroups |= metadata.DeferredIPCIDRMatchGroups } } else { for _, rule := range r.rules { - nestedMetadata := *metadata - nestedMetadata.ResetRuleCache() - if rule.Match(&nestedMetadata) { + metadata.ResetRuleCache() + if rule.Match(metadata) { matched = true - if nestedMetadata.DeferredIPCIDRMatchGroups == 0 { + if metadata.DeferredIPCIDRMatchGroups == 0 { deferredGroups = 0 break } - deferredGroups |= nestedMetadata.DeferredIPCIDRMatchGroups + deferredGroups |= metadata.DeferredIPCIDRMatchGroups } } } + snapshot.restore(metadata) if matched { metadata.DeferredIPCIDRMatchGroups |= deferredGroups } diff --git a/sing-box-rr/route/rule/rule_default_interface_address.go b/sing-box-rr/route/rule/rule_default_interface_address.go index 2d7fdebe..611cb0dc 100644 --- a/sing-box-rr/route/rule/rule_default_interface_address.go +++ b/sing-box-rr/route/rule/rule_default_interface_address.go @@ -29,6 +29,9 @@ func NewDefaultInterfaceAddressItem(networkManager adapter.NetworkManager, inter } func (r *DefaultInterfaceAddressItem) Match(metadata *adapter.InboundContext) bool { + if r.interfaceMonitor == nil { + return false + } defaultInterface := r.interfaceMonitor.DefaultInterface() if defaultInterface == nil { return false diff --git a/sing-box-rr/route/rule/rule_item_rule_set.go b/sing-box-rr/route/rule/rule_item_rule_set.go index eb7c74e1..256183e2 100644 --- a/sing-box-rr/route/rule/rule_item_rule_set.go +++ b/sing-box-rr/route/rule/rule_item_rule_set.go @@ -52,12 +52,15 @@ func (r *RuleSetItem) Close() error { } func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { + snapshot := snapshotRuleMatch(metadata) for _, ruleSet := range r.setList { - nestedMetadata := r.nestedMetadata(metadata) - if ruleSet.Match(&nestedMetadata) { + r.prepareNestedMatch(metadata) + if ruleSet.Match(metadata) { + snapshot.restore(metadata) return true } } + snapshot.restore(metadata) return false } @@ -67,17 +70,19 @@ func (r *RuleSetItem) matchWithOuterGroups(metadata *adapter.InboundContext, out matched bool deferredGroups uint8 ) + snapshot := snapshotRuleMatch(metadata) for _, ruleSet := range r.setList { - nestedMetadata := r.nestedMetadata(metadata) + r.prepareNestedMatch(metadata) if provider, isProvider := ruleSet.(mergeableRuleProvider); isProvider { branch := provider.mergeableRule() if branch != nil { - branchGroups, branchMatched := branch.evaluateForMerge(&nestedMetadata) + branchGroups, branchMatched := branch.evaluateForMerge(metadata) if branchMatched { merged := outerGroups.mergeWith(branchGroups) if merged.done() { - branchDeferredGroups := nestedMetadata.DeferredIPCIDRMatchGroups &^ uint8(merged.satisfied) + branchDeferredGroups := metadata.DeferredIPCIDRMatchGroups &^ uint8(merged.satisfied) if branchDeferredGroups == 0 { + snapshot.restore(metadata) metadata.DeferredIPCIDRMatchGroups &^= uint8(merged.satisfied) return true } @@ -88,26 +93,26 @@ func (r *RuleSetItem) matchWithOuterGroups(metadata *adapter.InboundContext, out continue } } - if outerDone && ruleSet.Match(&nestedMetadata) { - if nestedMetadata.DeferredIPCIDRMatchGroups == 0 { + if outerDone && ruleSet.Match(metadata) { + if metadata.DeferredIPCIDRMatchGroups == 0 { + snapshot.restore(metadata) return true } matched = true - deferredGroups |= nestedMetadata.DeferredIPCIDRMatchGroups + deferredGroups |= metadata.DeferredIPCIDRMatchGroups } } + snapshot.restore(metadata) if matched { metadata.DeferredIPCIDRMatchGroups |= deferredGroups } return matched } -func (r *RuleSetItem) nestedMetadata(metadata *adapter.InboundContext) adapter.InboundContext { - nestedMetadata := *metadata - nestedMetadata.ResetRuleMatchCache() - nestedMetadata.IPCIDRMatchSource = r.ipCidrMatchSource - nestedMetadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty - return nestedMetadata +func (r *RuleSetItem) prepareNestedMatch(metadata *adapter.InboundContext) { + metadata.ResetRuleMatchCache() + metadata.IPCIDRMatchSource = r.ipCidrMatchSource + metadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty } type mergeableRuleProvider interface { @@ -130,17 +135,19 @@ func matchAnyHeadlessRule(rules []adapter.HeadlessRule, metadata *adapter.Inboun matched bool deferredGroups uint8 ) + snapshot := snapshotRuleMatch(metadata) for _, rule := range rules { - nestedMetadata := *metadata - nestedMetadata.ResetRuleMatchCache() - if rule.Match(&nestedMetadata) { - if nestedMetadata.DeferredIPCIDRMatchGroups == 0 { + metadata.ResetRuleMatchCache() + if rule.Match(metadata) { + if metadata.DeferredIPCIDRMatchGroups == 0 { + snapshot.restore(metadata) return true } matched = true - deferredGroups |= nestedMetadata.DeferredIPCIDRMatchGroups + deferredGroups |= metadata.DeferredIPCIDRMatchGroups } } + snapshot.restore(metadata) if matched { metadata.DeferredIPCIDRMatchGroups |= deferredGroups } diff --git a/sing-box-rr/route/rule/rule_network_interface_address.go b/sing-box-rr/route/rule/rule_network_interface_address.go index e331d622..d20e6781 100644 --- a/sing-box-rr/route/rule/rule_network_interface_address.go +++ b/sing-box-rr/route/rule/rule_network_interface_address.go @@ -40,7 +40,11 @@ func NewNetworkInterfaceAddressItem(networkManager adapter.NetworkManager, inter func (r *NetworkInterfaceAddressItem) Match(metadata *adapter.InboundContext) bool { interfaces := r.networkManager.NetworkInterfaces() - myInterfaces := r.networkManager.InterfaceMonitor().MyInterfaces() + var myInterfaces []string + interfaceMonitor := r.networkManager.InterfaceMonitor() + if interfaceMonitor != nil { + myInterfaces = interfaceMonitor.MyInterfaces() + } match: for ifType, addresses := range r.interfaceAddresses { for _, networkInterface := range interfaces { diff --git a/sing-box-rr/transport/v2raygrpclite/client.go b/sing-box-rr/transport/v2raygrpclite/client.go index 26cbf769..5039074f 100644 --- a/sing-box-rr/transport/v2raygrpclite/client.go +++ b/sing-box-rr/transport/v2raygrpclite/client.go @@ -9,9 +9,9 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/force_close" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing-box/transport/v2rayhttp" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" @@ -102,6 +102,6 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { } func (c *Client) Close() error { - v2rayhttp.ResetTransport(c.transport) + force_close.ResetTransport(c.transport) return nil } diff --git a/sing-box-rr/transport/v2rayhttp/client.go b/sing-box-rr/transport/v2rayhttp/client.go index 6c327cd6..334f85a5 100644 --- a/sing-box-rr/transport/v2rayhttp/client.go +++ b/sing-box-rr/transport/v2rayhttp/client.go @@ -11,6 +11,7 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/force_close" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" @@ -152,6 +153,6 @@ func (c *Client) dialHTTP2(ctx context.Context) (net.Conn, error) { } func (c *Client) Close() error { - c.transport = ResetTransport(c.transport) + c.transport = force_close.ResetTransport(c.transport) return nil } diff --git a/sing-box-rr/transport/v2rayxhttp/dialer.go b/sing-box-rr/transport/v2rayxhttp/dialer.go index 96e54af7..18921829 100644 --- a/sing-box-rr/transport/v2rayxhttp/dialer.go +++ b/sing-box-rr/transport/v2rayxhttp/dialer.go @@ -9,18 +9,16 @@ import ( "net" "net/http" "net/http/httptrace" - "reflect" "strings" "sync" - "unsafe" + "sync/atomic" - "github.com/sagernet/quic-go/http3" + "github.com/sagernet/sing-box/common/force_close" common "github.com/sagernet/sing-box/common/xray" "github.com/sagernet/sing-box/common/xray/buf" "github.com/sagernet/sing-box/common/xray/signal/done" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" - "golang.org/x/net/http2" ) // interface to abstract between use of browser dialer, vs net/http @@ -48,20 +46,6 @@ type DefaultDialerClient struct { mtx sync.RWMutex } -type clientConnPool struct { - t *http2.Transport - mu sync.Mutex - conns map[string][]*http2.ClientConn // key is host:port -} - -type efaceWords struct { - typ unsafe.Pointer - data unsafe.Pointer -} - -//go:linkname transportConnPool golang.org/x/net/http2.(*Transport).connPool -func transportConnPool(t *http2.Transport) http2.ClientConnPool - func (c *DefaultDialerClient) Close() { c.mtx.Lock() defer c.mtx.Unlock() @@ -69,24 +53,7 @@ func (c *DefaultDialerClient) Close() { return } c.closed = true - switch transport := c.client.Transport.(type) { - case *http.Transport: - transport.CloseIdleConnections() - case *http2.Transport: - connPool := transportConnPool(transport) - p := (*clientConnPool)((*efaceWords)(unsafe.Pointer(&connPool)).data) - p.mu.Lock() - defer p.mu.Unlock() - for _, vv := range p.conns { - for _, cc := range vv { - cc.Close() - } - } - case *http3.Transport: - transport.Close() - default: - panic(E.New("unknown transport type: ", reflect.TypeOf(transport))) - } + c.client.Transport = force_close.ResetTransport(c.client.Transport) } func (c *DefaultDialerClient) IsClosed() bool { @@ -114,7 +81,7 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessio reqCtx, cancel := context.WithCancel(context.WithoutCancel(ctx)) req, _ := http.NewRequestWithContext(reqCtx, method, url, body) FillStreamRequest(req, sessionId, "", c.options) - wrc = &WaitReadCloser{Wait: make(chan struct{}), Cancel: cancel} + wrc = &WaitReadCloser{wait: done.New(), Cancel: cancel} go func() { var resp *http.Response resp, err = c.client.Do(req) @@ -215,42 +182,40 @@ func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, sessio } type WaitReadCloser struct { - Wait chan struct{} + wait *done.Instance Cancel context.CancelFunc - io.ReadCloser + reader atomic.Pointer[io.ReadCloser] } func (w *WaitReadCloser) Set(rc io.ReadCloser) { - w.ReadCloser = rc - defer func() { - if recover() != nil { - rc.Close() + w.reader.Store(&rc) + if w.wait.Done() { + if p := w.reader.Swap(nil); p != nil { + (*p).Close() } - }() - close(w.Wait) + } + w.wait.Close() } func (w *WaitReadCloser) Read(b []byte) (int, error) { - <-w.Wait - if w.ReadCloser == nil { - return 0, io.ErrClosedPipe + rc := w.reader.Load() + if rc == nil { + <-w.wait.Wait() + if rc = w.reader.Load(); rc == nil { + return 0, io.ErrClosedPipe + } } - return w.ReadCloser.Read(b) + return (*rc).Read(b) } func (w *WaitReadCloser) Close() error { if w.Cancel != nil { w.Cancel() } - if w.ReadCloser != nil { - return w.ReadCloser.Close() + w.wait.Close() + if p := w.reader.Swap(nil); p != nil { + return (*p).Close() } - defer func() { - if recover() != nil && w.ReadCloser != nil { - w.ReadCloser.Close() - } - }() - close(w.Wait) return nil } diff --git a/sing-box-rr/transport/v2rayxhttp/utils.go b/sing-box-rr/transport/v2rayxhttp/utils.go index 408343f3..96c9b8d5 100644 --- a/sing-box-rr/transport/v2rayxhttp/utils.go +++ b/sing-box-rr/transport/v2rayxhttp/utils.go @@ -1,6 +1,7 @@ package xhttp import ( + "bytes" "encoding/base64" "fmt" "io" @@ -73,14 +74,17 @@ func FillStreamRequest(request *http.Request, sessionId string, seqStr string, o func FillPacketRequest(request *http.Request, sessionId string, seqStr string, payload buf.MultiBuffer, options *option.V2RayXHTTPBaseOptions) error { dataPlacement := options.GetNormalizedUplinkDataPlacement() + data := make([]byte, payload.Len()) + payload.Copy(data) + buf.ReleaseMulti(payload) if dataPlacement == option.PlacementBody || dataPlacement == option.PlacementAuto { request.Header = options.GetRequestHeader() - request.Body = io.NopCloser(&buf.MultiBufferContainer{MultiBuffer: payload}) - request.ContentLength = int64(payload.Len()) + request.Body = io.NopCloser(bytes.NewReader(data)) + request.ContentLength = int64(len(data)) + request.GetBody = func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(data)), nil + } } else { - data := make([]byte, payload.Len()) - payload.Copy(data) - buf.ReleaseMulti(payload) switch dataPlacement { case option.PlacementHeader: request.Header = GetRequestHeaderWithPayload(data, options)