mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-11 17:47:20 +10:00
Compare commits
35 Commits
03a41bf6c9
...
stable
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
025b947a24 | ||
|
|
76fa3c2e5e | ||
|
|
53db1f178c | ||
|
|
55ec8abf17 | ||
|
|
5a957fd750 | ||
|
|
7c3d8cf8db | ||
|
|
813b634d08 | ||
|
|
d9b435fb62 | ||
|
|
354b4b040e | ||
|
|
7ffdc48b49 | ||
|
|
e15bdf11eb | ||
|
|
e3bcb06c3e | ||
|
|
84d2280960 | ||
|
|
4fd2532b0a | ||
|
|
02ccde6c71 | ||
|
|
e98b4ad449 | ||
|
|
d09182614c | ||
|
|
6381de7bab | ||
|
|
b0c6762bc1 | ||
|
|
7425100bac | ||
|
|
d454aa0fdf | ||
|
|
a3623eb41a | ||
|
|
72bc4c1f87 | ||
|
|
9ac1e2ff32 | ||
|
|
0045103d14 | ||
|
|
d2a933784c | ||
|
|
3f05a37f65 | ||
|
|
b8e5a71450 | ||
|
|
c13faa8e3c | ||
|
|
7623bcd19e | ||
|
|
795d1c2892 | ||
|
|
6913b11e0a | ||
|
|
1e57c06295 | ||
|
|
ea464cef8d | ||
|
|
a8e3cd3256 |
@@ -4,6 +4,7 @@
|
||||
--license GPL-3.0-or-later
|
||||
--description "The universal proxy platform."
|
||||
--url "https://sing-box.sagernet.org/"
|
||||
--vendor SagerNet
|
||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
|
||||
--no-deb-generate-changes
|
||||
|
||||
33
.github/detect_track.sh
vendored
Executable file
33
.github/detect_track.sh
vendored
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
branches=$(git branch -r --contains HEAD)
|
||||
if echo "$branches" | grep -q 'origin/stable'; then
|
||||
track=stable
|
||||
elif echo "$branches" | grep -q 'origin/testing'; then
|
||||
track=testing
|
||||
elif echo "$branches" | grep -q 'origin/oldstable'; then
|
||||
track=oldstable
|
||||
else
|
||||
echo "ERROR: HEAD is not on any known release branch (stable/testing/oldstable)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$track" == "stable" ]]; then
|
||||
tag=$(git describe --tags --exact-match HEAD 2>/dev/null || true)
|
||||
if [[ -n "$tag" && "$tag" == *"-"* ]]; then
|
||||
track=beta
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$track" in
|
||||
stable) name=sing-box; docker_tag=latest ;;
|
||||
beta) name=sing-box-beta; docker_tag=latest-beta ;;
|
||||
testing) name=sing-box-testing; docker_tag=latest-testing ;;
|
||||
oldstable) name=sing-box-oldstable; docker_tag=latest-oldstable ;;
|
||||
esac
|
||||
|
||||
echo "track=${track} name=${name} docker_tag=${docker_tag}" >&2
|
||||
echo "TRACK=${track}" >> "$GITHUB_ENV"
|
||||
echo "NAME=${name}" >> "$GITHUB_ENV"
|
||||
echo "DOCKER_TAG=${docker_tag}" >> "$GITHUB_ENV"
|
||||
19
.github/workflows/docker.yml
vendored
19
.github/workflows/docker.yml
vendored
@@ -19,7 +19,6 @@ env:
|
||||
jobs:
|
||||
build_binary:
|
||||
name: Build binary
|
||||
if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
@@ -260,13 +259,13 @@ jobs:
|
||||
fi
|
||||
echo "ref=$ref"
|
||||
echo "ref=$ref" >> $GITHUB_OUTPUT
|
||||
if [[ $ref == *"-"* ]]; then
|
||||
latest=latest-beta
|
||||
else
|
||||
latest=latest
|
||||
fi
|
||||
echo "latest=$latest"
|
||||
echo "latest=$latest" >> $GITHUB_OUTPUT
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
ref: ${{ steps.ref.outputs.ref }}
|
||||
fetch-depth: 0
|
||||
- name: Detect track
|
||||
run: bash .github/detect_track.sh
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
@@ -286,11 +285,11 @@ jobs:
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}" \
|
||||
-t "${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}" \
|
||||
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
|
||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||
- name: Inspect image
|
||||
if: github.event_name != 'push'
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}
|
||||
|
||||
16
.github/workflows/linux.yml
vendored
16
.github/workflows/linux.yml
vendored
@@ -11,11 +11,6 @@ on:
|
||||
description: "Version name"
|
||||
required: true
|
||||
type: string
|
||||
forceBeta:
|
||||
description: "Force beta"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
@@ -23,7 +18,6 @@ on:
|
||||
jobs:
|
||||
calculate_version:
|
||||
name: Calculate version
|
||||
if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.outputs.outputs.version }}
|
||||
@@ -168,14 +162,8 @@ jobs:
|
||||
- name: Set mtime
|
||||
run: |-
|
||||
TZ=UTC touch -t '197001010000' dist/sing-box
|
||||
- name: Set name
|
||||
if: (! contains(needs.calculate_version.outputs.version, '-')) && !inputs.forceBeta
|
||||
run: |-
|
||||
echo "NAME=sing-box" >> "$GITHUB_ENV"
|
||||
- name: Set beta name
|
||||
if: contains(needs.calculate_version.outputs.version, '-') || inputs.forceBeta
|
||||
run: |-
|
||||
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
|
||||
- name: Detect track
|
||||
run: bash .github/detect_track.sh
|
||||
- name: Set version
|
||||
run: |-
|
||||
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
||||
|
||||
@@ -101,6 +101,10 @@ type InboundContext struct {
|
||||
func (c *InboundContext) ResetRuleCache() {
|
||||
c.IPCIDRMatchSource = false
|
||||
c.IPCIDRAcceptEmpty = false
|
||||
c.ResetRuleMatchCache()
|
||||
}
|
||||
|
||||
func (c *InboundContext) ResetRuleMatchCache() {
|
||||
c.SourceAddressMatch = false
|
||||
c.SourcePortMatch = false
|
||||
c.DestinationAddressMatch = false
|
||||
|
||||
@@ -47,11 +47,11 @@ type FindConnectionOwnerRequest struct {
|
||||
}
|
||||
|
||||
type ConnectionOwner struct {
|
||||
ProcessID uint32
|
||||
UserId int32
|
||||
UserName string
|
||||
ProcessPath string
|
||||
AndroidPackageName string
|
||||
ProcessID uint32
|
||||
UserId int32
|
||||
UserName string
|
||||
ProcessPath string
|
||||
AndroidPackageNames []string
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
|
||||
10
box.go
10
box.go
@@ -19,7 +19,6 @@ import (
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport/local"
|
||||
"github.com/sagernet/sing-box/experimental"
|
||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -326,11 +325,12 @@ func New(options Options) (*Box, error) {
|
||||
)
|
||||
})
|
||||
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
|
||||
return local.NewTransport(
|
||||
return dnsTransportRegistry.CreateDNSTransport(
|
||||
ctx,
|
||||
logFactory.NewLogger("dns/local"),
|
||||
"local",
|
||||
option.LocalDNSServerOptions{},
|
||||
C.DNSTypeLocal,
|
||||
&option.LocalDNSServerOptions{},
|
||||
)
|
||||
})
|
||||
if platformInterface != nil {
|
||||
@@ -555,6 +555,10 @@ func (s *Box) Outbound() adapter.OutboundManager {
|
||||
return s.outbound
|
||||
}
|
||||
|
||||
func (s *Box) Endpoint() adapter.EndpointManager {
|
||||
return s.endpoint
|
||||
}
|
||||
|
||||
func (s *Box) LogFactory() log.Factory {
|
||||
return s.logFactory
|
||||
}
|
||||
|
||||
Submodule clients/android updated: 6f09892c71...fea0f3a7ba
Submodule clients/apple updated: f3b4b2238e...ffbf405b52
@@ -149,7 +149,10 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
} else {
|
||||
dialer.Timeout = C.TCPConnectTimeout
|
||||
}
|
||||
if !options.DisableTCPKeepAlive {
|
||||
if options.DisableTCPKeepAlive {
|
||||
dialer.KeepAlive = -1
|
||||
dialer.KeepAliveConfig.Enable = false
|
||||
} else {
|
||||
keepIdle := time.Duration(options.TCPKeepAlive)
|
||||
if keepIdle == 0 {
|
||||
keepIdle = C.TCPKeepAliveInitial
|
||||
@@ -329,9 +332,9 @@ func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd
|
||||
|
||||
func (d *DefaultDialer) DialerForICMPDestination(destination netip.Addr) net.Dialer {
|
||||
if !destination.Is6() {
|
||||
return d.dialer6.Dialer
|
||||
} else {
|
||||
return d.dialer4.Dialer
|
||||
} else {
|
||||
return d.dialer6.Dialer
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,10 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
|
||||
if l.listenOptions.ReuseAddr {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
|
||||
}
|
||||
if !l.listenOptions.DisableTCPKeepAlive {
|
||||
if l.listenOptions.DisableTCPKeepAlive {
|
||||
listenConfig.KeepAlive = -1
|
||||
listenConfig.KeepAliveConfig.Enable = false
|
||||
} else {
|
||||
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
|
||||
if keepIdle == 0 {
|
||||
keepIdle = C.TCPKeepAliveInitial
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
type Searcher interface {
|
||||
FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
var ErrNotFound = E.New("process not found")
|
||||
@@ -28,7 +29,7 @@ func FindProcessInfo(searcher Searcher, ctx context.Context, network string, sou
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.UserId != -1 {
|
||||
if info.UserId != -1 && info.UserName == "" {
|
||||
osUser, _ := user.LookupId(F.ToString(info.UserId))
|
||||
if osUser != nil {
|
||||
info.UserName = osUser.Username
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
var _ Searcher = (*androidSearcher)(nil)
|
||||
@@ -18,22 +19,30 @@ func NewSearcher(config Config) (Searcher, error) {
|
||||
return &androidSearcher{config.PackageManager}, nil
|
||||
}
|
||||
|
||||
func (s *androidSearcher) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
_, uid, err := resolveSocketByNetlink(network, source, destination)
|
||||
family, protocol, err := socketDiagSettings(network, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sharedPackage, loaded := s.packageManager.SharedPackageByID(uid % 100000); loaded {
|
||||
return &adapter.ConnectionOwner{
|
||||
UserId: int32(uid),
|
||||
AndroidPackageName: sharedPackage,
|
||||
}, nil
|
||||
_, uid, err := querySocketDiagOnce(family, protocol, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded {
|
||||
return &adapter.ConnectionOwner{
|
||||
UserId: int32(uid),
|
||||
AndroidPackageName: packageName,
|
||||
}, nil
|
||||
appID := uid % 100000
|
||||
var packageNames []string
|
||||
if sharedPackage, loaded := s.packageManager.SharedPackageByID(appID); loaded {
|
||||
packageNames = append(packageNames, sharedPackage)
|
||||
}
|
||||
return &adapter.ConnectionOwner{UserId: int32(uid)}, nil
|
||||
if packages, loaded := s.packageManager.PackagesByID(appID); loaded {
|
||||
packageNames = append(packageNames, packages...)
|
||||
}
|
||||
packageNames = common.Uniq(packageNames)
|
||||
return &adapter.ConnectionOwner{
|
||||
UserId: int32(uid),
|
||||
AndroidPackageNames: packageNames,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
//go:build darwin
|
||||
|
||||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var _ Searcher = (*darwinSearcher)(nil)
|
||||
@@ -24,12 +20,12 @@ func NewSearcher(_ Config) (Searcher, error) {
|
||||
return &darwinSearcher{}, nil
|
||||
}
|
||||
|
||||
func (d *darwinSearcher) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
processName, err := findProcessName(network, source.Addr(), int(source.Port()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &adapter.ConnectionOwner{ProcessPath: processName, UserId: -1}, nil
|
||||
return FindDarwinConnectionOwner(network, source, destination)
|
||||
}
|
||||
|
||||
var structSize = func() int {
|
||||
@@ -47,107 +43,3 @@ var structSize = func() int {
|
||||
return 384
|
||||
}
|
||||
}()
|
||||
|
||||
func findProcessName(network string, ip netip.Addr, port int) (string, error) {
|
||||
var spath string
|
||||
switch network {
|
||||
case N.NetworkTCP:
|
||||
spath = "net.inet.tcp.pcblist_n"
|
||||
case N.NetworkUDP:
|
||||
spath = "net.inet.udp.pcblist_n"
|
||||
default:
|
||||
return "", os.ErrInvalid
|
||||
}
|
||||
|
||||
isIPv4 := ip.Is4()
|
||||
|
||||
value, err := unix.SysctlRaw(spath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
buf := value
|
||||
|
||||
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
|
||||
// size/offset are round up (aligned) to 8 bytes in darwin
|
||||
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
|
||||
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
|
||||
itemSize := structSize
|
||||
if network == N.NetworkTCP {
|
||||
// rup8(sizeof(xtcpcb_n))
|
||||
itemSize += 208
|
||||
}
|
||||
|
||||
var fallbackUDPProcess string
|
||||
// skip the first xinpgen(24 bytes) block
|
||||
for i := 24; i+itemSize <= len(buf); i += itemSize {
|
||||
// offset of xinpcb_n and xsocket_n
|
||||
inp, so := i, i+104
|
||||
|
||||
srcPort := binary.BigEndian.Uint16(buf[inp+18 : inp+20])
|
||||
if uint16(port) != srcPort {
|
||||
continue
|
||||
}
|
||||
|
||||
// xinpcb_n.inp_vflag
|
||||
flag := buf[inp+44]
|
||||
|
||||
var srcIP netip.Addr
|
||||
srcIsIPv4 := false
|
||||
switch {
|
||||
case flag&0x1 > 0 && isIPv4:
|
||||
// ipv4
|
||||
srcIP = netip.AddrFrom4([4]byte(buf[inp+76 : inp+80]))
|
||||
srcIsIPv4 = true
|
||||
case flag&0x2 > 0 && !isIPv4:
|
||||
// ipv6
|
||||
srcIP = netip.AddrFrom16([16]byte(buf[inp+64 : inp+80]))
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if ip == srcIP {
|
||||
// xsocket_n.so_last_pid
|
||||
pid := readNativeUint32(buf[so+68 : so+72])
|
||||
return getExecPathFromPID(pid)
|
||||
}
|
||||
|
||||
// udp packet connection may be not equal with srcIP
|
||||
if network == N.NetworkUDP && srcIP.IsUnspecified() && isIPv4 == srcIsIPv4 {
|
||||
pid := readNativeUint32(buf[so+68 : so+72])
|
||||
fallbackUDPProcess, _ = getExecPathFromPID(pid)
|
||||
}
|
||||
}
|
||||
|
||||
if network == N.NetworkUDP && len(fallbackUDPProcess) > 0 {
|
||||
return fallbackUDPProcess, nil
|
||||
}
|
||||
|
||||
return "", ErrNotFound
|
||||
}
|
||||
|
||||
func getExecPathFromPID(pid uint32) (string, error) {
|
||||
const (
|
||||
procpidpathinfo = 0xb
|
||||
procpidpathinfosize = 1024
|
||||
proccallnumpidinfo = 0x2
|
||||
)
|
||||
buf := make([]byte, procpidpathinfosize)
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PROC_INFO,
|
||||
proccallnumpidinfo,
|
||||
uintptr(pid),
|
||||
procpidpathinfo,
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&buf[0])),
|
||||
procpidpathinfosize)
|
||||
if errno != 0 {
|
||||
return "", errno
|
||||
}
|
||||
|
||||
return unix.ByteSliceToString(buf), nil
|
||||
}
|
||||
|
||||
func readNativeUint32(b []byte) uint32 {
|
||||
return *(*uint32)(unsafe.Pointer(&b[0]))
|
||||
}
|
||||
|
||||
269
common/process/searcher_darwin_shared.go
Normal file
269
common/process/searcher_darwin_shared.go
Normal file
@@ -0,0 +1,269 @@
|
||||
//go:build darwin
|
||||
|
||||
package process
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
darwinSnapshotTTL = 200 * time.Millisecond
|
||||
|
||||
darwinXinpgenSize = 24
|
||||
darwinXsocketOffset = 104
|
||||
darwinXinpcbForeignPort = 16
|
||||
darwinXinpcbLocalPort = 18
|
||||
darwinXinpcbVFlag = 44
|
||||
darwinXinpcbForeignAddr = 48
|
||||
darwinXinpcbLocalAddr = 64
|
||||
darwinXinpcbIPv4Addr = 12
|
||||
darwinXsocketUID = 64
|
||||
darwinXsocketLastPID = 68
|
||||
darwinTCPExtraStructSize = 208
|
||||
)
|
||||
|
||||
type darwinConnectionEntry struct {
|
||||
localAddr netip.Addr
|
||||
remoteAddr netip.Addr
|
||||
localPort uint16
|
||||
remotePort uint16
|
||||
pid uint32
|
||||
uid int32
|
||||
}
|
||||
|
||||
type darwinConnectionMatchKind uint8
|
||||
|
||||
const (
|
||||
darwinConnectionMatchExact darwinConnectionMatchKind = iota
|
||||
darwinConnectionMatchLocalFallback
|
||||
darwinConnectionMatchWildcardFallback
|
||||
)
|
||||
|
||||
type darwinSnapshot struct {
|
||||
createdAt time.Time
|
||||
entries []darwinConnectionEntry
|
||||
}
|
||||
|
||||
type darwinConnectionFinder struct {
|
||||
access sync.Mutex
|
||||
ttl time.Duration
|
||||
snapshots map[string]darwinSnapshot
|
||||
builder func(string) (darwinSnapshot, error)
|
||||
}
|
||||
|
||||
var sharedDarwinConnectionFinder = newDarwinConnectionFinder(darwinSnapshotTTL)
|
||||
|
||||
func newDarwinConnectionFinder(ttl time.Duration) *darwinConnectionFinder {
|
||||
return &darwinConnectionFinder{
|
||||
ttl: ttl,
|
||||
snapshots: make(map[string]darwinSnapshot),
|
||||
builder: buildDarwinSnapshot,
|
||||
}
|
||||
}
|
||||
|
||||
func FindDarwinConnectionOwner(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
return sharedDarwinConnectionFinder.find(network, source, destination)
|
||||
}
|
||||
|
||||
func (f *darwinConnectionFinder) find(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
networkName := N.NetworkName(network)
|
||||
source = normalizeDarwinAddrPort(source)
|
||||
destination = normalizeDarwinAddrPort(destination)
|
||||
var lastOwner *adapter.ConnectionOwner
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
snapshot, fromCache, err := f.loadSnapshot(networkName, attempt > 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry, matchKind, err := matchDarwinConnectionEntry(snapshot.entries, networkName, source, destination)
|
||||
if err != nil {
|
||||
if err == ErrNotFound && fromCache {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if fromCache && matchKind != darwinConnectionMatchExact {
|
||||
continue
|
||||
}
|
||||
owner := &adapter.ConnectionOwner{
|
||||
UserId: entry.uid,
|
||||
}
|
||||
lastOwner = owner
|
||||
if entry.pid == 0 {
|
||||
return owner, nil
|
||||
}
|
||||
processPath, err := getExecPathFromPID(entry.pid)
|
||||
if err == nil {
|
||||
owner.ProcessPath = processPath
|
||||
return owner, nil
|
||||
}
|
||||
if fromCache {
|
||||
continue
|
||||
}
|
||||
return owner, nil
|
||||
}
|
||||
if lastOwner != nil {
|
||||
return lastOwner, nil
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
func (f *darwinConnectionFinder) loadSnapshot(network string, forceRefresh bool) (darwinSnapshot, bool, error) {
|
||||
f.access.Lock()
|
||||
defer f.access.Unlock()
|
||||
if !forceRefresh {
|
||||
if snapshot, loaded := f.snapshots[network]; loaded && time.Since(snapshot.createdAt) < f.ttl {
|
||||
return snapshot, true, nil
|
||||
}
|
||||
}
|
||||
snapshot, err := f.builder(network)
|
||||
if err != nil {
|
||||
return darwinSnapshot{}, false, err
|
||||
}
|
||||
f.snapshots[network] = snapshot
|
||||
return snapshot, false, nil
|
||||
}
|
||||
|
||||
func buildDarwinSnapshot(network string) (darwinSnapshot, error) {
|
||||
spath, itemSize, err := darwinSnapshotSettings(network)
|
||||
if err != nil {
|
||||
return darwinSnapshot{}, err
|
||||
}
|
||||
value, err := unix.SysctlRaw(spath)
|
||||
if err != nil {
|
||||
return darwinSnapshot{}, err
|
||||
}
|
||||
return darwinSnapshot{
|
||||
createdAt: time.Now(),
|
||||
entries: parseDarwinSnapshot(value, itemSize),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func darwinSnapshotSettings(network string) (string, int, error) {
|
||||
itemSize := structSize
|
||||
switch network {
|
||||
case N.NetworkTCP:
|
||||
return "net.inet.tcp.pcblist_n", itemSize + darwinTCPExtraStructSize, nil
|
||||
case N.NetworkUDP:
|
||||
return "net.inet.udp.pcblist_n", itemSize, nil
|
||||
default:
|
||||
return "", 0, os.ErrInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func parseDarwinSnapshot(buf []byte, itemSize int) []darwinConnectionEntry {
|
||||
entries := make([]darwinConnectionEntry, 0, (len(buf)-darwinXinpgenSize)/itemSize)
|
||||
for i := darwinXinpgenSize; i+itemSize <= len(buf); i += itemSize {
|
||||
inp := i
|
||||
so := i + darwinXsocketOffset
|
||||
entry, ok := parseDarwinConnectionEntry(buf[inp:so], buf[so:so+structSize-darwinXsocketOffset])
|
||||
if ok {
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func parseDarwinConnectionEntry(inp []byte, so []byte) (darwinConnectionEntry, bool) {
|
||||
if len(inp) < darwinXsocketOffset || len(so) < structSize-darwinXsocketOffset {
|
||||
return darwinConnectionEntry{}, false
|
||||
}
|
||||
entry := darwinConnectionEntry{
|
||||
remotePort: binary.BigEndian.Uint16(inp[darwinXinpcbForeignPort : darwinXinpcbForeignPort+2]),
|
||||
localPort: binary.BigEndian.Uint16(inp[darwinXinpcbLocalPort : darwinXinpcbLocalPort+2]),
|
||||
pid: binary.NativeEndian.Uint32(so[darwinXsocketLastPID : darwinXsocketLastPID+4]),
|
||||
uid: int32(binary.NativeEndian.Uint32(so[darwinXsocketUID : darwinXsocketUID+4])),
|
||||
}
|
||||
flag := inp[darwinXinpcbVFlag]
|
||||
switch {
|
||||
case flag&0x1 != 0:
|
||||
entry.remoteAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr : darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr+4]))
|
||||
entry.localAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr : darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr+4]))
|
||||
return entry, true
|
||||
case flag&0x2 != 0:
|
||||
entry.remoteAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbForeignAddr : darwinXinpcbForeignAddr+16]))
|
||||
entry.localAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbLocalAddr : darwinXinpcbLocalAddr+16]))
|
||||
return entry, true
|
||||
default:
|
||||
return darwinConnectionEntry{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func matchDarwinConnectionEntry(entries []darwinConnectionEntry, network string, source netip.AddrPort, destination netip.AddrPort) (darwinConnectionEntry, darwinConnectionMatchKind, error) {
|
||||
sourceAddr := source.Addr()
|
||||
if !sourceAddr.IsValid() {
|
||||
return darwinConnectionEntry{}, darwinConnectionMatchExact, os.ErrInvalid
|
||||
}
|
||||
var localFallback darwinConnectionEntry
|
||||
var hasLocalFallback bool
|
||||
var wildcardFallback darwinConnectionEntry
|
||||
var hasWildcardFallback bool
|
||||
for _, entry := range entries {
|
||||
if entry.localPort != source.Port() || sourceAddr.BitLen() != entry.localAddr.BitLen() {
|
||||
continue
|
||||
}
|
||||
if entry.localAddr == sourceAddr && destination.IsValid() && entry.remotePort == destination.Port() && entry.remoteAddr == destination.Addr() {
|
||||
return entry, darwinConnectionMatchExact, nil
|
||||
}
|
||||
if !destination.IsValid() && entry.localAddr == sourceAddr {
|
||||
return entry, darwinConnectionMatchExact, nil
|
||||
}
|
||||
if network != N.NetworkUDP {
|
||||
continue
|
||||
}
|
||||
if !hasLocalFallback && entry.localAddr == sourceAddr {
|
||||
hasLocalFallback = true
|
||||
localFallback = entry
|
||||
}
|
||||
if !hasWildcardFallback && entry.localAddr.IsUnspecified() {
|
||||
hasWildcardFallback = true
|
||||
wildcardFallback = entry
|
||||
}
|
||||
}
|
||||
if hasLocalFallback {
|
||||
return localFallback, darwinConnectionMatchLocalFallback, nil
|
||||
}
|
||||
if hasWildcardFallback {
|
||||
return wildcardFallback, darwinConnectionMatchWildcardFallback, nil
|
||||
}
|
||||
return darwinConnectionEntry{}, darwinConnectionMatchExact, ErrNotFound
|
||||
}
|
||||
|
||||
func normalizeDarwinAddrPort(addrPort netip.AddrPort) netip.AddrPort {
|
||||
if !addrPort.IsValid() {
|
||||
return addrPort
|
||||
}
|
||||
return netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port())
|
||||
}
|
||||
|
||||
func getExecPathFromPID(pid uint32) (string, error) {
|
||||
const (
|
||||
procpidpathinfo = 0xb
|
||||
procpidpathinfosize = 1024
|
||||
proccallnumpidinfo = 0x2
|
||||
)
|
||||
buf := make([]byte, procpidpathinfosize)
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PROC_INFO,
|
||||
proccallnumpidinfo,
|
||||
uintptr(pid),
|
||||
procpidpathinfo,
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&buf[0])),
|
||||
procpidpathinfosize)
|
||||
if errno != 0 {
|
||||
return "", errno
|
||||
}
|
||||
return unix.ByteSliceToString(buf), nil
|
||||
}
|
||||
@@ -4,33 +4,82 @@ package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/netip"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ Searcher = (*linuxSearcher)(nil)
|
||||
|
||||
type linuxSearcher struct {
|
||||
logger log.ContextLogger
|
||||
logger log.ContextLogger
|
||||
diagConns [4]*socketDiagConn
|
||||
processPathCache *uidProcessPathCache
|
||||
}
|
||||
|
||||
func NewSearcher(config Config) (Searcher, error) {
|
||||
return &linuxSearcher{config.Logger}, nil
|
||||
searcher := &linuxSearcher{
|
||||
logger: config.Logger,
|
||||
processPathCache: newUIDProcessPathCache(time.Second),
|
||||
}
|
||||
for _, family := range []uint8{syscall.AF_INET, syscall.AF_INET6} {
|
||||
for _, protocol := range []uint8{syscall.IPPROTO_TCP, syscall.IPPROTO_UDP} {
|
||||
searcher.diagConns[socketDiagConnIndex(family, protocol)] = newSocketDiagConn(family, protocol)
|
||||
}
|
||||
}
|
||||
return searcher, nil
|
||||
}
|
||||
|
||||
func (s *linuxSearcher) Close() error {
|
||||
var errs []error
|
||||
for _, conn := range s.diagConns {
|
||||
if conn == nil {
|
||||
continue
|
||||
}
|
||||
errs = append(errs, conn.Close())
|
||||
}
|
||||
return E.Errors(errs...)
|
||||
}
|
||||
|
||||
func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
inode, uid, err := resolveSocketByNetlink(network, source, destination)
|
||||
inode, uid, err := s.resolveSocketByNetlink(network, source, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
processPath, err := resolveProcessNameByProcSearch(inode, uid)
|
||||
processInfo := &adapter.ConnectionOwner{
|
||||
UserId: int32(uid),
|
||||
}
|
||||
processPath, err := s.processPathCache.findProcessPath(inode, uid)
|
||||
if err != nil {
|
||||
s.logger.DebugContext(ctx, "find process path: ", err)
|
||||
} else {
|
||||
processInfo.ProcessPath = processPath
|
||||
}
|
||||
return &adapter.ConnectionOwner{
|
||||
UserId: int32(uid),
|
||||
ProcessPath: processPath,
|
||||
}, nil
|
||||
return processInfo, nil
|
||||
}
|
||||
|
||||
func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
||||
family, protocol, err := socketDiagSettings(network, source)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
conn := s.diagConns[socketDiagConnIndex(family, protocol)]
|
||||
if conn == nil {
|
||||
return 0, 0, E.New("missing socket diag connection for family=", family, " protocol=", protocol)
|
||||
}
|
||||
if destination.IsValid() && source.Addr().BitLen() == destination.Addr().BitLen() {
|
||||
inode, uid, err = conn.query(source, destination)
|
||||
if err == nil {
|
||||
return inode, uid, nil
|
||||
}
|
||||
if !errors.Is(err, ErrNotFound) {
|
||||
return 0, 0, err
|
||||
}
|
||||
}
|
||||
return querySocketDiagOnce(family, protocol, source)
|
||||
}
|
||||
|
||||
@@ -3,43 +3,67 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"errors"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unicode"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/contrab/freelru"
|
||||
"github.com/sagernet/sing/contrab/maphash"
|
||||
)
|
||||
|
||||
// from https://github.com/vishvananda/netlink/blob/bca67dfc8220b44ef582c9da4e9172bf1c9ec973/nl/nl_linux.go#L52-L62
|
||||
var nativeEndian = func() binary.ByteOrder {
|
||||
var x uint32 = 0x01020304
|
||||
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
|
||||
return binary.BigEndian
|
||||
}
|
||||
|
||||
return binary.LittleEndian
|
||||
}()
|
||||
|
||||
const (
|
||||
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48
|
||||
socketDiagByFamily = 20
|
||||
pathProc = "/proc"
|
||||
sizeOfSocketDiagRequestData = 56
|
||||
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + sizeOfSocketDiagRequestData
|
||||
socketDiagResponseMinSize = 72
|
||||
socketDiagByFamily = 20
|
||||
pathProc = "/proc"
|
||||
)
|
||||
|
||||
func resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
||||
var family uint8
|
||||
var protocol uint8
|
||||
type socketDiagConn struct {
|
||||
access sync.Mutex
|
||||
family uint8
|
||||
protocol uint8
|
||||
fd int
|
||||
}
|
||||
|
||||
type uidProcessPathCache struct {
|
||||
cache freelru.Cache[uint32, *uidProcessPaths]
|
||||
}
|
||||
|
||||
type uidProcessPaths struct {
|
||||
entries map[uint32]string
|
||||
}
|
||||
|
||||
func newSocketDiagConn(family, protocol uint8) *socketDiagConn {
|
||||
return &socketDiagConn{
|
||||
family: family,
|
||||
protocol: protocol,
|
||||
fd: -1,
|
||||
}
|
||||
}
|
||||
|
||||
func socketDiagConnIndex(family, protocol uint8) int {
|
||||
index := 0
|
||||
if protocol == syscall.IPPROTO_UDP {
|
||||
index += 2
|
||||
}
|
||||
if family == syscall.AF_INET6 {
|
||||
index++
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
func socketDiagSettings(network string, source netip.AddrPort) (family, protocol uint8, err error) {
|
||||
switch network {
|
||||
case N.NetworkTCP:
|
||||
protocol = syscall.IPPROTO_TCP
|
||||
@@ -48,151 +72,308 @@ func resolveSocketByNetlink(network string, source netip.AddrPort, destination n
|
||||
default:
|
||||
return 0, 0, os.ErrInvalid
|
||||
}
|
||||
|
||||
if source.Addr().Is4() {
|
||||
switch {
|
||||
case source.Addr().Is4():
|
||||
family = syscall.AF_INET
|
||||
} else {
|
||||
case source.Addr().Is6():
|
||||
family = syscall.AF_INET6
|
||||
default:
|
||||
return 0, 0, os.ErrInvalid
|
||||
}
|
||||
|
||||
req := packSocketDiagRequest(family, protocol, source)
|
||||
|
||||
socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG)
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "dial netlink")
|
||||
}
|
||||
defer syscall.Close(socket)
|
||||
|
||||
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 100})
|
||||
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 100})
|
||||
|
||||
err = syscall.Connect(socket, &syscall.SockaddrNetlink{
|
||||
Family: syscall.AF_NETLINK,
|
||||
Pad: 0,
|
||||
Pid: 0,
|
||||
Groups: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = syscall.Write(socket, req)
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "write netlink request")
|
||||
}
|
||||
|
||||
buffer := buf.New()
|
||||
defer buffer.Release()
|
||||
|
||||
n, err := syscall.Read(socket, buffer.FreeBytes())
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "read netlink response")
|
||||
}
|
||||
|
||||
buffer.Truncate(n)
|
||||
|
||||
messages, err := syscall.ParseNetlinkMessage(buffer.Bytes())
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "parse netlink message")
|
||||
} else if len(messages) == 0 {
|
||||
return 0, 0, E.New("unexcepted netlink response")
|
||||
}
|
||||
|
||||
message := messages[0]
|
||||
if message.Header.Type&syscall.NLMSG_ERROR != 0 {
|
||||
return 0, 0, E.New("netlink message: NLMSG_ERROR")
|
||||
}
|
||||
|
||||
inode, uid = unpackSocketDiagResponse(&messages[0])
|
||||
return
|
||||
return family, protocol, nil
|
||||
}
|
||||
|
||||
func packSocketDiagRequest(family, protocol byte, source netip.AddrPort) []byte {
|
||||
s := make([]byte, 16)
|
||||
copy(s, source.Addr().AsSlice())
|
||||
|
||||
buf := make([]byte, sizeOfSocketDiagRequest)
|
||||
|
||||
nativeEndian.PutUint32(buf[0:4], sizeOfSocketDiagRequest)
|
||||
nativeEndian.PutUint16(buf[4:6], socketDiagByFamily)
|
||||
nativeEndian.PutUint16(buf[6:8], syscall.NLM_F_REQUEST|syscall.NLM_F_DUMP)
|
||||
nativeEndian.PutUint32(buf[8:12], 0)
|
||||
nativeEndian.PutUint32(buf[12:16], 0)
|
||||
|
||||
buf[16] = family
|
||||
buf[17] = protocol
|
||||
buf[18] = 0
|
||||
buf[19] = 0
|
||||
nativeEndian.PutUint32(buf[20:24], 0xFFFFFFFF)
|
||||
|
||||
binary.BigEndian.PutUint16(buf[24:26], source.Port())
|
||||
binary.BigEndian.PutUint16(buf[26:28], 0)
|
||||
|
||||
copy(buf[28:44], s)
|
||||
copy(buf[44:60], net.IPv6zero)
|
||||
|
||||
nativeEndian.PutUint32(buf[60:64], 0)
|
||||
nativeEndian.PutUint64(buf[64:72], 0xFFFFFFFFFFFFFFFF)
|
||||
|
||||
return buf
|
||||
func newUIDProcessPathCache(ttl time.Duration) *uidProcessPathCache {
|
||||
cache := common.Must1(freelru.NewSharded[uint32, *uidProcessPaths](64, maphash.NewHasher[uint32]().Hash32))
|
||||
cache.SetLifetime(ttl)
|
||||
return &uidProcessPathCache{cache: cache}
|
||||
}
|
||||
|
||||
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
|
||||
if len(msg.Data) < 72 {
|
||||
return 0, 0
|
||||
func (c *uidProcessPathCache) findProcessPath(targetInode, uid uint32) (string, error) {
|
||||
if cached, ok := c.cache.Get(uid); ok {
|
||||
if processPath, found := cached.entries[targetInode]; found {
|
||||
return processPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
data := msg.Data
|
||||
|
||||
uid = nativeEndian.Uint32(data[64:68])
|
||||
inode = nativeEndian.Uint32(data[68:72])
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) {
|
||||
files, err := os.ReadDir(pathProc)
|
||||
processPaths, err := buildProcessPathByUIDCache(uid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
c.cache.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
|
||||
}
|
||||
|
||||
func (c *socketDiagConn) Close() error {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
return c.closeLocked()
|
||||
}
|
||||
|
||||
func (c *socketDiagConn) query(source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
request := packSocketDiagRequest(c.family, c.protocol, source, destination, false)
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
err = c.ensureOpenLocked()
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "dial netlink")
|
||||
}
|
||||
inode, uid, err = querySocketDiag(c.fd, request)
|
||||
if err == nil || errors.Is(err, ErrNotFound) {
|
||||
return inode, uid, err
|
||||
}
|
||||
if !shouldRetrySocketDiag(err) {
|
||||
return 0, 0, err
|
||||
}
|
||||
_ = c.closeLocked()
|
||||
}
|
||||
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
|
||||
}
|
||||
fd, err := openSocketDiag()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.fd = fd
|
||||
return nil
|
||||
}
|
||||
|
||||
func openSocketDiag() (int, error) {
|
||||
fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM|syscall.SOCK_CLOEXEC, syscall.NETLINK_INET_DIAG)
|
||||
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 {
|
||||
syscall.Close(fd)
|
||||
return -1, err
|
||||
}
|
||||
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, timeout); err != nil {
|
||||
syscall.Close(fd)
|
||||
return -1, err
|
||||
}
|
||||
if err = syscall.Connect(fd, &syscall.SockaddrNetlink{
|
||||
Family: syscall.AF_NETLINK,
|
||||
Pid: 0,
|
||||
Groups: 0,
|
||||
}); err != nil {
|
||||
syscall.Close(fd)
|
||||
return -1, err
|
||||
}
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
func (c *socketDiagConn) closeLocked() error {
|
||||
if c.fd == -1 {
|
||||
return nil
|
||||
}
|
||||
err := syscall.Close(c.fd)
|
||||
c.fd = -1
|
||||
return err
|
||||
}
|
||||
|
||||
func packSocketDiagRequest(family, protocol byte, source netip.AddrPort, destination netip.AddrPort, dump bool) []byte {
|
||||
request := make([]byte, sizeOfSocketDiagRequest)
|
||||
|
||||
binary.NativeEndian.PutUint32(request[0:4], sizeOfSocketDiagRequest)
|
||||
binary.NativeEndian.PutUint16(request[4:6], socketDiagByFamily)
|
||||
flags := uint16(syscall.NLM_F_REQUEST)
|
||||
if dump {
|
||||
flags |= syscall.NLM_F_DUMP
|
||||
}
|
||||
binary.NativeEndian.PutUint16(request[6:8], flags)
|
||||
binary.NativeEndian.PutUint32(request[8:12], 0)
|
||||
binary.NativeEndian.PutUint32(request[12:16], 0)
|
||||
|
||||
request[16] = family
|
||||
request[17] = protocol
|
||||
request[18] = 0
|
||||
request[19] = 0
|
||||
if dump {
|
||||
binary.NativeEndian.PutUint32(request[20:24], 0xFFFFFFFF)
|
||||
}
|
||||
requestSource := source
|
||||
requestDestination := destination
|
||||
if protocol == syscall.IPPROTO_UDP && !dump && destination.IsValid() {
|
||||
// udp_dump_one expects the exact-match endpoints reversed for historical reasons.
|
||||
requestSource, requestDestination = destination, source
|
||||
}
|
||||
binary.BigEndian.PutUint16(request[24:26], requestSource.Port())
|
||||
binary.BigEndian.PutUint16(request[26:28], requestDestination.Port())
|
||||
if family == syscall.AF_INET6 {
|
||||
copy(request[28:44], requestSource.Addr().AsSlice())
|
||||
if requestDestination.IsValid() {
|
||||
copy(request[44:60], requestDestination.Addr().AsSlice())
|
||||
}
|
||||
} else {
|
||||
copy(request[28:32], requestSource.Addr().AsSlice())
|
||||
if requestDestination.IsValid() {
|
||||
copy(request[44:48], requestDestination.Addr().AsSlice())
|
||||
}
|
||||
}
|
||||
binary.NativeEndian.PutUint32(request[60:64], 0)
|
||||
binary.NativeEndian.PutUint64(request[64:72], 0xFFFFFFFFFFFFFFFF)
|
||||
return request
|
||||
}
|
||||
|
||||
func querySocketDiag(fd int, request []byte) (inode, uid uint32, err error) {
|
||||
_, err = syscall.Write(fd, request)
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "write netlink request")
|
||||
}
|
||||
buffer := make([]byte, 64<<10)
|
||||
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")
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, 0, ErrNotFound
|
||||
}
|
||||
|
||||
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
|
||||
if len(msg.Data) < socketDiagResponseMinSize {
|
||||
return 0, 0
|
||||
}
|
||||
uid = binary.NativeEndian.Uint32(msg.Data[64:68])
|
||||
inode = binary.NativeEndian.Uint32(msg.Data[68:72])
|
||||
return inode, uid
|
||||
}
|
||||
|
||||
func unpackSocketDiagError(msg *syscall.NetlinkMessage) error {
|
||||
if len(msg.Data) < 4 {
|
||||
return E.New("netlink message: NLMSG_ERROR")
|
||||
}
|
||||
errno := int32(binary.NativeEndian.Uint32(msg.Data[:4]))
|
||||
if errno == 0 {
|
||||
return nil
|
||||
}
|
||||
if errno < 0 {
|
||||
errno = -errno
|
||||
}
|
||||
sysErr := syscall.Errno(errno)
|
||||
switch sysErr {
|
||||
case syscall.ENOENT, syscall.ESRCH:
|
||||
return ErrNotFound
|
||||
default:
|
||||
return E.New("netlink message: ", sysErr)
|
||||
}
|
||||
}
|
||||
|
||||
func shouldRetrySocketDiag(err error) bool {
|
||||
return err != nil && !errors.Is(err, ErrNotFound)
|
||||
}
|
||||
|
||||
func buildProcessPathByUIDCache(uid uint32) (map[uint32]string, error) {
|
||||
files, err := os.ReadDir(pathProc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buffer := make([]byte, syscall.PathMax)
|
||||
socket := []byte(fmt.Sprintf("socket:[%d]", inode))
|
||||
|
||||
for _, f := range files {
|
||||
if !f.IsDir() || !isPid(f.Name()) {
|
||||
processPaths := make(map[uint32]string)
|
||||
for _, file := range files {
|
||||
if !file.IsDir() || !isPid(file.Name()) {
|
||||
continue
|
||||
}
|
||||
|
||||
info, err := f.Info()
|
||||
info, err := file.Info()
|
||||
if err != nil {
|
||||
return "", err
|
||||
if isIgnorableProcError(err) {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if info.Sys().(*syscall.Stat_t).Uid != uid {
|
||||
continue
|
||||
}
|
||||
|
||||
processPath := path.Join(pathProc, f.Name())
|
||||
fdPath := path.Join(processPath, "fd")
|
||||
|
||||
processPath := filepath.Join(pathProc, file.Name())
|
||||
fdPath := filepath.Join(processPath, "fd")
|
||||
exePath, err := os.Readlink(filepath.Join(processPath, "exe"))
|
||||
if err != nil {
|
||||
if isIgnorableProcError(err) {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
fds, err := os.ReadDir(fdPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, fd := range fds {
|
||||
n, err := syscall.Readlink(path.Join(fdPath, fd.Name()), buffer)
|
||||
n, err := syscall.Readlink(filepath.Join(fdPath, fd.Name()), buffer)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if bytes.Equal(buffer[:n], socket) {
|
||||
return os.Readlink(path.Join(processPath, "exe"))
|
||||
inode, ok := parseSocketInode(buffer[:n])
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if _, loaded := processPaths[inode]; !loaded {
|
||||
processPaths[inode] = exePath
|
||||
}
|
||||
}
|
||||
}
|
||||
return processPaths, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode)
|
||||
func isIgnorableProcError(err error) bool {
|
||||
return os.IsNotExist(err) || os.IsPermission(err)
|
||||
}
|
||||
|
||||
func parseSocketInode(link []byte) (uint32, bool) {
|
||||
const socketPrefix = "socket:["
|
||||
if len(link) <= len(socketPrefix) || string(link[:len(socketPrefix)]) != socketPrefix || link[len(link)-1] != ']' {
|
||||
return 0, false
|
||||
}
|
||||
var inode uint64
|
||||
for _, char := range link[len(socketPrefix) : len(link)-1] {
|
||||
if char < '0' || char > '9' {
|
||||
return 0, false
|
||||
}
|
||||
inode = inode*10 + uint64(char-'0')
|
||||
if inode > uint64(^uint32(0)) {
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
return uint32(inode), true
|
||||
}
|
||||
|
||||
func isPid(s string) bool {
|
||||
|
||||
60
common/process/searcher_linux_shared_test.go
Normal file
60
common/process/searcher_linux_shared_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
//go:build linux
|
||||
|
||||
package process
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestQuerySocketDiagUDPExact(t *testing.T) {
|
||||
t.Parallel()
|
||||
server, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||
require.NoError(t, err)
|
||||
defer server.Close()
|
||||
|
||||
client, err := net.DialUDP("udp4", nil, server.LocalAddr().(*net.UDPAddr))
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
err = client.SetDeadline(time.Now().Add(time.Second))
|
||||
require.NoError(t, err)
|
||||
_, err = client.Write([]byte{0})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = server.SetReadDeadline(time.Now().Add(time.Second))
|
||||
require.NoError(t, err)
|
||||
buffer := make([]byte, 1)
|
||||
_, _, err = server.ReadFromUDP(buffer)
|
||||
require.NoError(t, err)
|
||||
|
||||
source := addrPortFromUDPAddr(t, client.LocalAddr())
|
||||
destination := addrPortFromUDPAddr(t, client.RemoteAddr())
|
||||
|
||||
fd, err := openSocketDiag()
|
||||
require.NoError(t, err)
|
||||
defer syscall.Close(fd)
|
||||
|
||||
inode, uid, err := querySocketDiag(fd, packSocketDiagRequest(syscall.AF_INET, syscall.IPPROTO_UDP, source, destination, false))
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, inode)
|
||||
require.EqualValues(t, os.Getuid(), uid)
|
||||
}
|
||||
|
||||
func addrPortFromUDPAddr(t *testing.T, addr net.Addr) netip.AddrPort {
|
||||
t.Helper()
|
||||
|
||||
udpAddr, ok := addr.(*net.UDPAddr)
|
||||
require.True(t, ok)
|
||||
|
||||
ip, ok := netip.AddrFromSlice(udpAddr.IP)
|
||||
require.True(t, ok)
|
||||
|
||||
return netip.AddrPortFrom(ip.Unmap(), uint16(udpAddr.Port))
|
||||
}
|
||||
@@ -28,6 +28,10 @@ func initWin32API() error {
|
||||
return winiphlpapi.LoadExtendedTable()
|
||||
}
|
||||
|
||||
func (s *windowsSearcher) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
pid, err := winiphlpapi.FindPid(network, source)
|
||||
if err != nil {
|
||||
|
||||
@@ -168,7 +168,7 @@ func (s *StartedService) waitForStarted(ctx context.Context) error {
|
||||
func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error {
|
||||
s.serviceAccess.Lock()
|
||||
switch s.serviceStatus.Status {
|
||||
case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING:
|
||||
case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING, ServiceStatus_FATAL:
|
||||
default:
|
||||
s.serviceAccess.Unlock()
|
||||
return os.ErrInvalid
|
||||
@@ -226,13 +226,14 @@ func (s *StartedService) CloseService() error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
s.updateStatus(ServiceStatus_STOPPING)
|
||||
if s.instance != nil {
|
||||
err := s.instance.Close()
|
||||
instance := s.instance
|
||||
s.instance = nil
|
||||
if instance != nil {
|
||||
err := instance.Close()
|
||||
if err != nil {
|
||||
return s.updateStatusError(err)
|
||||
}
|
||||
}
|
||||
s.instance = nil
|
||||
s.startedAt = time.Time{}
|
||||
s.updateStatus(ServiceStatus_IDLE)
|
||||
s.serviceAccess.Unlock()
|
||||
@@ -949,11 +950,11 @@ func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection {
|
||||
var processInfo *ProcessInfo
|
||||
if metadata.Metadata.ProcessInfo != nil {
|
||||
processInfo = &ProcessInfo{
|
||||
ProcessId: metadata.Metadata.ProcessInfo.ProcessID,
|
||||
UserId: metadata.Metadata.ProcessInfo.UserId,
|
||||
UserName: metadata.Metadata.ProcessInfo.UserName,
|
||||
ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath,
|
||||
PackageName: metadata.Metadata.ProcessInfo.AndroidPackageName,
|
||||
ProcessId: metadata.Metadata.ProcessInfo.ProcessID,
|
||||
UserId: metadata.Metadata.ProcessInfo.UserId,
|
||||
UserName: metadata.Metadata.ProcessInfo.UserName,
|
||||
ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath,
|
||||
PackageNames: metadata.Metadata.ProcessInfo.AndroidPackageNames,
|
||||
}
|
||||
}
|
||||
return &Connection{
|
||||
|
||||
@@ -1460,7 +1460,7 @@ type ProcessInfo struct {
|
||||
UserId int32 `protobuf:"varint,2,opt,name=userId,proto3" json:"userId,omitempty"`
|
||||
UserName string `protobuf:"bytes,3,opt,name=userName,proto3" json:"userName,omitempty"`
|
||||
ProcessPath string `protobuf:"bytes,4,opt,name=processPath,proto3" json:"processPath,omitempty"`
|
||||
PackageName string `protobuf:"bytes,5,opt,name=packageName,proto3" json:"packageName,omitempty"`
|
||||
PackageNames []string `protobuf:"bytes,5,rep,name=packageNames,proto3" json:"packageNames,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -1523,11 +1523,11 @@ func (x *ProcessInfo) GetProcessPath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ProcessInfo) GetPackageName() string {
|
||||
func (x *ProcessInfo) GetPackageNames() []string {
|
||||
if x != nil {
|
||||
return x.PackageName
|
||||
return x.PackageNames
|
||||
}
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
type CloseConnectionRequest struct {
|
||||
@@ -1884,13 +1884,13 @@ const file_daemon_started_service_proto_rawDesc = "" +
|
||||
"\boutbound\x18\x13 \x01(\tR\boutbound\x12\"\n" +
|
||||
"\foutboundType\x18\x14 \x01(\tR\foutboundType\x12\x1c\n" +
|
||||
"\tchainList\x18\x15 \x03(\tR\tchainList\x125\n" +
|
||||
"\vprocessInfo\x18\x16 \x01(\v2\x13.daemon.ProcessInfoR\vprocessInfo\"\xa3\x01\n" +
|
||||
"\vprocessInfo\x18\x16 \x01(\v2\x13.daemon.ProcessInfoR\vprocessInfo\"\xa5\x01\n" +
|
||||
"\vProcessInfo\x12\x1c\n" +
|
||||
"\tprocessId\x18\x01 \x01(\rR\tprocessId\x12\x16\n" +
|
||||
"\x06userId\x18\x02 \x01(\x05R\x06userId\x12\x1a\n" +
|
||||
"\buserName\x18\x03 \x01(\tR\buserName\x12 \n" +
|
||||
"\vprocessPath\x18\x04 \x01(\tR\vprocessPath\x12 \n" +
|
||||
"\vpackageName\x18\x05 \x01(\tR\vpackageName\"(\n" +
|
||||
"\vprocessPath\x18\x04 \x01(\tR\vprocessPath\x12\"\n" +
|
||||
"\fpackageNames\x18\x05 \x03(\tR\fpackageNames\"(\n" +
|
||||
"\x16CloseConnectionRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\"K\n" +
|
||||
"\x12DeprecatedWarnings\x125\n" +
|
||||
|
||||
@@ -195,7 +195,7 @@ message ProcessInfo {
|
||||
int32 userId = 2;
|
||||
string userName = 3;
|
||||
string processPath = 4;
|
||||
string packageName = 5;
|
||||
repeated string packageNames = 5;
|
||||
}
|
||||
|
||||
message CloseConnectionRequest {
|
||||
|
||||
@@ -283,6 +283,9 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
if timeToLive == 0 {
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
|
||||
timeToLive = record.Header().Ttl
|
||||
}
|
||||
@@ -294,6 +297,9 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
}
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
record.Header().Ttl = timeToLive
|
||||
}
|
||||
}
|
||||
@@ -381,21 +387,21 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
|
||||
}
|
||||
if c.disableExpire {
|
||||
if !c.independentCache {
|
||||
c.cache.Add(question, message)
|
||||
c.cache.Add(question, message.Copy())
|
||||
} else {
|
||||
c.transportCache.Add(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
}, message)
|
||||
}, message.Copy())
|
||||
}
|
||||
} else {
|
||||
if !c.independentCache {
|
||||
c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive))
|
||||
c.cache.AddWithLifetime(question, message.Copy(), time.Second*time.Duration(timeToLive))
|
||||
} else {
|
||||
c.transportCache.AddWithLifetime(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
}, message, time.Second*time.Duration(timeToLive))
|
||||
}, message.Copy(), time.Second*time.Duration(timeToLive))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -486,6 +492,9 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
|
||||
var originTTL int
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL {
|
||||
originTTL = int(record.Header().Ttl)
|
||||
}
|
||||
@@ -500,12 +509,18 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
|
||||
duration := uint32(originTTL - nowTTL)
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
record.Header().Ttl = record.Header().Ttl - duration
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
record.Header().Ttl = uint32(nowTTL)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
@@ -40,13 +39,6 @@ func (t *Transport) exchangeParallel(ctx context.Context, servers []M.Socksaddr,
|
||||
results := make(chan queryResult)
|
||||
startRacer := func(ctx context.Context, fqdn string) {
|
||||
response, err := t.tryOneName(ctx, servers, fqdn, message)
|
||||
if err == nil {
|
||||
if response.Rcode != mDNS.RcodeSuccess {
|
||||
err = dns.RcodeError(response.Rcode)
|
||||
} else if len(dns.MessageToAddresses(response)) == 0 {
|
||||
err = dns.RcodeSuccess
|
||||
}
|
||||
}
|
||||
select {
|
||||
case results <- queryResult{response, err}:
|
||||
case <-returned:
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
@@ -49,13 +48,6 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
|
||||
results := make(chan queryResult)
|
||||
startRacer := func(ctx context.Context, fqdn string) {
|
||||
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
|
||||
if err == nil {
|
||||
if response.Rcode != mDNS.RcodeSuccess {
|
||||
err = dns.RcodeError(response.Rcode)
|
||||
} else if len(dns.MessageToAddresses(response)) == 0 {
|
||||
err = E.New(fqdn, ": empty result")
|
||||
}
|
||||
}
|
||||
select {
|
||||
case results <- queryResult{response, err}:
|
||||
case <-returned:
|
||||
|
||||
@@ -2,6 +2,22 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.13.7
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.6
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.5
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.4
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.3
|
||||
|
||||
* Add OpenWrt and Alpine APK packages to release **1**
|
||||
|
||||
@@ -209,7 +209,7 @@ icon: material/alert-decagram
|
||||
(`source_port` || `source_port_range`) &&
|
||||
`other fields`
|
||||
|
||||
Additionally, included rule-sets can be considered merged rather than as a single rule sub-item.
|
||||
Additionally, each branch inside an included rule-set can be considered merged into the outer rule, while different branches keep OR semantics.
|
||||
|
||||
#### inbound
|
||||
|
||||
@@ -546,4 +546,4 @@ Match any IP with query response.
|
||||
|
||||
#### rules
|
||||
|
||||
Included rules.
|
||||
Included rules.
|
||||
|
||||
@@ -208,7 +208,7 @@ icon: material/alert-decagram
|
||||
(`source_port` || `source_port_range`) &&
|
||||
`other fields`
|
||||
|
||||
另外,引用的规则集可视为被合并,而不是作为一个单独的规则子项。
|
||||
另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义。
|
||||
|
||||
#### inbound
|
||||
|
||||
@@ -550,4 +550,4 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
==必填==
|
||||
|
||||
包括的规则。
|
||||
包括的规则。
|
||||
|
||||
@@ -199,7 +199,7 @@ icon: material/new-box
|
||||
(`source_port` || `source_port_range`) &&
|
||||
`other fields`
|
||||
|
||||
Additionally, included rule-sets can be considered merged rather than as a single rule sub-item.
|
||||
Additionally, each branch inside an included rule-set can be considered merged into the outer rule, while different branches keep OR semantics.
|
||||
|
||||
#### inbound
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ icon: material/new-box
|
||||
(`source_port` || `source_port_range`) &&
|
||||
`other fields`
|
||||
|
||||
另外,引用的规则集可视为被合并,而不是作为一个单独的规则子项。
|
||||
另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义。
|
||||
|
||||
#### inbound
|
||||
|
||||
@@ -501,4 +501,4 @@ icon: material/new-box
|
||||
|
||||
==必填==
|
||||
|
||||
包括的规则。
|
||||
包括的规则。
|
||||
|
||||
@@ -45,8 +45,8 @@ func (t TrackerMetadata) MarshalJSON() ([]byte, error) {
|
||||
if t.Metadata.ProcessInfo != nil {
|
||||
if t.Metadata.ProcessInfo.ProcessPath != "" {
|
||||
processPath = t.Metadata.ProcessInfo.ProcessPath
|
||||
} else if t.Metadata.ProcessInfo.AndroidPackageName != "" {
|
||||
processPath = t.Metadata.ProcessInfo.AndroidPackageName
|
||||
} else if len(t.Metadata.ProcessInfo.AndroidPackageNames) > 0 {
|
||||
processPath = t.Metadata.ProcessInfo.AndroidPackageNames[0]
|
||||
}
|
||||
if processPath == "" {
|
||||
if t.Metadata.ProcessInfo.UserId != -1 {
|
||||
|
||||
@@ -239,11 +239,15 @@ func (c *Connections) Iterator() ConnectionIterator {
|
||||
}
|
||||
|
||||
type ProcessInfo struct {
|
||||
ProcessID int64
|
||||
UserID int32
|
||||
UserName string
|
||||
ProcessPath string
|
||||
PackageName string
|
||||
ProcessID int64
|
||||
UserID int32
|
||||
UserName string
|
||||
ProcessPath string
|
||||
packageNames []string
|
||||
}
|
||||
|
||||
func (p *ProcessInfo) PackageNames() StringIterator {
|
||||
return newIterator(p.packageNames)
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
@@ -339,11 +343,11 @@ func connectionFromGRPC(conn *daemon.Connection) Connection {
|
||||
var processInfo *ProcessInfo
|
||||
if conn.ProcessInfo != nil {
|
||||
processInfo = &ProcessInfo{
|
||||
ProcessID: int64(conn.ProcessInfo.ProcessId),
|
||||
UserID: conn.ProcessInfo.UserId,
|
||||
UserName: conn.ProcessInfo.UserName,
|
||||
ProcessPath: conn.ProcessInfo.ProcessPath,
|
||||
PackageName: conn.ProcessInfo.PackageName,
|
||||
ProcessID: int64(conn.ProcessInfo.ProcessId),
|
||||
UserID: conn.ProcessInfo.UserId,
|
||||
UserName: conn.ProcessInfo.UserName,
|
||||
ProcessPath: conn.ProcessInfo.ProcessPath,
|
||||
packageNames: conn.ProcessInfo.PackageNames,
|
||||
}
|
||||
}
|
||||
return Connection{
|
||||
|
||||
57
experimental/libbox/connection_owner_darwin.go
Normal file
57
experimental/libbox/connection_owner_darwin.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"os/user"
|
||||
"syscall"
|
||||
|
||||
"github.com/sagernet/sing-box/common/process"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
func FindConnectionOwner(ipProtocol int32, sourceAddress string, sourcePort int32, destinationAddress string, destinationPort int32) (*ConnectionOwner, error) {
|
||||
source, err := parseConnectionOwnerAddrPort(sourceAddress, sourcePort)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse source")
|
||||
}
|
||||
destination, err := parseConnectionOwnerAddrPort(destinationAddress, destinationPort)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse destination")
|
||||
}
|
||||
var network string
|
||||
switch ipProtocol {
|
||||
case syscall.IPPROTO_TCP:
|
||||
network = "tcp"
|
||||
case syscall.IPPROTO_UDP:
|
||||
network = "udp"
|
||||
default:
|
||||
return nil, E.New("unknown protocol: ", ipProtocol)
|
||||
}
|
||||
owner, err := process.FindDarwinConnectionOwner(network, source, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &ConnectionOwner{
|
||||
UserId: owner.UserId,
|
||||
ProcessPath: owner.ProcessPath,
|
||||
}
|
||||
if owner.UserId != -1 && owner.UserName == "" {
|
||||
osUser, _ := user.LookupId(F.ToString(owner.UserId))
|
||||
if osUser != nil {
|
||||
result.UserName = osUser.Username
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseConnectionOwnerAddrPort(address string, port int32) (netip.AddrPort, error) {
|
||||
if port < 0 || port > 65535 {
|
||||
return netip.AddrPort{}, E.New("invalid port: ", port)
|
||||
}
|
||||
addr, err := netip.ParseAddr(address)
|
||||
if err != nil {
|
||||
return netip.AddrPort{}, err
|
||||
}
|
||||
return netip.AddrPortFrom(addr.Unmap(), uint16(port)), nil
|
||||
}
|
||||
@@ -52,6 +52,11 @@ type HTTPRequest interface {
|
||||
type HTTPResponse interface {
|
||||
GetContent() (*StringBox, error)
|
||||
WriteTo(path string) error
|
||||
WriteToWithProgress(path string, handler HTTPResponseWriteToProgressHandler) error
|
||||
}
|
||||
|
||||
type HTTPResponseWriteToProgressHandler interface {
|
||||
Update(progress int64, total int64)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -239,3 +244,31 @@ func (h *httpResponse) WriteTo(path string) error {
|
||||
defer file.Close()
|
||||
return common.Error(bufio.Copy(file, h.Body))
|
||||
}
|
||||
|
||||
func (h *httpResponse) WriteToWithProgress(path string, handler HTTPResponseWriteToProgressHandler) error {
|
||||
defer h.Body.Close()
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
return common.Error(bufio.Copy(&progressWriter{
|
||||
writer: file,
|
||||
handler: handler,
|
||||
total: h.ContentLength,
|
||||
}, h.Body))
|
||||
}
|
||||
|
||||
type progressWriter struct {
|
||||
writer io.Writer
|
||||
handler HTTPResponseWriteToProgressHandler
|
||||
total int64
|
||||
written int64
|
||||
}
|
||||
|
||||
func (w *progressWriter) Write(p []byte) (int, error) {
|
||||
n, err := w.writer.Write(p)
|
||||
w.written += int64(n)
|
||||
w.handler.Update(w.written, w.total)
|
||||
return n, err
|
||||
}
|
||||
|
||||
@@ -24,10 +24,18 @@ type PlatformInterface interface {
|
||||
}
|
||||
|
||||
type ConnectionOwner struct {
|
||||
UserId int32
|
||||
UserName string
|
||||
ProcessPath string
|
||||
AndroidPackageName string
|
||||
UserId int32
|
||||
UserName string
|
||||
ProcessPath string
|
||||
androidPackageNames []string
|
||||
}
|
||||
|
||||
func (c *ConnectionOwner) SetAndroidPackageNames(names StringIterator) {
|
||||
c.androidPackageNames = iteratorToArray[string](names)
|
||||
}
|
||||
|
||||
func (c *ConnectionOwner) AndroidPackageNames() StringIterator {
|
||||
return newIterator(c.androidPackageNames)
|
||||
}
|
||||
|
||||
type InterfaceUpdateListener interface {
|
||||
|
||||
@@ -201,10 +201,10 @@ func (w *platformInterfaceWrapper) FindConnectionOwner(request *adapter.FindConn
|
||||
return nil, err
|
||||
}
|
||||
return &adapter.ConnectionOwner{
|
||||
UserId: result.UserId,
|
||||
UserName: result.UserName,
|
||||
ProcessPath: result.ProcessPath,
|
||||
AndroidPackageName: result.AndroidPackageName,
|
||||
UserId: result.UserId,
|
||||
UserName: result.UserName,
|
||||
ProcessPath: result.ProcessPath,
|
||||
AndroidPackageNames: result.androidPackageNames,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
8
go.mod
8
go.mod
@@ -33,16 +33,16 @@ require (
|
||||
github.com/sagernet/gomobile v0.1.12
|
||||
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1
|
||||
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4
|
||||
github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde
|
||||
github.com/sagernet/sing v0.8.4
|
||||
github.com/sagernet/sing-mux v0.3.4
|
||||
github.com/sagernet/sing-quic v0.6.0
|
||||
github.com/sagernet/sing-quic v0.6.1
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
|
||||
github.com/sagernet/sing-tun v0.8.3
|
||||
github.com/sagernet/sing-tun v0.8.7
|
||||
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1
|
||||
github.com/sagernet/smux v1.5.50-sing-box-mod.1
|
||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e
|
||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7
|
||||
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c
|
||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
||||
github.com/spf13/cobra v1.10.2
|
||||
|
||||
16
go.sum
16
go.sum
@@ -236,26 +236,26 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen
|
||||
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
|
||||
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
|
||||
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
|
||||
github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde h1:RNQzlpnsXIuu1HGts/fIzJ1PR7RhrzaNlU52MDyiX1c=
|
||||
github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing v0.8.4 h1:Fj+jlY3F8vhcRfz/G/P3Dwcs5wqnmyNPT7u1RVVmjFI=
|
||||
github.com/sagernet/sing v0.8.4/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
|
||||
github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
|
||||
github.com/sagernet/sing-quic v0.6.0 h1:dhrFnP45wgVKEOT1EvtsToxdzRnHIDIAgj6WHV9pLyM=
|
||||
github.com/sagernet/sing-quic v0.6.0/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8=
|
||||
github.com/sagernet/sing-quic v0.6.1 h1:lx0tcm99wIA1RkyvILNzRSsMy1k7TTQYIhx71E/WBlw=
|
||||
github.com/sagernet/sing-quic v0.6.1/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
|
||||
github.com/sagernet/sing-tun v0.8.3 h1:mozxmuIoRhFdVHnheenLpBaammVj7bZPcnkApaYKDPY=
|
||||
github.com/sagernet/sing-tun v0.8.3/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs=
|
||||
github.com/sagernet/sing-tun v0.8.7 h1:q49cI7Cbp+BcgzaJitQ9QdLO77BqnnaQRkSEMoGmF3g=
|
||||
github.com/sagernet/sing-tun v0.8.7/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs=
|
||||
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o=
|
||||
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY=
|
||||
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
|
||||
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
|
||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e h1:Sv1qUhJIidjSTc24XEknovDZnbmVSlAXj8wNVgIfgGo=
|
||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc=
|
||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 h1:8zc1Aph1+ElqF9/7aSPkO0o4vTd+AfQC+CO324mLWGg=
|
||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc=
|
||||
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c h1:f9cXNB+IOOPnR8DOLMTpr42jf7naxh5Un5Y09BBf5Cg=
|
||||
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0=
|
||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
|
||||
|
||||
@@ -44,6 +44,12 @@ func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) erro
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if listenWrapper, isListen := options.(ListenOptionsWrapper); isListen {
|
||||
//nolint:staticcheck
|
||||
if listenWrapper.TakeListenOptions().InboundOptions != (InboundOptions{}) {
|
||||
return E.New("legacy inbound fields are deprecated in sing-box 1.11.0 and removed in sing-box 1.13.0, checkout migration: https://sing-box.sagernet.org/migration/#migrate-legacy-inbound-fields-to-rule-actions")
|
||||
}
|
||||
}
|
||||
h.Options = options
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -29,7 +29,10 @@ import (
|
||||
"golang.org/x/net/http2/h2c"
|
||||
)
|
||||
|
||||
var ConfigureHTTP3ListenerFunc func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.NaiveInboundOptions) (io.Closer, error)
|
||||
var (
|
||||
ConfigureHTTP3ListenerFunc func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.NaiveInboundOptions) (io.Closer, error)
|
||||
WrapError func(error) error
|
||||
)
|
||||
|
||||
func RegisterInbound(registry *inbound.Registry) {
|
||||
inbound.Register[option.NaiveInboundOptions](registry, C.TypeNaive, NewInbound)
|
||||
|
||||
@@ -95,7 +95,7 @@ func (p *paddingConn) writeWithPadding(writer io.Writer, data []byte) (n int, er
|
||||
binary.BigEndian.PutUint16(header, uint16(len(data)))
|
||||
header[2] = byte(paddingSize)
|
||||
common.Must1(buffer.Write(data))
|
||||
buffer.Extend(paddingSize)
|
||||
common.Must(buffer.WriteZeroN(paddingSize))
|
||||
_, err = writer.Write(buffer.Bytes())
|
||||
if err == nil {
|
||||
n = len(data)
|
||||
@@ -117,7 +117,7 @@ func (p *paddingConn) writeBufferWithPadding(writer io.Writer, buffer *buf.Buffe
|
||||
header := buffer.ExtendHeader(3)
|
||||
binary.BigEndian.PutUint16(header, uint16(bufferLen))
|
||||
header[2] = byte(paddingSize)
|
||||
buffer.Extend(paddingSize)
|
||||
common.Must(buffer.WriteZeroN(paddingSize))
|
||||
p.writePadding++
|
||||
}
|
||||
return common.Error(writer.Write(buffer.Bytes()))
|
||||
@@ -179,18 +179,18 @@ type naiveConn struct {
|
||||
|
||||
func (c *naiveConn) Read(p []byte) (n int, err error) {
|
||||
n, err = c.readWithPadding(c.Conn, p)
|
||||
return n, baderror.WrapH2(err)
|
||||
return n, wrapError(err)
|
||||
}
|
||||
|
||||
func (c *naiveConn) Write(p []byte) (n int, err error) {
|
||||
n, err = c.writeChunked(c.Conn, p)
|
||||
return n, baderror.WrapH2(err)
|
||||
return n, wrapError(err)
|
||||
}
|
||||
|
||||
func (c *naiveConn) WriteBuffer(buffer *buf.Buffer) error {
|
||||
defer buffer.Release()
|
||||
err := c.writeBufferWithPadding(c.Conn, buffer)
|
||||
return baderror.WrapH2(err)
|
||||
return wrapError(err)
|
||||
}
|
||||
|
||||
func (c *naiveConn) FrontHeadroom() int { return c.frontHeadroom() }
|
||||
@@ -210,7 +210,7 @@ type naiveH2Conn struct {
|
||||
|
||||
func (c *naiveH2Conn) Read(p []byte) (n int, err error) {
|
||||
n, err = c.readWithPadding(c.reader, p)
|
||||
return n, baderror.WrapH2(err)
|
||||
return n, wrapError(err)
|
||||
}
|
||||
|
||||
func (c *naiveH2Conn) Write(p []byte) (n int, err error) {
|
||||
@@ -218,7 +218,7 @@ func (c *naiveH2Conn) Write(p []byte) (n int, err error) {
|
||||
if err == nil {
|
||||
c.flusher.Flush()
|
||||
}
|
||||
return n, baderror.WrapH2(err)
|
||||
return n, wrapError(err)
|
||||
}
|
||||
|
||||
func (c *naiveH2Conn) WriteBuffer(buffer *buf.Buffer) error {
|
||||
@@ -227,7 +227,15 @@ func (c *naiveH2Conn) WriteBuffer(buffer *buf.Buffer) error {
|
||||
if err == nil {
|
||||
c.flusher.Flush()
|
||||
}
|
||||
return baderror.WrapH2(err)
|
||||
return wrapError(err)
|
||||
}
|
||||
|
||||
func wrapError(err error) error {
|
||||
err = baderror.WrapH2(err)
|
||||
if WrapError != nil {
|
||||
err = WrapError(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *naiveH2Conn) Close() error {
|
||||
|
||||
@@ -124,4 +124,5 @@ func init() {
|
||||
|
||||
return quicListener, nil
|
||||
}
|
||||
naive.WrapError = qtls.WrapError
|
||||
}
|
||||
|
||||
@@ -262,9 +262,16 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
}
|
||||
|
||||
func (t *Endpoint) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
switch stage {
|
||||
case adapter.StartStateStart:
|
||||
return t.start()
|
||||
case adapter.StartStatePostStart:
|
||||
return t.postStart()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Endpoint) start() error {
|
||||
if t.platformInterface != nil {
|
||||
err := t.network.UpdateInterfaces()
|
||||
if err != nil {
|
||||
@@ -347,6 +354,10 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
|
||||
})
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Endpoint) postStart() error {
|
||||
err := t.server.Start()
|
||||
if err != nil {
|
||||
if t.systemTun != nil {
|
||||
@@ -471,13 +482,13 @@ func (t *Endpoint) watchState() {
|
||||
}
|
||||
|
||||
func (t *Endpoint) Close() error {
|
||||
err := common.Close(common.PtrOrNil(t.server))
|
||||
netmon.RegisterInterfaceGetter(nil)
|
||||
netns.SetControlFunc(nil)
|
||||
if t.fallbackTCPCloser != nil {
|
||||
t.fallbackTCPCloser()
|
||||
t.fallbackTCPCloser = nil
|
||||
}
|
||||
err := common.Close(common.PtrOrNil(t.server))
|
||||
if t.systemTun != nil {
|
||||
t.systemTun.Close()
|
||||
t.systemTun = nil
|
||||
|
||||
@@ -67,6 +67,10 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
if options.GSO {
|
||||
return nil, E.New("GSO option in tun is deprecated in sing-box 1.11.0 and removed in sing-box 1.12.0")
|
||||
}
|
||||
//nolint:staticcheck
|
||||
if options.InboundOptions != (option.InboundOptions{}) {
|
||||
return nil, E.New("legacy inbound fields are deprecated in sing-box 1.11.0 and removed in sing-box 1.13.0, checkout migration: https://sing-box.sagernet.org/migration/#migrate-legacy-inbound-fields-to-rule-actions")
|
||||
}
|
||||
|
||||
address := options.Address
|
||||
inet4Address := common.Filter(address, func(it netip.Prefix) bool {
|
||||
|
||||
@@ -43,3 +43,7 @@ func (s *platformSearcher) FindProcessInfo(ctx context.Context, network string,
|
||||
|
||||
return s.platform.FindConnectionOwner(request)
|
||||
}
|
||||
|
||||
func (s *platformSearcher) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
34
route/process_cache.go
Normal file
34
route/process_cache.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/process"
|
||||
)
|
||||
|
||||
type processCacheKey struct {
|
||||
Network string
|
||||
Source netip.AddrPort
|
||||
Destination netip.AddrPort
|
||||
}
|
||||
|
||||
type processCacheEntry struct {
|
||||
result *adapter.ConnectionOwner
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *Router) findProcessInfoCached(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
key := processCacheKey{
|
||||
Network: network,
|
||||
Source: source,
|
||||
Destination: destination,
|
||||
}
|
||||
if entry, ok := r.processCache.Get(key); ok {
|
||||
return entry.result, entry.err
|
||||
}
|
||||
result, err := process.FindProcessInfo(r.processSearcher, ctx, network, source, destination)
|
||||
r.processCache.Add(key, processCacheEntry{result: result, err: err})
|
||||
return result, err
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/process"
|
||||
"github.com/sagernet/sing-box/common/sniff"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
R "github.com/sagernet/sing-box/route/rule"
|
||||
@@ -415,7 +414,7 @@ func (r *Router) matchRule(
|
||||
} else if metadata.Destination.IsIP() {
|
||||
originDestination = metadata.Destination.AddrPort()
|
||||
}
|
||||
processInfo, fErr := process.FindProcessInfo(r.processSearcher, ctx, metadata.Network, metadata.Source.AddrPort(), originDestination)
|
||||
processInfo, fErr := r.findProcessInfoCached(ctx, metadata.Network, metadata.Source.AddrPort(), originDestination)
|
||||
if fErr != nil {
|
||||
r.logger.InfoContext(ctx, "failed to search process: ", fErr)
|
||||
} else {
|
||||
@@ -427,8 +426,8 @@ func (r *Router) matchRule(
|
||||
} else {
|
||||
r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath)
|
||||
}
|
||||
} else if processInfo.AndroidPackageName != "" {
|
||||
r.logger.InfoContext(ctx, "found package name: ", processInfo.AndroidPackageName)
|
||||
} else if len(processInfo.AndroidPackageNames) > 0 {
|
||||
r.logger.InfoContext(ctx, "found package name: ", strings.Join(processInfo.AndroidPackageNames, ", "))
|
||||
} else if processInfo.UserId != -1 {
|
||||
if processInfo.UserName != "" {
|
||||
r.logger.InfoContext(ctx, "found user: ", processInfo.UserName)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/process"
|
||||
@@ -12,8 +13,11 @@ import (
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
R "github.com/sagernet/sing-box/route/rule"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/task"
|
||||
"github.com/sagernet/sing/contrab/freelru"
|
||||
"github.com/sagernet/sing/contrab/maphash"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/pause"
|
||||
)
|
||||
@@ -34,6 +38,7 @@ type Router struct {
|
||||
ruleSets []adapter.RuleSet
|
||||
ruleSetMap map[string]adapter.RuleSet
|
||||
processSearcher process.Searcher
|
||||
processCache freelru.Cache[processCacheKey, processCacheEntry]
|
||||
pauseManager pause.Manager
|
||||
trackers []adapter.ConnectionTracker
|
||||
platformInterface adapter.PlatformInterface
|
||||
@@ -141,6 +146,11 @@ func (r *Router) Start(stage adapter.StartStage) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
if r.processSearcher != nil {
|
||||
processCache := common.Must1(freelru.NewSharded[processCacheKey, processCacheEntry](256, maphash.NewHasher[processCacheKey]().Hash32))
|
||||
processCache.SetLifetime(200 * time.Millisecond)
|
||||
r.processCache = processCache
|
||||
}
|
||||
case adapter.StartStatePostStart:
|
||||
for i, rule := range r.rules {
|
||||
monitor.Start("initialize rule[", i, "]")
|
||||
@@ -186,6 +196,13 @@ func (r *Router) Close() error {
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
if r.processSearcher != nil {
|
||||
monitor.Start("close process searcher")
|
||||
err = E.Append(err, r.processSearcher.Close(), func(err error) error {
|
||||
return E.Cause(err, "close process searcher")
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
126
route/rule/match_state.go
Normal file
126
route/rule/match_state.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package rule
|
||||
|
||||
import "github.com/sagernet/sing-box/adapter"
|
||||
|
||||
type ruleMatchState uint8
|
||||
|
||||
const (
|
||||
ruleMatchSourceAddress ruleMatchState = 1 << iota
|
||||
ruleMatchSourcePort
|
||||
ruleMatchDestinationAddress
|
||||
ruleMatchDestinationPort
|
||||
)
|
||||
|
||||
type ruleMatchStateSet uint16
|
||||
|
||||
func singleRuleMatchState(state ruleMatchState) ruleMatchStateSet {
|
||||
return 1 << state
|
||||
}
|
||||
|
||||
func emptyRuleMatchState() ruleMatchStateSet {
|
||||
return singleRuleMatchState(0)
|
||||
}
|
||||
|
||||
func (s ruleMatchStateSet) isEmpty() bool {
|
||||
return s == 0
|
||||
}
|
||||
|
||||
func (s ruleMatchStateSet) contains(state ruleMatchState) bool {
|
||||
return s&(1<<state) != 0
|
||||
}
|
||||
|
||||
func (s ruleMatchStateSet) add(state ruleMatchState) ruleMatchStateSet {
|
||||
return s | singleRuleMatchState(state)
|
||||
}
|
||||
|
||||
func (s ruleMatchStateSet) merge(other ruleMatchStateSet) ruleMatchStateSet {
|
||||
return s | other
|
||||
}
|
||||
|
||||
func (s ruleMatchStateSet) combine(other ruleMatchStateSet) ruleMatchStateSet {
|
||||
if s.isEmpty() || other.isEmpty() {
|
||||
return 0
|
||||
}
|
||||
var combined ruleMatchStateSet
|
||||
for left := ruleMatchState(0); left < 16; left++ {
|
||||
if !s.contains(left) {
|
||||
continue
|
||||
}
|
||||
for right := ruleMatchState(0); right < 16; right++ {
|
||||
if !other.contains(right) {
|
||||
continue
|
||||
}
|
||||
combined = combined.add(left | right)
|
||||
}
|
||||
}
|
||||
return combined
|
||||
}
|
||||
|
||||
func (s ruleMatchStateSet) withBase(base ruleMatchState) ruleMatchStateSet {
|
||||
if s.isEmpty() {
|
||||
return 0
|
||||
}
|
||||
var withBase ruleMatchStateSet
|
||||
for state := ruleMatchState(0); state < 16; state++ {
|
||||
if !s.contains(state) {
|
||||
continue
|
||||
}
|
||||
withBase = withBase.add(state | base)
|
||||
}
|
||||
return withBase
|
||||
}
|
||||
|
||||
func (s ruleMatchStateSet) filter(allowed func(ruleMatchState) bool) ruleMatchStateSet {
|
||||
var filtered ruleMatchStateSet
|
||||
for state := ruleMatchState(0); state < 16; state++ {
|
||||
if !s.contains(state) {
|
||||
continue
|
||||
}
|
||||
if allowed(state) {
|
||||
filtered = filtered.add(state)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
type ruleStateMatcher interface {
|
||||
matchStates(metadata *adapter.InboundContext) ruleMatchStateSet
|
||||
}
|
||||
|
||||
type ruleStateMatcherWithBase interface {
|
||||
matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet
|
||||
}
|
||||
|
||||
func matchHeadlessRuleStates(rule adapter.HeadlessRule, metadata *adapter.InboundContext) ruleMatchStateSet {
|
||||
return matchHeadlessRuleStatesWithBase(rule, metadata, 0)
|
||||
}
|
||||
|
||||
func matchHeadlessRuleStatesWithBase(rule adapter.HeadlessRule, metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
|
||||
if matcher, isStateMatcher := rule.(ruleStateMatcherWithBase); isStateMatcher {
|
||||
return matcher.matchStatesWithBase(metadata, base)
|
||||
}
|
||||
if matcher, isStateMatcher := rule.(ruleStateMatcher); isStateMatcher {
|
||||
return matcher.matchStates(metadata).withBase(base)
|
||||
}
|
||||
if rule.Match(metadata) {
|
||||
return emptyRuleMatchState().withBase(base)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func matchRuleItemStates(item RuleItem, metadata *adapter.InboundContext) ruleMatchStateSet {
|
||||
return matchRuleItemStatesWithBase(item, metadata, 0)
|
||||
}
|
||||
|
||||
func matchRuleItemStatesWithBase(item RuleItem, metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
|
||||
if matcher, isStateMatcher := item.(ruleStateMatcherWithBase); isStateMatcher {
|
||||
return matcher.matchStatesWithBase(metadata, base)
|
||||
}
|
||||
if matcher, isStateMatcher := item.(ruleStateMatcher); isStateMatcher {
|
||||
return matcher.matchStates(metadata).withBase(base)
|
||||
}
|
||||
if item.Match(metadata) {
|
||||
return emptyRuleMatchState().withBase(base)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -52,88 +52,124 @@ func (r *abstractDefaultRule) Close() error {
|
||||
}
|
||||
|
||||
func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
|
||||
return !r.matchStates(metadata).isEmpty()
|
||||
}
|
||||
|
||||
func (r *abstractDefaultRule) destinationIPCIDRMatchesSource(metadata *adapter.InboundContext) bool {
|
||||
return !metadata.IgnoreDestinationIPCIDRMatch && metadata.IPCIDRMatchSource && len(r.destinationIPCIDRItems) > 0
|
||||
}
|
||||
|
||||
func (r *abstractDefaultRule) destinationIPCIDRMatchesDestination(metadata *adapter.InboundContext) bool {
|
||||
return !metadata.IgnoreDestinationIPCIDRMatch && !metadata.IPCIDRMatchSource && len(r.destinationIPCIDRItems) > 0
|
||||
}
|
||||
|
||||
func (r *abstractDefaultRule) requiresSourceAddressMatch(metadata *adapter.InboundContext) bool {
|
||||
return len(r.sourceAddressItems) > 0 || r.destinationIPCIDRMatchesSource(metadata)
|
||||
}
|
||||
|
||||
func (r *abstractDefaultRule) requiresDestinationAddressMatch(metadata *adapter.InboundContext) bool {
|
||||
return len(r.destinationAddressItems) > 0 || r.destinationIPCIDRMatchesDestination(metadata)
|
||||
}
|
||||
|
||||
func (r *abstractDefaultRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
|
||||
return r.matchStatesWithBase(metadata, 0)
|
||||
}
|
||||
|
||||
func (r *abstractDefaultRule) matchStatesWithBase(metadata *adapter.InboundContext, inheritedBase ruleMatchState) ruleMatchStateSet {
|
||||
if len(r.allItems) == 0 {
|
||||
return true
|
||||
return emptyRuleMatchState().withBase(inheritedBase)
|
||||
}
|
||||
|
||||
if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch {
|
||||
evaluationBase := inheritedBase
|
||||
if r.invert {
|
||||
evaluationBase = 0
|
||||
}
|
||||
baseState := evaluationBase
|
||||
if len(r.sourceAddressItems) > 0 {
|
||||
metadata.DidMatch = true
|
||||
for _, item := range r.sourceAddressItems {
|
||||
if item.Match(metadata) {
|
||||
metadata.SourceAddressMatch = true
|
||||
break
|
||||
}
|
||||
if matchAnyItem(r.sourceAddressItems, metadata) {
|
||||
baseState |= ruleMatchSourceAddress
|
||||
}
|
||||
}
|
||||
|
||||
if len(r.sourcePortItems) > 0 && !metadata.SourcePortMatch {
|
||||
if r.destinationIPCIDRMatchesSource(metadata) && !baseState.has(ruleMatchSourceAddress) {
|
||||
metadata.DidMatch = true
|
||||
for _, item := range r.sourcePortItems {
|
||||
if item.Match(metadata) {
|
||||
metadata.SourcePortMatch = true
|
||||
break
|
||||
}
|
||||
if matchAnyItem(r.destinationIPCIDRItems, metadata) {
|
||||
baseState |= ruleMatchSourceAddress
|
||||
}
|
||||
} else if r.destinationIPCIDRMatchesSource(metadata) {
|
||||
metadata.DidMatch = true
|
||||
}
|
||||
if len(r.sourcePortItems) > 0 {
|
||||
metadata.DidMatch = true
|
||||
if matchAnyItem(r.sourcePortItems, metadata) {
|
||||
baseState |= ruleMatchSourcePort
|
||||
}
|
||||
}
|
||||
|
||||
if len(r.destinationAddressItems) > 0 && !metadata.DestinationAddressMatch {
|
||||
if len(r.destinationAddressItems) > 0 {
|
||||
metadata.DidMatch = true
|
||||
for _, item := range r.destinationAddressItems {
|
||||
if item.Match(metadata) {
|
||||
metadata.DestinationAddressMatch = true
|
||||
break
|
||||
}
|
||||
if matchAnyItem(r.destinationAddressItems, metadata) {
|
||||
baseState |= ruleMatchDestinationAddress
|
||||
}
|
||||
}
|
||||
|
||||
if !metadata.IgnoreDestinationIPCIDRMatch && len(r.destinationIPCIDRItems) > 0 && !metadata.DestinationAddressMatch {
|
||||
if r.destinationIPCIDRMatchesDestination(metadata) && !baseState.has(ruleMatchDestinationAddress) {
|
||||
metadata.DidMatch = true
|
||||
for _, item := range r.destinationIPCIDRItems {
|
||||
if item.Match(metadata) {
|
||||
metadata.DestinationAddressMatch = true
|
||||
break
|
||||
}
|
||||
if matchAnyItem(r.destinationIPCIDRItems, metadata) {
|
||||
baseState |= ruleMatchDestinationAddress
|
||||
}
|
||||
} else if r.destinationIPCIDRMatchesDestination(metadata) {
|
||||
metadata.DidMatch = true
|
||||
}
|
||||
if len(r.destinationPortItems) > 0 {
|
||||
metadata.DidMatch = true
|
||||
if matchAnyItem(r.destinationPortItems, metadata) {
|
||||
baseState |= ruleMatchDestinationPort
|
||||
}
|
||||
}
|
||||
|
||||
if len(r.destinationPortItems) > 0 && !metadata.DestinationPortMatch {
|
||||
metadata.DidMatch = true
|
||||
for _, item := range r.destinationPortItems {
|
||||
if item.Match(metadata) {
|
||||
metadata.DestinationPortMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range r.items {
|
||||
metadata.DidMatch = true
|
||||
if !item.Match(metadata) {
|
||||
return r.invert
|
||||
return r.invertedFailure(inheritedBase)
|
||||
}
|
||||
}
|
||||
|
||||
if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch {
|
||||
return r.invert
|
||||
var stateSet ruleMatchStateSet
|
||||
if r.ruleSetItem != nil {
|
||||
metadata.DidMatch = true
|
||||
stateSet = matchRuleItemStatesWithBase(r.ruleSetItem, metadata, baseState)
|
||||
} else {
|
||||
stateSet = singleRuleMatchState(baseState)
|
||||
}
|
||||
|
||||
if len(r.sourcePortItems) > 0 && !metadata.SourcePortMatch {
|
||||
return r.invert
|
||||
}
|
||||
|
||||
if ((!metadata.IgnoreDestinationIPCIDRMatch && len(r.destinationIPCIDRItems) > 0) || len(r.destinationAddressItems) > 0) && !metadata.DestinationAddressMatch {
|
||||
return r.invert
|
||||
}
|
||||
|
||||
if len(r.destinationPortItems) > 0 && !metadata.DestinationPortMatch {
|
||||
return r.invert
|
||||
}
|
||||
|
||||
if !metadata.DidMatch {
|
||||
stateSet = stateSet.filter(func(state ruleMatchState) bool {
|
||||
if r.requiresSourceAddressMatch(metadata) && !state.has(ruleMatchSourceAddress) {
|
||||
return false
|
||||
}
|
||||
if len(r.sourcePortItems) > 0 && !state.has(ruleMatchSourcePort) {
|
||||
return false
|
||||
}
|
||||
if r.requiresDestinationAddressMatch(metadata) && !state.has(ruleMatchDestinationAddress) {
|
||||
return false
|
||||
}
|
||||
if len(r.destinationPortItems) > 0 && !state.has(ruleMatchDestinationPort) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if stateSet.isEmpty() {
|
||||
return r.invertedFailure(inheritedBase)
|
||||
}
|
||||
if r.invert {
|
||||
// DNS pre-lookup defers destination address-limit checks until the response phase.
|
||||
if metadata.IgnoreDestinationIPCIDRMatch && stateSet == emptyRuleMatchState() && !metadata.DidMatch && len(r.destinationIPCIDRItems) > 0 {
|
||||
return emptyRuleMatchState().withBase(inheritedBase)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return stateSet
|
||||
}
|
||||
|
||||
return !r.invert
|
||||
func (r *abstractDefaultRule) invertedFailure(base ruleMatchState) ruleMatchStateSet {
|
||||
if r.invert {
|
||||
return emptyRuleMatchState().withBase(base)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *abstractDefaultRule) Action() adapter.RuleAction {
|
||||
@@ -191,17 +227,50 @@ func (r *abstractLogicalRule) Close() error {
|
||||
}
|
||||
|
||||
func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool {
|
||||
if r.mode == C.LogicalTypeAnd {
|
||||
return common.All(r.rules, func(it adapter.HeadlessRule) bool {
|
||||
metadata.ResetRuleCache()
|
||||
return it.Match(metadata)
|
||||
}) != r.invert
|
||||
} else {
|
||||
return common.Any(r.rules, func(it adapter.HeadlessRule) bool {
|
||||
metadata.ResetRuleCache()
|
||||
return it.Match(metadata)
|
||||
}) != r.invert
|
||||
return !r.matchStates(metadata).isEmpty()
|
||||
}
|
||||
|
||||
func (r *abstractLogicalRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
|
||||
return r.matchStatesWithBase(metadata, 0)
|
||||
}
|
||||
|
||||
func (r *abstractLogicalRule) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
|
||||
evaluationBase := base
|
||||
if r.invert {
|
||||
evaluationBase = 0
|
||||
}
|
||||
var stateSet ruleMatchStateSet
|
||||
if r.mode == C.LogicalTypeAnd {
|
||||
stateSet = emptyRuleMatchState().withBase(evaluationBase)
|
||||
for _, rule := range r.rules {
|
||||
nestedMetadata := *metadata
|
||||
nestedMetadata.ResetRuleCache()
|
||||
nestedStateSet := matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, evaluationBase)
|
||||
if nestedStateSet.isEmpty() {
|
||||
if r.invert {
|
||||
return emptyRuleMatchState().withBase(base)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
stateSet = stateSet.combine(nestedStateSet)
|
||||
}
|
||||
} else {
|
||||
for _, rule := range r.rules {
|
||||
nestedMetadata := *metadata
|
||||
nestedMetadata.ResetRuleCache()
|
||||
stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, evaluationBase))
|
||||
}
|
||||
if stateSet.isEmpty() {
|
||||
if r.invert {
|
||||
return emptyRuleMatchState().withBase(base)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
if r.invert {
|
||||
return 0
|
||||
}
|
||||
return stateSet
|
||||
}
|
||||
|
||||
func (r *abstractLogicalRule) Action() adapter.RuleAction {
|
||||
@@ -222,3 +291,13 @@ func (r *abstractLogicalRule) String() string {
|
||||
return "!(" + strings.Join(F.MapToString(r.rules), " "+op+" ") + ")"
|
||||
}
|
||||
}
|
||||
|
||||
func matchAnyItem(items []RuleItem, metadata *adapter.InboundContext) bool {
|
||||
return common.Any(items, func(it RuleItem) bool {
|
||||
return it.Match(metadata)
|
||||
})
|
||||
}
|
||||
|
||||
func (s ruleMatchState) has(target ruleMatchState) bool {
|
||||
return s&target != 0
|
||||
}
|
||||
|
||||
@@ -78,9 +78,9 @@ func newRuleSetOnlyRule(ruleSetMatched bool, invert bool) *DefaultRule {
|
||||
}
|
||||
return &DefaultRule{
|
||||
abstractDefaultRule: abstractDefaultRule{
|
||||
items: []RuleItem{ruleSetItem},
|
||||
allItems: []RuleItem{ruleSetItem},
|
||||
invert: invert,
|
||||
ruleSetItem: ruleSetItem,
|
||||
allItems: []RuleItem{ruleSetItem},
|
||||
invert: invert,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ type DefaultRule struct {
|
||||
abstractDefaultRule
|
||||
}
|
||||
|
||||
func (r *DefaultRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
|
||||
return r.abstractDefaultRule.matchStates(metadata)
|
||||
}
|
||||
|
||||
type RuleItem interface {
|
||||
Match(metadata *adapter.InboundContext) bool
|
||||
String() string
|
||||
@@ -275,7 +279,7 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio
|
||||
matchSource = true
|
||||
}
|
||||
item := NewRuleSetItem(router, options.RuleSet, matchSource, false)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.ruleSetItem = item
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
return rule, nil
|
||||
@@ -287,6 +291,10 @@ type LogicalRule struct {
|
||||
abstractLogicalRule
|
||||
}
|
||||
|
||||
func (r *LogicalRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
|
||||
return r.abstractLogicalRule.matchStates(metadata)
|
||||
}
|
||||
|
||||
func NewLogicalRule(ctx context.Context, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) {
|
||||
action, err := NewRuleAction(ctx, logger, options.RuleAction)
|
||||
if err != nil {
|
||||
|
||||
@@ -47,6 +47,10 @@ type DefaultDNSRule struct {
|
||||
abstractDefaultRule
|
||||
}
|
||||
|
||||
func (r *DefaultDNSRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
|
||||
return r.abstractDefaultRule.matchStates(metadata)
|
||||
}
|
||||
|
||||
func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options option.DefaultDNSRule) (*DefaultDNSRule, error) {
|
||||
rule := &DefaultDNSRule{
|
||||
abstractDefaultRule: abstractDefaultRule{
|
||||
@@ -271,7 +275,7 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
|
||||
matchSource = true
|
||||
}
|
||||
item := NewRuleSetItem(router, options.RuleSet, matchSource, options.RuleSetIPCIDRAcceptEmpty)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.ruleSetItem = item
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
return rule, nil
|
||||
@@ -285,12 +289,9 @@ func (r *DefaultDNSRule) WithAddressLimit() bool {
|
||||
if len(r.destinationIPCIDRItems) > 0 {
|
||||
return true
|
||||
}
|
||||
for _, rawRule := range r.items {
|
||||
ruleSet, isRuleSet := rawRule.(*RuleSetItem)
|
||||
if !isRuleSet {
|
||||
continue
|
||||
}
|
||||
if ruleSet.ContainsDestinationIPCIDRRule() {
|
||||
if r.ruleSetItem != nil {
|
||||
ruleSet, isRuleSet := r.ruleSetItem.(*RuleSetItem)
|
||||
if isRuleSet && ruleSet.ContainsDestinationIPCIDRRule() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -302,11 +303,11 @@ func (r *DefaultDNSRule) Match(metadata *adapter.InboundContext) bool {
|
||||
defer func() {
|
||||
metadata.IgnoreDestinationIPCIDRMatch = false
|
||||
}()
|
||||
return r.abstractDefaultRule.Match(metadata)
|
||||
return !r.matchStates(metadata).isEmpty()
|
||||
}
|
||||
|
||||
func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool {
|
||||
return r.abstractDefaultRule.Match(metadata)
|
||||
return !r.matchStates(metadata).isEmpty()
|
||||
}
|
||||
|
||||
var _ adapter.DNSRule = (*LogicalDNSRule)(nil)
|
||||
@@ -315,6 +316,10 @@ type LogicalDNSRule struct {
|
||||
abstractLogicalRule
|
||||
}
|
||||
|
||||
func (r *LogicalDNSRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
|
||||
return r.abstractLogicalRule.matchStates(metadata)
|
||||
}
|
||||
|
||||
func NewLogicalDNSRule(ctx context.Context, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) {
|
||||
r := &LogicalDNSRule{
|
||||
abstractLogicalRule: abstractLogicalRule{
|
||||
@@ -362,29 +367,13 @@ func (r *LogicalDNSRule) WithAddressLimit() bool {
|
||||
}
|
||||
|
||||
func (r *LogicalDNSRule) Match(metadata *adapter.InboundContext) bool {
|
||||
if r.mode == C.LogicalTypeAnd {
|
||||
return common.All(r.rules, func(it adapter.HeadlessRule) bool {
|
||||
metadata.ResetRuleCache()
|
||||
return it.(adapter.DNSRule).Match(metadata)
|
||||
}) != r.invert
|
||||
} else {
|
||||
return common.Any(r.rules, func(it adapter.HeadlessRule) bool {
|
||||
metadata.ResetRuleCache()
|
||||
return it.(adapter.DNSRule).Match(metadata)
|
||||
}) != r.invert
|
||||
}
|
||||
metadata.IgnoreDestinationIPCIDRMatch = true
|
||||
defer func() {
|
||||
metadata.IgnoreDestinationIPCIDRMatch = false
|
||||
}()
|
||||
return !r.matchStates(metadata).isEmpty()
|
||||
}
|
||||
|
||||
func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool {
|
||||
if r.mode == C.LogicalTypeAnd {
|
||||
return common.All(r.rules, func(it adapter.HeadlessRule) bool {
|
||||
metadata.ResetRuleCache()
|
||||
return it.(adapter.DNSRule).MatchAddressLimit(metadata)
|
||||
}) != r.invert
|
||||
} else {
|
||||
return common.Any(r.rules, func(it adapter.HeadlessRule) bool {
|
||||
metadata.ResetRuleCache()
|
||||
return it.(adapter.DNSRule).MatchAddressLimit(metadata)
|
||||
}) != r.invert
|
||||
}
|
||||
return !r.matchStates(metadata).isEmpty()
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@ type DefaultHeadlessRule struct {
|
||||
abstractDefaultRule
|
||||
}
|
||||
|
||||
func (r *DefaultHeadlessRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
|
||||
return r.abstractDefaultRule.matchStates(metadata)
|
||||
}
|
||||
|
||||
func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessRule) (*DefaultHeadlessRule, error) {
|
||||
networkManager := service.FromContext[adapter.NetworkManager](ctx)
|
||||
rule := &DefaultHeadlessRule{
|
||||
@@ -41,6 +45,11 @@ func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessR
|
||||
invert: options.Invert,
|
||||
},
|
||||
}
|
||||
if len(options.QueryType) > 0 {
|
||||
item := NewQueryTypeItem(options.QueryType)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.Network) > 0 {
|
||||
item := NewNetworkItem(options.Network)
|
||||
rule.items = append(rule.items, item)
|
||||
@@ -199,6 +208,10 @@ type LogicalHeadlessRule struct {
|
||||
abstractLogicalRule
|
||||
}
|
||||
|
||||
func (r *LogicalHeadlessRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
|
||||
return r.abstractLogicalRule.matchStates(metadata)
|
||||
}
|
||||
|
||||
func NewLogicalHeadlessRule(ctx context.Context, options option.LogicalHeadlessRule) (*LogicalHeadlessRule, error) {
|
||||
r := &LogicalHeadlessRule{
|
||||
abstractLogicalRule{
|
||||
|
||||
@@ -25,10 +25,15 @@ func NewPackageNameItem(packageNameList []string) *PackageNameItem {
|
||||
}
|
||||
|
||||
func (r *PackageNameItem) Match(metadata *adapter.InboundContext) bool {
|
||||
if metadata.ProcessInfo == nil || metadata.ProcessInfo.AndroidPackageName == "" {
|
||||
if metadata.ProcessInfo == nil || len(metadata.ProcessInfo.AndroidPackageNames) == 0 {
|
||||
return false
|
||||
}
|
||||
return r.packageMap[metadata.ProcessInfo.AndroidPackageName]
|
||||
for _, packageName := range metadata.ProcessInfo.AndroidPackageNames {
|
||||
if r.packageMap[packageName] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *PackageNameItem) String() string {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
)
|
||||
|
||||
var _ RuleItem = (*ProcessPathItem)(nil)
|
||||
@@ -25,10 +26,20 @@ func NewProcessPathItem(processNameList []string) *ProcessPathItem {
|
||||
}
|
||||
|
||||
func (r *ProcessPathItem) Match(metadata *adapter.InboundContext) bool {
|
||||
if metadata.ProcessInfo == nil || metadata.ProcessInfo.ProcessPath == "" {
|
||||
if metadata.ProcessInfo == nil {
|
||||
return false
|
||||
}
|
||||
return r.processMap[metadata.ProcessInfo.ProcessPath]
|
||||
if metadata.ProcessInfo.ProcessPath != "" && r.processMap[metadata.ProcessInfo.ProcessPath] {
|
||||
return true
|
||||
}
|
||||
if C.IsAndroid {
|
||||
for _, packageName := range metadata.ProcessInfo.AndroidPackageNames {
|
||||
if r.processMap[packageName] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *ProcessPathItem) String() string {
|
||||
|
||||
@@ -41,14 +41,23 @@ func (r *RuleSetItem) Start() error {
|
||||
}
|
||||
|
||||
func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool {
|
||||
metadata.IPCIDRMatchSource = r.ipCidrMatchSource
|
||||
metadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty
|
||||
return !r.matchStates(metadata).isEmpty()
|
||||
}
|
||||
|
||||
func (r *RuleSetItem) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
|
||||
return r.matchStatesWithBase(metadata, 0)
|
||||
}
|
||||
|
||||
func (r *RuleSetItem) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
|
||||
var stateSet ruleMatchStateSet
|
||||
for _, ruleSet := range r.setList {
|
||||
if ruleSet.Match(metadata) {
|
||||
return true
|
||||
}
|
||||
nestedMetadata := *metadata
|
||||
nestedMetadata.ResetRuleMatchCache()
|
||||
nestedMetadata.IPCIDRMatchSource = r.ipCidrMatchSource
|
||||
nestedMetadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty
|
||||
stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(ruleSet, &nestedMetadata, base))
|
||||
}
|
||||
return false
|
||||
return stateSet
|
||||
}
|
||||
|
||||
func (r *RuleSetItem) ContainsDestinationIPCIDRRule() bool {
|
||||
|
||||
@@ -202,10 +202,19 @@ func (s *LocalRuleSet) Close() error {
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool {
|
||||
for _, rule := range s.rules {
|
||||
if rule.Match(metadata) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return !s.matchStates(metadata).isEmpty()
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
|
||||
return s.matchStatesWithBase(metadata, 0)
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
|
||||
var stateSet ruleMatchStateSet
|
||||
for _, rule := range s.rules {
|
||||
nestedMetadata := *metadata
|
||||
nestedMetadata.ResetRuleMatchCache()
|
||||
stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, base))
|
||||
}
|
||||
return stateSet
|
||||
}
|
||||
|
||||
@@ -322,10 +322,19 @@ func (s *RemoteRuleSet) Close() error {
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool {
|
||||
for _, rule := range s.rules {
|
||||
if rule.Match(metadata) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return !s.matchStates(metadata).isEmpty()
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
|
||||
return s.matchStatesWithBase(metadata, 0)
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
|
||||
var stateSet ruleMatchStateSet
|
||||
for _, rule := range s.rules {
|
||||
nestedMetadata := *metadata
|
||||
nestedMetadata.ResetRuleMatchCache()
|
||||
stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, base))
|
||||
}
|
||||
return stateSet
|
||||
}
|
||||
|
||||
852
route/rule/rule_set_semantics_test.go
Normal file
852
route/rule/rule_set_semantics_test.go
Normal file
@@ -0,0 +1,852 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/convertor/adguard"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
slogger "github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRouteRuleSetMergeDestinationAddressGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
name string
|
||||
metadata adapter.InboundContext
|
||||
inner adapter.HeadlessRule
|
||||
}{
|
||||
{
|
||||
name: "domain",
|
||||
metadata: testMetadata("www.example.com"),
|
||||
inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, []string{"www.example.com"}, nil) }),
|
||||
},
|
||||
{
|
||||
name: "domain_suffix",
|
||||
metadata: testMetadata("www.example.com"),
|
||||
inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) }),
|
||||
},
|
||||
{
|
||||
name: "domain_keyword",
|
||||
metadata: testMetadata("www.example.com"),
|
||||
inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationKeywordItem(rule, []string{"example"}) }),
|
||||
},
|
||||
{
|
||||
name: "domain_regex",
|
||||
metadata: testMetadata("www.example.com"),
|
||||
inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationRegexItem(t, rule, []string{`^www\.example\.com$`}) }),
|
||||
},
|
||||
{
|
||||
name: "ip_cidr",
|
||||
metadata: func() adapter.InboundContext {
|
||||
metadata := testMetadata("lookup.example")
|
||||
metadata.DestinationAddresses = []netip.Addr{netip.MustParseAddr("8.8.8.8")}
|
||||
return metadata
|
||||
}(),
|
||||
inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationIPCIDRItem(t, rule, []string{"8.8.8.0/24"})
|
||||
}),
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ruleSet := newLocalRuleSetForTest("merge-destination", testCase.inner)
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
})
|
||||
require.True(t, rule.Match(&testCase.metadata))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteRuleSetMergeSourceAndPortGroups(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("source address", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("merge-source-address", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addSourceAddressItem(t, rule, []string{"10.0.0.0/8"})
|
||||
}))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
addSourceAddressItem(t, rule, []string{"198.51.100.0/24"})
|
||||
})
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("source address via ruleset ipcidr match source", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("merge-source-address-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationIPCIDRItem(t, rule, []string{"10.0.0.0/8"})
|
||||
}))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{
|
||||
setList: []adapter.RuleSet{ruleSet},
|
||||
ipCidrMatchSource: true,
|
||||
})
|
||||
addSourceAddressItem(t, rule, []string{"198.51.100.0/24"})
|
||||
})
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("destination port", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("merge-destination-port", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationPortItem(rule, []uint16{443})
|
||||
}))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
addDestinationPortItem(rule, []uint16{8443})
|
||||
})
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("destination port range", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("merge-destination-port-range", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationPortRangeItem(t, rule, []string{"400:500"})
|
||||
}))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
addDestinationPortItem(rule, []uint16{8443})
|
||||
})
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("source port", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("merge-source-port", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addSourcePortItem(rule, []uint16{1000})
|
||||
}))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
addSourcePortItem(rule, []uint16{2000})
|
||||
})
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("source port range", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("merge-source-port-range", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addSourcePortRangeItem(t, rule, []string{"900:1100"})
|
||||
}))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
addSourcePortItem(rule, []uint16{2000})
|
||||
})
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouteRuleSetOuterGroupedStateMergesIntoSameGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
name string
|
||||
metadata adapter.InboundContext
|
||||
buildOuter func(*testing.T, *abstractDefaultRule)
|
||||
buildInner func(*testing.T, *abstractDefaultRule)
|
||||
}{
|
||||
{
|
||||
name: "destination address",
|
||||
metadata: testMetadata("www.example.com"),
|
||||
buildOuter: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
},
|
||||
buildInner: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addDestinationAddressItem(t, rule, nil, []string{"google.com"})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "source address",
|
||||
metadata: testMetadata("www.example.com"),
|
||||
buildOuter: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addSourceAddressItem(t, rule, []string{"10.0.0.0/8"})
|
||||
},
|
||||
buildInner: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addSourceAddressItem(t, rule, []string{"198.51.100.0/24"})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "source port",
|
||||
metadata: testMetadata("www.example.com"),
|
||||
buildOuter: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addSourcePortItem(rule, []uint16{1000})
|
||||
},
|
||||
buildInner: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addSourcePortItem(rule, []uint16{2000})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "destination port",
|
||||
metadata: testMetadata("www.example.com"),
|
||||
buildOuter: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addDestinationPortItem(rule, []uint16{443})
|
||||
},
|
||||
buildInner: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addDestinationPortItem(rule, []uint16{8443})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "destination ip cidr",
|
||||
metadata: func() adapter.InboundContext {
|
||||
metadata := testMetadata("lookup.example")
|
||||
metadata.DestinationAddresses = []netip.Addr{netip.MustParseAddr("203.0.113.1")}
|
||||
return metadata
|
||||
}(),
|
||||
buildOuter: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
},
|
||||
buildInner: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addDestinationIPCIDRItem(t, rule, []string{"198.51.100.0/24"})
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ruleSet := newLocalRuleSetForTest("outer-merge-"+testCase.name, headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
testCase.buildInner(t, rule)
|
||||
}))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
testCase.buildOuter(t, rule)
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
require.True(t, rule.Match(&testCase.metadata))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteRuleSetOtherFieldsStayAnd(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("other-fields-and", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
}))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP}))
|
||||
})
|
||||
require.False(t, rule.Match(&metadata))
|
||||
}
|
||||
|
||||
func TestRouteRuleSetMergedBranchKeepsAndConstraints(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("outer group does not bypass inner non grouped condition", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("network-and", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP}))
|
||||
}))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
require.False(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("outer group does not satisfy different grouped branch", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("different-group", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"google.com"})
|
||||
}))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addSourcePortItem(rule, []uint16{1000})
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
require.False(t, rule.Match(&metadata))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouteRuleSetOrSemantics(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("later ruleset can satisfy outer group", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
emptyStateSet := newLocalRuleSetForTest("network-only", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP}))
|
||||
}))
|
||||
destinationStateSet := newLocalRuleSetForTest("domain-only", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
}))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{emptyStateSet, destinationStateSet}})
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
})
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("later rule in same set can satisfy outer group", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest(
|
||||
"rule-set-or",
|
||||
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP}))
|
||||
}),
|
||||
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
}),
|
||||
)
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
})
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("cross ruleset union is not allowed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
sourceStateSet := newLocalRuleSetForTest("source-only", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addSourcePortItem(rule, []uint16{1000})
|
||||
}))
|
||||
destinationStateSet := newLocalRuleSetForTest("destination-only", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
}))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{sourceStateSet, destinationStateSet}})
|
||||
addSourcePortItem(rule, []uint16{2000})
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
})
|
||||
require.False(t, rule.Match(&metadata))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouteRuleSetLogicalSemantics(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("logical or keeps all successful branch states", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("logical-or", headlessLogicalRule(
|
||||
C.LogicalTypeOr,
|
||||
false,
|
||||
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP}))
|
||||
}),
|
||||
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
}),
|
||||
))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
})
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("logical and unions child states", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("logical-and", headlessLogicalRule(
|
||||
C.LogicalTypeAnd,
|
||||
false,
|
||||
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
}),
|
||||
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addSourcePortItem(rule, []uint16{1000})
|
||||
}),
|
||||
))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
addSourcePortItem(rule, []uint16{2000})
|
||||
})
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("invert success does not contribute positive state", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("invert", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
rule.invert = true
|
||||
addDestinationAddressItem(t, rule, nil, []string{"cn"})
|
||||
}))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
})
|
||||
require.False(t, rule.Match(&metadata))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouteRuleSetInvertMergedBranchSemantics(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("default invert keeps inherited group outside grouped predicate", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("invert-grouped", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
rule.invert = true
|
||||
addDestinationAddressItem(t, rule, nil, []string{"google.com"})
|
||||
}))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("default invert keeps inherited group after negation succeeds", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("invert-network", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
rule.invert = true
|
||||
addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP}))
|
||||
}))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("logical invert keeps inherited group outside grouped predicate", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("logical-invert-grouped", headlessLogicalRule(
|
||||
C.LogicalTypeOr,
|
||||
true,
|
||||
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"google.com"})
|
||||
}),
|
||||
))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("logical invert keeps inherited group after negation succeeds", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("logical-invert-network", headlessLogicalRule(
|
||||
C.LogicalTypeOr,
|
||||
true,
|
||||
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP}))
|
||||
}),
|
||||
))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouteRuleSetNoLeakageRegressions(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("same ruleset failed branch does not leak", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest(
|
||||
"same-set",
|
||||
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
addSourcePortItem(rule, []uint16{1})
|
||||
}),
|
||||
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
addSourcePortItem(rule, []uint16{1000})
|
||||
}),
|
||||
)
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
require.False(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("adguard exclusion remains isolated across rulesets", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("im.qq.com")
|
||||
excludeSet := newLocalRuleSetForTest("adguard", mustAdGuardRule(t, "@@||im.qq.com^\n||whatever1.com^\n"))
|
||||
otherSet := newLocalRuleSetForTest("other", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"whatever2.com"})
|
||||
}))
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{excludeSet, otherSet}})
|
||||
})
|
||||
require.False(t, rule.Match(&metadata))
|
||||
})
|
||||
}
|
||||
|
||||
func TestDefaultRuleDoesNotReuseGroupedMatchCacheAcrossEvaluations(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
})
|
||||
require.True(t, rule.Match(&metadata))
|
||||
|
||||
metadata.Destination.Fqdn = "www.example.org"
|
||||
require.False(t, rule.Match(&metadata))
|
||||
}
|
||||
|
||||
func TestRouteRuleSetRemoteUsesSameSemantics(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newRemoteRuleSetForTest(
|
||||
"remote",
|
||||
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP}))
|
||||
}),
|
||||
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
}),
|
||||
)
|
||||
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
})
|
||||
require.True(t, rule.Match(&metadata))
|
||||
}
|
||||
|
||||
func TestDNSRuleSetSemantics(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("outer destination group merges into matching ruleset branch", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.baidu.com")
|
||||
ruleSet := newLocalRuleSetForTest("dns-merged-branch", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"google.com"})
|
||||
}))
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"baidu.com"})
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("outer destination group does not bypass ruleset non grouped condition", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("dns-network-and", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP}))
|
||||
}))
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
require.False(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("outer destination group stays outside inverted grouped branch", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.baidu.com")
|
||||
ruleSet := newLocalRuleSetForTest("dns-invert-grouped", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
rule.invert = true
|
||||
addDestinationAddressItem(t, rule, nil, []string{"google.com"})
|
||||
}))
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"baidu.com"})
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("outer destination group stays outside inverted logical branch", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("dns-logical-invert-network", headlessLogicalRule(
|
||||
C.LogicalTypeOr,
|
||||
true,
|
||||
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP}))
|
||||
}),
|
||||
))
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("match address limit merges destination group", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("dns-merge", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
}))
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
})
|
||||
require.True(t, rule.MatchAddressLimit(&metadata))
|
||||
})
|
||||
t.Run("dns keeps ruleset or semantics", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
emptyStateSet := newLocalRuleSetForTest("dns-empty", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP}))
|
||||
}))
|
||||
destinationStateSet := newLocalRuleSetForTest("dns-destination", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
}))
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{emptyStateSet, destinationStateSet}})
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
})
|
||||
require.True(t, rule.MatchAddressLimit(&metadata))
|
||||
})
|
||||
t.Run("ruleset ip cidr flags stay scoped", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest("dns-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
}))
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{
|
||||
setList: []adapter.RuleSet{ruleSet},
|
||||
ipCidrAcceptEmpty: true,
|
||||
})
|
||||
})
|
||||
require.True(t, rule.MatchAddressLimit(&metadata))
|
||||
require.False(t, metadata.IPCIDRMatchSource)
|
||||
require.False(t, metadata.IPCIDRAcceptEmpty)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDNSInvertAddressLimitPreLookupRegression(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
name string
|
||||
build func(*testing.T, *abstractDefaultRule)
|
||||
matchedAddrs []netip.Addr
|
||||
unmatchedAddrs []netip.Addr
|
||||
}{
|
||||
{
|
||||
name: "ip_cidr",
|
||||
build: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
},
|
||||
matchedAddrs: []netip.Addr{netip.MustParseAddr("203.0.113.1")},
|
||||
unmatchedAddrs: []netip.Addr{netip.MustParseAddr("8.8.8.8")},
|
||||
},
|
||||
{
|
||||
name: "ip_is_private",
|
||||
build: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addDestinationIPIsPrivateItem(rule)
|
||||
},
|
||||
matchedAddrs: []netip.Addr{netip.MustParseAddr("10.0.0.1")},
|
||||
unmatchedAddrs: []netip.Addr{netip.MustParseAddr("8.8.8.8")},
|
||||
},
|
||||
{
|
||||
name: "ip_accept_any",
|
||||
build: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addDestinationIPAcceptAnyItem(rule)
|
||||
},
|
||||
matchedAddrs: []netip.Addr{netip.MustParseAddr("203.0.113.1")},
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
rule.invert = true
|
||||
testCase.build(t, rule)
|
||||
})
|
||||
|
||||
preLookupMetadata := testMetadata("lookup.example")
|
||||
require.True(t, rule.Match(&preLookupMetadata))
|
||||
|
||||
matchedMetadata := testMetadata("lookup.example")
|
||||
matchedMetadata.DestinationAddresses = testCase.matchedAddrs
|
||||
require.False(t, rule.MatchAddressLimit(&matchedMetadata))
|
||||
|
||||
unmatchedMetadata := testMetadata("lookup.example")
|
||||
unmatchedMetadata.DestinationAddresses = testCase.unmatchedAddrs
|
||||
require.True(t, rule.MatchAddressLimit(&unmatchedMetadata))
|
||||
})
|
||||
}
|
||||
t.Run("mixed resolved and deferred fields keep old pre lookup false", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("lookup.example")
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
rule.invert = true
|
||||
addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP}))
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
})
|
||||
require.False(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("ruleset only deferred fields keep old pre lookup false", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("lookup.example")
|
||||
ruleSet := newLocalRuleSetForTest("dns-ruleset-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
}))
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
rule.invert = true
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
require.False(t, rule.Match(&metadata))
|
||||
})
|
||||
}
|
||||
|
||||
func routeRuleForTest(build func(*abstractDefaultRule)) *DefaultRule {
|
||||
rule := &DefaultRule{}
|
||||
build(&rule.abstractDefaultRule)
|
||||
return rule
|
||||
}
|
||||
|
||||
func dnsRuleForTest(build func(*abstractDefaultRule)) *DefaultDNSRule {
|
||||
rule := &DefaultDNSRule{}
|
||||
build(&rule.abstractDefaultRule)
|
||||
return rule
|
||||
}
|
||||
|
||||
func headlessDefaultRule(t *testing.T, build func(*abstractDefaultRule)) *DefaultHeadlessRule {
|
||||
t.Helper()
|
||||
rule := &DefaultHeadlessRule{}
|
||||
build(&rule.abstractDefaultRule)
|
||||
return rule
|
||||
}
|
||||
|
||||
func headlessLogicalRule(mode string, invert bool, rules ...adapter.HeadlessRule) *LogicalHeadlessRule {
|
||||
return &LogicalHeadlessRule{
|
||||
abstractLogicalRule: abstractLogicalRule{
|
||||
rules: rules,
|
||||
mode: mode,
|
||||
invert: invert,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newLocalRuleSetForTest(tag string, rules ...adapter.HeadlessRule) *LocalRuleSet {
|
||||
return &LocalRuleSet{
|
||||
tag: tag,
|
||||
rules: rules,
|
||||
}
|
||||
}
|
||||
|
||||
func newRemoteRuleSetForTest(tag string, rules ...adapter.HeadlessRule) *RemoteRuleSet {
|
||||
return &RemoteRuleSet{
|
||||
options: option.RuleSet{Tag: tag},
|
||||
rules: rules,
|
||||
}
|
||||
}
|
||||
|
||||
func mustAdGuardRule(t *testing.T, content string) adapter.HeadlessRule {
|
||||
t.Helper()
|
||||
rules, err := adguard.ToOptions(strings.NewReader(content), slogger.NOP())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rules, 1)
|
||||
rule, err := NewHeadlessRule(context.Background(), rules[0])
|
||||
require.NoError(t, err)
|
||||
return rule
|
||||
}
|
||||
|
||||
func testMetadata(domain string) adapter.InboundContext {
|
||||
return adapter.InboundContext{
|
||||
Network: N.NetworkTCP,
|
||||
Source: M.Socksaddr{
|
||||
Addr: netip.MustParseAddr("10.0.0.1"),
|
||||
Port: 1000,
|
||||
},
|
||||
Destination: M.Socksaddr{
|
||||
Fqdn: domain,
|
||||
Port: 443,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func addRuleSetItem(rule *abstractDefaultRule, item *RuleSetItem) {
|
||||
rule.ruleSetItem = item
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
|
||||
func addOtherItem(rule *abstractDefaultRule, item RuleItem) {
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
|
||||
func addSourceAddressItem(t *testing.T, rule *abstractDefaultRule, cidrs []string) {
|
||||
t.Helper()
|
||||
item, err := NewIPCIDRItem(true, cidrs)
|
||||
require.NoError(t, err)
|
||||
rule.sourceAddressItems = append(rule.sourceAddressItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
|
||||
func addDestinationAddressItem(t *testing.T, rule *abstractDefaultRule, domains []string, suffixes []string) {
|
||||
t.Helper()
|
||||
item, err := NewDomainItem(domains, suffixes)
|
||||
require.NoError(t, err)
|
||||
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
|
||||
func addDestinationKeywordItem(rule *abstractDefaultRule, keywords []string) {
|
||||
item := NewDomainKeywordItem(keywords)
|
||||
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
|
||||
func addDestinationRegexItem(t *testing.T, rule *abstractDefaultRule, regexes []string) {
|
||||
t.Helper()
|
||||
item, err := NewDomainRegexItem(regexes)
|
||||
require.NoError(t, err)
|
||||
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
|
||||
func addDestinationIPCIDRItem(t *testing.T, rule *abstractDefaultRule, cidrs []string) {
|
||||
t.Helper()
|
||||
item, err := NewIPCIDRItem(false, cidrs)
|
||||
require.NoError(t, err)
|
||||
rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
|
||||
func addDestinationIPIsPrivateItem(rule *abstractDefaultRule) {
|
||||
item := NewIPIsPrivateItem(false)
|
||||
rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
|
||||
func addDestinationIPAcceptAnyItem(rule *abstractDefaultRule) {
|
||||
item := NewIPAcceptAnyItem()
|
||||
rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
|
||||
func addSourcePortItem(rule *abstractDefaultRule, ports []uint16) {
|
||||
item := NewPortItem(true, ports)
|
||||
rule.sourcePortItems = append(rule.sourcePortItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
|
||||
func addSourcePortRangeItem(t *testing.T, rule *abstractDefaultRule, ranges []string) {
|
||||
t.Helper()
|
||||
item, err := NewPortRangeItem(true, ranges)
|
||||
require.NoError(t, err)
|
||||
rule.sourcePortItems = append(rule.sourcePortItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
|
||||
func addDestinationPortItem(rule *abstractDefaultRule, ports []uint16) {
|
||||
item := NewPortItem(false, ports)
|
||||
rule.destinationPortItems = append(rule.destinationPortItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
|
||||
func addDestinationPortRangeItem(t *testing.T, rule *abstractDefaultRule, ranges []string) {
|
||||
t.Helper()
|
||||
item, err := NewPortRangeItem(false, ranges)
|
||||
require.NoError(t, err)
|
||||
rule.destinationPortItems = append(rule.destinationPortItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
@@ -229,12 +229,13 @@ func (e *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
|
||||
}
|
||||
|
||||
func (e *Endpoint) Close() error {
|
||||
if e.device != nil {
|
||||
e.device.Close()
|
||||
}
|
||||
if e.pauseCallback != nil {
|
||||
e.pause.UnregisterCallback(e.pauseCallback)
|
||||
}
|
||||
if e.device != nil {
|
||||
e.device.Down()
|
||||
e.device.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user