Backport sing-box 2.7.2 fixes
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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:
|
||||
|
||||
+11
-7
@@ -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
|
||||
}
|
||||
+15
-15
@@ -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
|
||||
|
||||
+2
-2
@@ -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()
|
||||
}
|
||||
+8
-8
@@ -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
|
||||
@@ -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())
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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==
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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=
|
||||
|
||||
+127
-117
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user