Compare commits

..

34 Commits

Author SHA1 Message Date
dependabot[bot]
870d9d3a57 build(deps): bump google.golang.org/grpc from 1.77.0 to 1.79.3 in /test
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.77.0 to 1.79.3.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.77.0...v1.79.3)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.79.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 12:04:58 +00:00
世界
71626beaf8 Bump version 2026-03-23 20:01:44 +08:00
世界
004a967e72 cronet-go: Update chromium to 145.0.7632.159 2026-03-23 20:01:30 +08:00
世界
3ab8ba76d0 documentation: Update descriptions for neighbor rules 2026-03-23 20:01:29 +08:00
世界
1edc8fd855 Add macOS support for MAC and hostname rule items 2026-03-23 20:01:29 +08:00
世界
22a8661858 Add Android support for MAC and hostname rule items 2026-03-23 20:01:29 +08:00
世界
52aa5716be Add MAC and hostname rule items 2026-03-23 20:01:25 +08:00
世界
2c24e82257 Bump version 2026-03-23 19:59:05 +08:00
世界
a3623eb41a tun: Fix system stack rewriting TUN subnet destinations to loopback 2026-03-23 19:38:55 +08:00
世界
72bc4c1f87 Fix DNS transport returning error for empty AAAA response
Closes #3925
2026-03-23 19:21:55 +08:00
世界
9ac1e2ff32 Match package_name in process_path rule on Android 2026-03-23 18:57:35 +08:00
世界
0045103d14 Fix package_name shared uid matching 2026-03-23 18:57:35 +08:00
世界
d2a933784c Optimize Darwin process finder 2026-03-23 18:57:35 +08:00
世界
3f05a37f65 Optimize Linux process finder 2026-03-23 18:57:35 +08:00
世界
b8e5a71450 Add process information cache to avoid duplicate lookups
PreMatch and full match phases each created a fresh InboundContext,
causing process search (expensive OS syscalls) to run twice per
connection. Use a freelru ShardedLRU cache with 200ms TTL to serve
the second lookup from cache.
2026-03-23 14:26:45 +08:00
世界
c13faa8e3c tailscale: Only set ProcessLocalIPs/ProcessSubnets for fake TUN 2026-03-23 14:16:40 +08:00
世界
7623bcd19e Fix DialerForICMPDestination 2026-03-23 13:58:55 +08:00
世界
795d1c2892 Fix nested rule-set match cache isolation 2026-03-23 12:26:19 +08:00
世界
6913b11e0a Reject removed legacy inbound fields instead of silently ignoring 2026-03-21 17:16:10 +08:00
世界
1e57c06295 daemon: Allow StartOrReloadService to recover from FATAL state 2026-03-21 13:37:14 +08:00
世界
ea464cef8d daemon: Fix CloseService leaving instance non-nil on close error 2026-03-21 13:23:57 +08:00
Andrew Novikov
a8e3cd3256 tun: Fix nfqueue not working in prerouting 2026-03-17 11:05:40 +08:00
世界
686cf1f304 documentation: Fix Chinese link anchors 2026-03-16 12:24:10 +08:00
世界
9fbfb87723 documentation: Fix unicode heading anchors 2026-03-16 12:10:32 +08:00
世界
d2fa21d07b Deprecate Socksaddr.IsFqdn: do not reject potentially valid domain names 2026-03-16 09:37:59 +08:00
世界
d3768cca36 Bump version 2026-03-15 17:56:37 +08:00
世界
0889ddd001 Fix connector canceled dial cleanup 2026-03-15 17:56:37 +08:00
深鸣
f46fbf188a documentation: Minor fixes 2026-03-15 17:56:37 +08:00
世界
f2d15139f5 tun: Fix nftables single include_uid not working 2026-03-15 16:58:34 +08:00
世界
041646b728 Fix kTLS crash 2026-03-14 21:38:38 +08:00
世界
b990de2e12 tun: Fix "Fix auto_redirect dropping SO_BINDTODEVICE traffic" 2026-03-14 21:38:38 +08:00
世界
fe585157d2 Bump version 2026-03-14 21:38:38 +08:00
世界
eed6a36e5d tun:Fix auto_redirect dropping SO_BINDTODEVICE traffic 2026-03-14 21:38:38 +08:00
世界
eb0f38544c tailscale: Fix system interface rules 2026-03-14 21:38:38 +08:00
139 changed files with 2059 additions and 7964 deletions

View File

@@ -41,13 +41,13 @@ jobs:
version: ${{ steps.outputs.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.26.0
go-version: ~1.25.8
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -117,14 +117,14 @@ jobs:
- { os: android, arch: "386", ndk: "i686-linux-android23" }
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go
if: ${{ ! matrix.legacy_win7 }}
uses: actions/setup-go@v5
with:
go-version: ~1.26.0
go-version: ~1.25.8
- name: Cache Go for Windows 7
if: matrix.legacy_win7
id: cache-go-for-windows7
@@ -458,7 +458,7 @@ jobs:
- { arch: amd64, legacy_osx: true, legacy_name: "macos-10.13" }
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go
@@ -551,7 +551,7 @@ jobs:
- { arch: arm64, naive: true }
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go
@@ -634,14 +634,14 @@ jobs:
- calculate_version
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
submodules: 'recursive'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.26.0
go-version: ~1.25.8
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -724,14 +724,14 @@ jobs:
- calculate_version
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
submodules: 'recursive'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.26.0
go-version: ~1.25.8
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -822,7 +822,7 @@ jobs:
steps:
- name: Checkout
if: matrix.if
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
submodules: 'recursive'
@@ -830,7 +830,7 @@ jobs:
if: matrix.if
uses: actions/setup-go@v5
with:
go-version: ~1.26.0
go-version: ~1.25.8
- name: Set tag
if: matrix.if
run: |-
@@ -976,7 +976,7 @@ jobs:
- build_apple
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Cache ghr

View File

@@ -49,14 +49,14 @@ jobs:
echo "ref=$ref"
echo "ref=$ref" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: ${{ steps.ref.outputs.ref }}
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.26.0
go-version: ~1.25.8
- name: Clone cronet-go
if: matrix.naive
run: |
@@ -188,7 +188,7 @@ jobs:
echo "ref=$ref"
echo "ref=$ref" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: ${{ steps.ref.outputs.ref }}
fetch-depth: 0

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go

View File

@@ -29,13 +29,13 @@ jobs:
version: ${{ steps.outputs.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.26.0
go-version: ~1.25.8
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -72,13 +72,13 @@ jobs:
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.26.0
go-version: ~1.25.8
- name: Clone cronet-go
if: matrix.naive
run: |
@@ -236,7 +236,7 @@ jobs:
- build
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Set tag

View File

@@ -104,6 +104,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

View File

@@ -51,11 +51,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 {

View File

@@ -239,7 +239,7 @@ func setMarkWrapper(networkManager adapter.NetworkManager, mark uint32, isDefaul
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
if !address.IsValid() {
return nil, E.New("invalid address")
} else if address.IsFqdn() {
} else if address.IsDomain() {
return nil, E.New("domain not resolved")
}
if d.networkStrategy == nil {
@@ -329,9 +329,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
}
}

View File

@@ -96,7 +96,7 @@ func (d *resolveDialer) DialContext(ctx context.Context, network string, destina
if err != nil {
return nil, err
}
if !destination.IsFqdn() {
if !destination.IsDomain() {
return d.dialer.DialContext(ctx, network, destination)
}
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
@@ -116,7 +116,7 @@ func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
if err != nil {
return nil, err
}
if !destination.IsFqdn() {
if !destination.IsDomain() {
return d.dialer.ListenPacket(ctx, destination)
}
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
@@ -144,7 +144,7 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context
if err != nil {
return nil, err
}
if !destination.IsFqdn() {
if !destination.IsDomain() {
return d.dialer.DialContext(ctx, network, destination)
}
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
@@ -167,7 +167,7 @@ func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.C
if err != nil {
return nil, err
}
if !destination.IsFqdn() {
if !destination.IsDomain() {
return d.dialer.ListenPacket(ctx, destination)
}
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)

View File

@@ -12,6 +12,7 @@ import (
"fmt"
"io"
"net"
"unsafe"
)
func (c *Conn) Read(b []byte) (int, error) {
@@ -229,7 +230,7 @@ func (c *Conn) readRawRecord() (typ uint8, data []byte, err error) {
record := c.rawConn.RawInput.Next(recordHeaderLen + n)
data, typ, err = c.rawConn.In.Decrypt(record)
if err != nil {
err = c.rawConn.In.SetErrorLocked(c.sendAlert(uint8(err.(tls.AlertError))))
err = c.rawConn.In.SetErrorLocked(c.sendAlert(*(*uint8)((*[2]unsafe.Pointer)(unsafe.Pointer(&err))[1])))
return
}
return

View File

@@ -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

View File

@@ -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
}

View File

@@ -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]))
}

View 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
}

View File

@@ -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)
}

View File

@@ -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 {

View 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))
}

View File

@@ -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 {

View File

@@ -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{

View File

@@ -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" +

View File

@@ -195,7 +195,7 @@ message ProcessInfo {
int32 userId = 2;
string userName = 3;
string processPath = 4;
string packageName = 5;
repeated string packageNames = 5;
}
message CloseConnectionRequest {

View File

@@ -55,6 +55,12 @@ type contextKeyConnecting struct{}
var errRecursiveConnectorDial = E.New("recursive connector dial")
type connectorDialResult[T any] struct {
connection T
cancel context.CancelFunc
err error
}
func (c *Connector[T]) Get(ctx context.Context) (T, error) {
var zero T
for {
@@ -100,41 +106,37 @@ func (c *Connector[T]) Get(ctx context.Context) (T, error) {
return zero, err
}
c.connecting = make(chan struct{})
connecting := make(chan struct{})
c.connecting = connecting
dialContext := context.WithValue(ctx, contextKeyConnecting{}, c)
dialResult := make(chan connectorDialResult[T], 1)
c.access.Unlock()
dialContext := context.WithValue(ctx, contextKeyConnecting{}, c)
connection, cancel, err := c.dialWithCancellation(dialContext)
go func() {
connection, cancel, err := c.dialWithCancellation(dialContext)
dialResult <- connectorDialResult[T]{
connection: connection,
cancel: cancel,
err: err,
}
}()
c.access.Lock()
close(c.connecting)
c.connecting = nil
if err != nil {
c.access.Unlock()
return zero, err
}
if c.closed {
cancel()
c.callbacks.Close(connection)
c.access.Unlock()
select {
case result := <-dialResult:
return c.completeDial(ctx, connecting, result)
case <-ctx.Done():
go func() {
result := <-dialResult
_, _ = c.completeDial(ctx, connecting, result)
}()
return zero, ctx.Err()
case <-c.closeCtx.Done():
go func() {
result := <-dialResult
_, _ = c.completeDial(ctx, connecting, result)
}()
return zero, ErrTransportClosed
}
if err = ctx.Err(); err != nil {
cancel()
c.callbacks.Close(connection)
c.access.Unlock()
return zero, err
}
c.connection = connection
c.hasConnection = true
c.connectionCancel = cancel
result := c.connection
c.access.Unlock()
return result, nil
}
}
@@ -143,6 +145,38 @@ func isRecursiveConnectorDial[T any](ctx context.Context, connector *Connector[T
return loaded && dialConnector == connector
}
func (c *Connector[T]) completeDial(ctx context.Context, connecting chan struct{}, result connectorDialResult[T]) (T, error) {
var zero T
c.access.Lock()
defer c.access.Unlock()
defer func() {
if c.connecting == connecting {
c.connecting = nil
}
close(connecting)
}()
if result.err != nil {
return zero, result.err
}
if c.closed || c.closeCtx.Err() != nil {
result.cancel()
c.callbacks.Close(result.connection)
return zero, ErrTransportClosed
}
if err := ctx.Err(); err != nil {
result.cancel()
c.callbacks.Close(result.connection)
return zero, err
}
c.connection = result.connection
c.hasConnection = true
c.connectionCancel = result.cancel
return c.connection, nil
}
func (c *Connector[T]) dialWithCancellation(ctx context.Context) (T, context.CancelFunc, error) {
var zero T
if err := ctx.Err(); err != nil {

View File

@@ -188,13 +188,157 @@ func TestConnectorCanceledRequestDoesNotCacheConnection(t *testing.T) {
err := <-result
require.ErrorIs(t, err, context.Canceled)
require.EqualValues(t, 1, dialCount.Load())
require.EqualValues(t, 1, closeCount.Load())
require.Eventually(t, func() bool {
return closeCount.Load() == 1
}, time.Second, 10*time.Millisecond)
_, err = connector.Get(context.Background())
require.NoError(t, err)
require.EqualValues(t, 2, dialCount.Load())
}
func TestConnectorCanceledRequestReturnsBeforeIgnoredDialCompletes(t *testing.T) {
t.Parallel()
var (
dialCount atomic.Int32
closeCount atomic.Int32
)
dialStarted := make(chan struct{}, 1)
releaseDial := make(chan struct{})
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
dialCount.Add(1)
select {
case dialStarted <- struct{}{}:
default:
}
<-releaseDial
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {
closeCount.Add(1)
},
Reset: func(connection *testConnectorConnection) {},
})
requestContext, cancel := context.WithCancel(context.Background())
result := make(chan error, 1)
go func() {
_, err := connector.Get(requestContext)
result <- err
}()
<-dialStarted
cancel()
select {
case err := <-result:
require.ErrorIs(t, err, context.Canceled)
case <-time.After(time.Second):
t.Fatal("Get did not return after request cancel")
}
require.EqualValues(t, 1, dialCount.Load())
require.EqualValues(t, 0, closeCount.Load())
close(releaseDial)
require.Eventually(t, func() bool {
return closeCount.Load() == 1
}, time.Second, 10*time.Millisecond)
_, err := connector.Get(context.Background())
require.NoError(t, err)
require.EqualValues(t, 2, dialCount.Load())
}
func TestConnectorWaiterDoesNotStartNewDialBeforeCanceledDialCompletes(t *testing.T) {
t.Parallel()
var (
dialCount atomic.Int32
closeCount atomic.Int32
)
firstDialStarted := make(chan struct{}, 1)
secondDialStarted := make(chan struct{}, 1)
releaseFirstDial := make(chan struct{})
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
attempt := dialCount.Add(1)
switch attempt {
case 1:
select {
case firstDialStarted <- struct{}{}:
default:
}
<-releaseFirstDial
case 2:
select {
case secondDialStarted <- struct{}{}:
default:
}
}
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {
closeCount.Add(1)
},
Reset: func(connection *testConnectorConnection) {},
})
requestContext, cancel := context.WithCancel(context.Background())
firstResult := make(chan error, 1)
go func() {
_, err := connector.Get(requestContext)
firstResult <- err
}()
<-firstDialStarted
cancel()
secondResult := make(chan error, 1)
go func() {
_, err := connector.Get(context.Background())
secondResult <- err
}()
select {
case <-secondDialStarted:
t.Fatal("second dial started before first dial completed")
case <-time.After(100 * time.Millisecond):
}
select {
case err := <-firstResult:
require.ErrorIs(t, err, context.Canceled)
case <-time.After(time.Second):
t.Fatal("first Get did not return after request cancel")
}
close(releaseFirstDial)
require.Eventually(t, func() bool {
return closeCount.Load() == 1
}, time.Second, 10*time.Millisecond)
select {
case <-secondDialStarted:
case <-time.After(time.Second):
t.Fatal("second dial did not start after first dial completed")
}
err := <-secondResult
require.NoError(t, err)
require.EqualValues(t, 2, dialCount.Load())
}
func TestConnectorDialContextNotCanceledByRequestContextAfterDial(t *testing.T) {
t.Parallel()

View File

@@ -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:

View File

@@ -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:

View File

@@ -2,7 +2,15 @@
icon: material/alert-decagram
---
#### 1.14.0-alpha.2
#### 1.14.0-alpha.4
* Fixes and improvements
#### 1.13.4-beta.1
* Fixes and improvements
#### 1.13.3
* Add OpenWrt and Alpine APK packages to release **1**
* Backport to macOS 10.13 High Sierra **2**
@@ -26,7 +34,11 @@ from [SagerNet/go](https://github.com/SagerNet/go).
See [OCM](/configuration/service/ocm).
#### 1.13.3-beta.1
#### 1.12.24
* Fixes and improvements
#### 1.14.0-alpha.2
* Add OpenWrt and Alpine APK packages to release **1**
* Backport to macOS 10.13 High Sierra **2**

View File

@@ -4,7 +4,7 @@ icon: material/delete-clock
!!! failure "已在 sing-box 1.12.0 废弃"
旧的 fake-ip 配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-to-new-dns-servers)。
旧的 fake-ip 配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
### 结构

View File

@@ -267,7 +267,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
!!! failure "已在 sing-box 1.12.0 中被移除"
GeoSite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geosite)。
GeoSite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。
匹配 Geosite。
@@ -275,7 +275,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
!!! failure "已在 sing-box 1.12.0 中被移除"
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geoip)。
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
匹配源 GeoIP。
@@ -484,7 +484,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
!!! failure "已在 sing-box 1.12.0 废弃"
`outbound` 规则项已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver)。
`outbound` 规则项已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-outbound-dns-规则项到域解析选项)。
匹配出站。
@@ -536,7 +536,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
!!! failure "已在 sing-box 1.12.0 中被移除"
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geoip)。
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
与查询响应匹配 GeoIP。

View File

@@ -64,7 +64,7 @@ DNS 服务器的路径。
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
### 拨号字段

View File

@@ -64,7 +64,7 @@ DNS 服务器的路径。
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
### 拨号字段

View File

@@ -4,7 +4,7 @@ icon: material/delete-clock
!!! failure "Deprecated in sing-box 1.12.0"
旧的 DNS 服务器配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-to-new-dns-servers)。
旧的 DNS 服务器配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
!!! quote "sing-box 1.9.0 中的更改"

View File

@@ -51,7 +51,7 @@ DNS 服务器的端口。
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
### 拨号字段

View File

@@ -51,7 +51,7 @@ DNS 服务器的端口。
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
### 拨号字段

View File

@@ -42,7 +42,7 @@
将拒绝的 DNS 响应缓存存储在缓存文件中。
[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#_3) 的检查结果将被缓存至过期。
[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#地址筛选字段) 的检查结果将被缓存至过期。
#### rdrc_timeout

View File

@@ -1,6 +1,6 @@
!!! quote ""
默认安装不包含 V2Ray API参阅 [安装](/zh/installation/build-from-source/#_5)。
默认安装不包含 V2Ray API参阅 [安装](/zh/installation/build-from-source/#构建标记)。
### 结构

View File

@@ -58,4 +58,4 @@ AnyTLS 填充方案行数组。
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。

View File

@@ -26,7 +26,7 @@
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
#### users

View File

@@ -104,4 +104,4 @@ base64 编码的认证密码。
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。

View File

@@ -38,7 +38,7 @@ icon: material/alert-decagram
!!! warning "与官方 Hysteria2 的区别"
官方程序支持一种名为 **userpass** 的验证方式,
本质上是将用户名与密码的组合 `<username>:<password>` 作为实际上的密码,而 sing-box 不提供此别名。
本质上是将用户名与密码的组合 `<username>:<password>` 作为实际上的密码,而 sing-box 不提供此别名。
要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。
### 监听字段
@@ -85,7 +85,7 @@ Hysteria 用户
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
#### masquerade

View File

@@ -60,4 +60,4 @@ QUIC 拥塞控制算法。
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。

View File

@@ -93,4 +93,4 @@
#### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。

View File

@@ -43,7 +43,7 @@ Trojan 用户。
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
#### fallback
@@ -61,7 +61,7 @@ TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
#### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。
#### transport

View File

@@ -75,4 +75,4 @@ QUIC 拥塞控制算法
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。

View File

@@ -4,8 +4,8 @@ icon: material/new-box
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [include_mac_address](#include_mac_address)
:material-plus: [exclude_mac_address](#exclude_mac_address)
:material-plus: [include_mac_address](#include_mac_address)
:material-plus: [exclude_mac_address](#exclude_mac_address)
!!! quote "Changes in sing-box 1.13.3"

View File

@@ -4,7 +4,7 @@ icon: material/new-box
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [include_mac_address](#include_mac_address)
:material-plus: [include_mac_address](#include_mac_address)
:material-plus: [exclude_mac_address](#exclude_mac_address)
!!! quote "sing-box 1.13.3 中的更改"

View File

@@ -48,11 +48,11 @@ VLESS 子协议。
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
#### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。
#### transport

View File

@@ -43,11 +43,11 @@ VMess 用户。
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
#### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。
#### transport

View File

@@ -59,7 +59,7 @@ AnyTLS 密码。
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
### 拨号字段

View File

@@ -4,8 +4,8 @@ icon: material/alert-decagram
!!! quote "sing-box 1.11.0 中的更改"
:material-alert-decagram: [override_address](#override_address)
:material-alert-decagram: [override_port](#override_port)
:material-delete-clock: [override_address](#override_address)
:material-delete-clock: [override_port](#override_port)
`direct` 出站直接发送请求。
@@ -29,7 +29,7 @@ icon: material/alert-decagram
!!! failure "已在 sing-box 1.11.0 废弃"
目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。
目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。
覆盖连接目标地址。
@@ -37,7 +37,7 @@ icon: material/alert-decagram
!!! failure "已在 sing-box 1.11.0 废弃"
目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。
目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。
覆盖连接目标端口。

View File

@@ -4,7 +4,7 @@ icon: material/delete-clock
!!! failure "已在 sing-box 1.11.0 废弃"
旧的特殊出站已被弃用,且将在 sing-box 1.13.0 中被移除, 参阅 [迁移指南](/migration/#migrate-legacy-special-outbounds-to-rule-actions).
旧的特殊出站已被弃用,且将在 sing-box 1.13.0 中被移除, 参阅 [迁移指南](/zh/migration/#迁移旧的特殊出站到规则动作).
`dns` 出站是一个内部 DNS 服务器。

View File

@@ -51,7 +51,7 @@ HTTP 请求的额外标头。
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
### 拨号字段

View File

@@ -134,7 +134,7 @@ base64 编码的认证密码。
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
### 拨号字段

View File

@@ -38,7 +38,7 @@
!!! warning "与官方 Hysteria2 的区别"
官方程序支持一种名为 **userpass** 的验证方式,
本质上是将用户名与密码的组合 `<username>:<password>` 作为实际上的密码,而 sing-box 不提供此别名。
本质上是将用户名与密码的组合 `<username>:<password>` 作为实际上的密码,而 sing-box 不提供此别名。
要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。
### 字段
@@ -105,7 +105,7 @@ QUIC 流量混淆器密码.
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
#### brutal_debug

View File

@@ -105,7 +105,7 @@ QUIC 拥塞控制算法。
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
只有 `server_name``certificate``certificate_path``ech` 是被支持的。

View File

@@ -17,7 +17,7 @@
!!! quote ""
选择器目前只能通过 [Clash API](/zh/configuration/experimental#clash-api) 来控制。
选择器目前只能通过 [Clash API](/zh/configuration/experimental/clash-api/) 来控制。
### 字段

View File

@@ -95,7 +95,7 @@ UDP over TCP 配置。
#### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。
### 拨号字段

View File

@@ -49,7 +49,7 @@ ShadowTLS 协议版本。
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
### 拨号字段

View File

@@ -18,7 +18,7 @@
!!! info ""
默认安装不包含嵌入式 Tor, 参阅 [安装](/zh/installation/build-from-source/#_5)。
默认安装不包含嵌入式 Tor, 参阅 [安装](/zh/installation/build-from-source/#构建标记)。
### 字段

View File

@@ -47,11 +47,11 @@ Trojan 密码。
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
#### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。
#### transport

View File

@@ -97,7 +97,7 @@ UDP 包中继模式
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
### 拨号字段

View File

@@ -57,7 +57,7 @@ VLESS 子协议。
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
#### packet_encoding
@@ -71,7 +71,7 @@ UDP 包编码,默认使用 xudp。
#### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。
#### transport

View File

@@ -82,7 +82,7 @@ VMess 用户 ID。
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
#### packet_encoding
@@ -96,7 +96,7 @@ UDP 包编码。
#### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。
#### transport

View File

@@ -4,7 +4,7 @@ icon: material/delete-clock
!!! failure "已在 sing-box 1.11.0 废弃"
WireGuard 出站已被弃用,且将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-wireguard-outbound-to-endpoint)。
WireGuard 出站已被弃用,且将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-wireguard-出站到端点)。
!!! quote "sing-box 1.11.0 中的更改"

View File

@@ -4,7 +4,7 @@ icon: material/note-remove
!!! failure "已在 sing-box 1.12.0 中被移除"
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geoip)。
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
### 结构

View File

@@ -4,7 +4,7 @@ icon: material/note-remove
!!! failure "已在 sing-box 1.12.0 中被移除"
Geosite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geosite)。
Geosite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。
### 结构

View File

@@ -17,7 +17,7 @@ icon: material/alert-decagram
!!! quote "sing-box 1.11.0 中的更改"
:material-plus: [network_strategy](#network_strategy)
:material-plus: [default_network_strategy](#default_network_strategy)
:material-plus: [default_network_type](#default_network_type)
:material-plus: [default_fallback_network_type](#default_fallback_network_type)
:material-plus: [default_fallback_delay](#default_fallback_delay)
@@ -150,7 +150,7 @@ icon: material/alert-decagram
!!! question "自 sing-box 1.12.0 起"
详情参阅 [拨号字段](/configuration/shared/dial/#domain_resolver)。
详情参阅 [拨号字段](/zh/configuration/shared/dial/#domain_resolver)。
可以被 `outbound.domain_resolver` 覆盖。
@@ -158,7 +158,7 @@ icon: material/alert-decagram
!!! question "自 sing-box 1.11.0 起"
详情参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。
详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_strategy)。
`outbound.bind_interface`, `outbound.inet4_bind_address``outbound.inet6_bind_address` 已设置时不生效。
@@ -170,16 +170,16 @@ icon: material/alert-decagram
!!! question "自 sing-box 1.11.0 起"
详情参阅 [拨号字段](/configuration/shared/dial/#default_network_type)。
详情参阅 [拨号字段](/zh/configuration/shared/dial/#default_network_type)。
#### default_fallback_network_type
!!! question "自 sing-box 1.11.0 起"
详情参阅 [拨号字段](/configuration/shared/dial/#default_fallback_network_type)。
详情参阅 [拨号字段](/zh/configuration/shared/dial/#default_fallback_network_type)。
#### default_fallback_delay
!!! question "自 sing-box 1.11.0 起"
详情参阅 [拨号字段](/configuration/shared/dial/#fallback_delay)。
详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_delay)。

View File

@@ -27,6 +27,7 @@ icon: material/new-box
:material-plus: [client](#client)
:material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
:material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source)
:material-plus: [process_path_regex](#process_path_regex)
!!! quote "sing-box 1.8.0 中的更改"
@@ -265,7 +266,7 @@ icon: material/new-box
!!! failure "已在 sing-box 1.8.0 废弃"
Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geosite)。
Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。
匹配 Geosite。
@@ -273,7 +274,7 @@ icon: material/new-box
!!! failure "已在 sing-box 1.8.0 废弃"
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
匹配源 GeoIP。
@@ -281,7 +282,7 @@ icon: material/new-box
!!! failure "已在 sing-box 1.8.0 废弃"
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
匹配 GeoIP。

View File

@@ -66,7 +66,7 @@ icon: material/new-box
目标出站的标签。
如果未指定,规则仅在来自 auto redirect 的[预匹配](/configuration/shared/pre-match/)中匹配,在其他场景中将被跳过。
如果未指定,规则仅在来自 auto redirect 的[预匹配](/zh/configuration/shared/pre-match/)中匹配,在其他场景中将被跳过。
#### route-options 字段
@@ -154,22 +154,22 @@ icon: material/new-box
#### network_strategy
详情参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。
详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_strategy)。
仅当出站为 `direct``outbound.bind_interface`, `outbound.inet4_bind_address`
`outbound.inet6_bind_address` 未设置时生效。
#### network_type
详情参阅 [拨号字段](/configuration/shared/dial/#network_type)。
详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_type)。
#### fallback_network_type
详情参阅 [拨号字段](/configuration/shared/dial/#fallback_network_type)。
详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_network_type)。
#### fallback_delay
详情参阅 [拨号字段](/configuration/shared/dial/#fallback_delay)。
详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_delay)。
#### udp_disable_domain_unmapping

View File

@@ -10,8 +10,8 @@ icon: material/new-box
!!! quote "sing-box 1.11.0 中的更改"
:material-plus: [network_type](#network_type)
:material-alert: [network_is_expensive](#network_is_expensive)
:material-alert: [network_is_constrained](#network_is_constrained)
:material-plus: [network_is_expensive](#network_is_expensive)
:material-plus: [network_is_constrained](#network_is_constrained)
### 结构

View File

@@ -10,11 +10,6 @@ CCM (Claude Code Multiplexer) service is a multiplexing service that allows you
It handles OAuth authentication with Claude's API on your local machine while allowing remote Claude Code to authenticate using Auth Tokens via the `ANTHROPIC_AUTH_TOKEN` environment variable.
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [credentials](#credentials)
:material-alert: [users](#users)
### Structure
```json
@@ -24,7 +19,6 @@ It handles OAuth authentication with Claude's API on your local machine while al
... // Listen Fields
"credential_path": "",
"credentials": [],
"usages_path": "",
"users": [],
"headers": {},
@@ -51,77 +45,6 @@ On macOS, credentials are read from the system keychain first, then fall back to
Refreshed tokens are automatically written back to the same location.
When `credential_path` points to a file, the service can start before the file exists. The credential becomes available automatically after the file is created or updated, and becomes unavailable immediately if the file is later removed or becomes invalid.
On macOS without an explicit `credential_path`, keychain changes are not watched. Automatic reload only applies to the credential file path.
Conflict with `credentials`.
#### credentials
!!! question "Since sing-box 1.14.0"
List of credential configurations for multi-credential mode.
When set, top-level `credential_path`, `usages_path`, and `detour` are forbidden. Each user must specify a `credential` tag.
Each credential has a `type` field (`default`, `balancer`, or `fallback`) and a required `tag` field.
##### Default Credential
```json
{
"tag": "a",
"credential_path": "/path/to/.credentials.json",
"usages_path": "/path/to/usages.json",
"detour": "",
"reserve_5h": 20,
"reserve_weekly": 20
}
```
A single OAuth credential file. The `type` field can be omitted (defaults to `default`). The service can start before the file exists, and reloads file updates automatically.
- `credential_path`: Path to the credentials file. Same defaults as top-level `credential_path`.
- `usages_path`: Optional usage tracking file for this credential.
- `detour`: Outbound tag for connecting to the Claude API with this credential.
- `reserve_5h`: Reserve threshold (1-99) for 5-hour window. Credential pauses at (100-N)% utilization.
- `reserve_weekly`: Reserve threshold (1-99) for weekly window. Credential pauses at (100-N)% utilization.
##### Balancer Credential
```json
{
"tag": "pool",
"type": "balancer",
"strategy": "",
"credentials": ["a", "b"],
"poll_interval": "60s"
}
```
Assigns sessions to default credentials based on the selected strategy. Sessions are sticky until the assigned credential hits a rate limit.
- `strategy`: Selection strategy. One of `least_used` `round_robin` `random`. `least_used` will be used by default.
- `credentials`: ==Required== List of default credential tags.
- `poll_interval`: How often to poll upstream usage API. Default `60s`.
##### Fallback Credential
```json
{
"tag": "backup",
"type": "fallback",
"credentials": ["a", "b"],
"poll_interval": "30s"
}
```
Uses credentials in order. Falls through to the next when the current one is exhausted.
- `credentials`: ==Required== Ordered list of default credential tags.
- `poll_interval`: How often to poll upstream usage API. Default `60s`.
#### usages_path
Path to the file for storing aggregated API usage statistics.
@@ -137,8 +60,6 @@ Statistics are organized by model, context window (200k standard vs 1M premium),
The statistics file is automatically saved every minute and upon service shutdown.
Conflict with `credentials`. In multi-credential mode, use `usages_path` on individual default credentials.
#### users
List of authorized users for token authentication.
@@ -150,8 +71,7 @@ Object format:
```json
{
"name": "",
"token": "",
"credential": ""
"token": ""
}
```
@@ -159,7 +79,6 @@ Object fields:
- `name`: Username identifier for tracking purposes.
- `token`: Bearer token for authentication. Claude Code authenticates by setting the `ANTHROPIC_AUTH_TOKEN` environment variable to their token value.
- `credential`: Credential tag to use for this user. ==Required== when `credentials` is set.
#### headers
@@ -171,8 +90,6 @@ These headers will override any existing headers with the same name.
Outbound tag for connecting to the Claude API.
Conflict with `credentials`. In multi-credential mode, use `detour` on individual default credentials.
#### tls
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
@@ -212,52 +129,3 @@ export ANTHROPIC_AUTH_TOKEN="ak-ccm-hello-world"
claude
```
### Example with Multiple Credentials
#### Server
```json
{
"services": [
{
"type": "ccm",
"listen": "0.0.0.0",
"listen_port": 8080,
"credentials": [
{
"tag": "a",
"credential_path": "/home/user/.claude-a/.credentials.json",
"usages_path": "/data/usages-a.json",
"reserve_5h": 20,
"reserve_weekly": 20
},
{
"tag": "b",
"credential_path": "/home/user/.claude-b/.credentials.json",
"reserve_5h": 10,
"reserve_weekly": 10
},
{
"tag": "pool",
"type": "balancer",
"poll_interval": "60s",
"credentials": ["a", "b"]
}
],
"users": [
{
"name": "alice",
"token": "ak-ccm-hello-world",
"credential": "pool"
},
{
"name": "bob",
"token": "ak-ccm-hello-bob",
"credential": "a"
}
]
}
]
}
```

View File

@@ -10,11 +10,6 @@ CCMClaude Code 多路复用器)服务是一个多路复用服务,允许
它在本地机器上处理与 Claude API 的 OAuth 身份验证,同时允许远程 Claude Code 通过 `ANTHROPIC_AUTH_TOKEN` 环境变量使用认证令牌进行身份验证。
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [credentials](#credentials)
:material-alert: [users](#users)
### 结构
```json
@@ -24,7 +19,6 @@ CCMClaude Code 多路复用器)服务是一个多路复用服务,允许
... // 监听字段
"credential_path": "",
"credentials": [],
"usages_path": "",
"users": [],
"headers": {},
@@ -51,77 +45,6 @@ Claude Code OAuth 凭据文件的路径。
刷新的令牌会自动写回相同位置。
`credential_path` 指向文件时,即使文件尚不存在,服务也可以启动。文件被创建或更新后,凭据会自动变为可用;如果文件之后被删除或变为无效,该凭据会立即变为不可用。
在 macOS 上如果未显式设置 `credential_path`,不会监听钥匙串变化。自动重载只作用于凭据文件路径。
`credentials` 冲突。
#### credentials
!!! question "自 sing-box 1.14.0 起"
多凭据模式的凭据配置列表。
设置后,顶层 `credential_path``usages_path``detour` 被禁止。每个用户必须指定 `credential` 标签。
每个凭据有一个 `type` 字段(`default``balancer``fallback`)和一个必填的 `tag` 字段。
##### 默认凭据
```json
{
"tag": "a",
"credential_path": "/path/to/.credentials.json",
"usages_path": "/path/to/usages.json",
"detour": "",
"reserve_5h": 20,
"reserve_weekly": 20
}
```
单个 OAuth 凭据文件。`type` 字段可以省略(默认为 `default`)。即使文件尚不存在,服务也可以启动,并会自动重载文件更新。
- `credential_path`:凭据文件的路径。默认值与顶层 `credential_path` 相同。
- `usages_path`:此凭据的可选使用跟踪文件。
- `detour`:此凭据用于连接 Claude API 的出站标签。
- `reserve_5h`5 小时窗口的保留阈值1-99。凭据在利用率达到 (100-N)% 时暂停。
- `reserve_weekly`每周窗口的保留阈值1-99。凭据在利用率达到 (100-N)% 时暂停。
##### 均衡凭据
```json
{
"tag": "pool",
"type": "balancer",
"strategy": "",
"credentials": ["a", "b"],
"poll_interval": "60s"
}
```
根据选择的策略将会话分配给默认凭据。会话保持粘性,直到分配的凭据触发速率限制。
- `strategy`:选择策略。可选值:`least_used` `round_robin` `random`。默认使用 `least_used`
- `credentials`==必填== 默认凭据标签列表。
- `poll_interval`:轮询上游使用 API 的间隔。默认 `60s`
##### 回退凭据
```json
{
"tag": "backup",
"type": "fallback",
"credentials": ["a", "b"],
"poll_interval": "30s"
}
```
按顺序使用凭据。当前凭据耗尽后切换到下一个。
- `credentials`==必填== 有序的默认凭据标签列表。
- `poll_interval`:轮询上游使用 API 的间隔。默认 `60s`
#### usages_path
用于存储聚合 API 使用统计信息的文件路径。
@@ -137,8 +60,6 @@ Claude Code OAuth 凭据文件的路径。
统计文件每分钟自动保存一次,并在服务关闭时保存。
`credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `usages_path`
#### users
用于令牌身份验证的授权用户列表。
@@ -150,8 +71,7 @@ Claude Code OAuth 凭据文件的路径。
```json
{
"name": "",
"token": "",
"credential": ""
"token": ""
}
```
@@ -159,7 +79,6 @@ Claude Code OAuth 凭据文件的路径。
- `name`:用于跟踪的用户名标识符。
- `token`:用于身份验证的 Bearer 令牌。Claude Code 通过设置 `ANTHROPIC_AUTH_TOKEN` 环境变量为其令牌值进行身份验证。
- `credential`:此用户使用的凭据标签。设置 `credentials` 时==必填==。
#### headers
@@ -171,11 +90,9 @@ Claude Code OAuth 凭据文件的路径。
用于连接 Claude API 的出站标签。
`credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `detour`
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
### 示例
@@ -212,52 +129,3 @@ export ANTHROPIC_AUTH_TOKEN="ak-ccm-hello-world"
claude
```
### 多凭据示例
#### 服务端
```json
{
"services": [
{
"type": "ccm",
"listen": "0.0.0.0",
"listen_port": 8080,
"credentials": [
{
"tag": "a",
"credential_path": "/home/user/.claude-a/.credentials.json",
"usages_path": "/data/usages-a.json",
"reserve_5h": 20,
"reserve_weekly": 20
},
{
"tag": "b",
"credential_path": "/home/user/.claude-b/.credentials.json",
"reserve_5h": 10,
"reserve_weekly": 10
},
{
"tag": "pool",
"type": "balancer",
"poll_interval": "60s",
"credentials": ["a", "b"]
}
],
"users": [
{
"name": "alice",
"token": "ak-ccm-hello-world",
"credential": "pool"
},
{
"name": "bob",
"token": "ak-ccm-hello-bob",
"credential": "a"
}
]
}
]
}
```

View File

@@ -36,7 +36,7 @@ DERP 服务是一个 Tailscale DERP 服务器,类似于 [derper](https://pkg.g
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
#### config_path
@@ -96,7 +96,7 @@ Derper 配置文件路径。
- `server`**必填** DERP 服务器地址。
- `server_port`**必填** DERP 服务器端口。
- `host`:自定义 DERP 主机名。
- `tls`[TLS](/zh/configuration/shared/tls/#outbound)
- `tls`[TLS](/zh/configuration/shared/tls/#出站)
- `拨号字段`[拨号字段](/zh/configuration/shared/dial/)
#### mesh_psk

View File

@@ -10,11 +10,6 @@ OCM (OpenAI Codex Multiplexer) service is a multiplexing service that allows you
It handles OAuth authentication with OpenAI's API on your local machine while allowing remote clients to authenticate using custom tokens.
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [credentials](#credentials)
:material-alert: [users](#users)
### Structure
```json
@@ -24,7 +19,6 @@ It handles OAuth authentication with OpenAI's API on your local machine while al
... // Listen Fields
"credential_path": "",
"credentials": [],
"usages_path": "",
"users": [],
"headers": {},
@@ -49,75 +43,6 @@ If not specified, defaults to:
Refreshed tokens are automatically written back to the same location.
When `credential_path` points to a file, the service can start before the file exists. The credential becomes available automatically after the file is created or updated, and becomes unavailable immediately if the file is later removed or becomes invalid.
Conflict with `credentials`.
#### credentials
!!! question "Since sing-box 1.14.0"
List of credential configurations for multi-credential mode.
When set, top-level `credential_path`, `usages_path`, and `detour` are forbidden. Each user must specify a `credential` tag.
Each credential has a `type` field (`default`, `balancer`, or `fallback`) and a required `tag` field.
##### Default Credential
```json
{
"tag": "a",
"credential_path": "/path/to/auth.json",
"usages_path": "/path/to/usages.json",
"detour": "",
"reserve_5h": 20,
"reserve_weekly": 20
}
```
A single OAuth credential file. The `type` field can be omitted (defaults to `default`). The service can start before the file exists, and reloads file updates automatically.
- `credential_path`: Path to the credentials file. Same defaults as top-level `credential_path`.
- `usages_path`: Optional usage tracking file for this credential.
- `detour`: Outbound tag for connecting to the OpenAI API with this credential.
- `reserve_5h`: Reserve threshold (1-99) for primary rate limit window. Credential pauses at (100-N)% utilization.
- `reserve_weekly`: Reserve threshold (1-99) for secondary (weekly) rate limit window. Credential pauses at (100-N)% utilization.
##### Balancer Credential
```json
{
"tag": "pool",
"type": "balancer",
"strategy": "",
"credentials": ["a", "b"],
"poll_interval": "60s"
}
```
Assigns sessions to default credentials based on the selected strategy. Sessions are sticky until the assigned credential hits a rate limit.
- `strategy`: Selection strategy. One of `least_used` `round_robin` `random`. `least_used` will be used by default.
- `credentials`: ==Required== List of default credential tags.
- `poll_interval`: How often to poll upstream usage API. Default `60s`.
##### Fallback Credential
```json
{
"tag": "backup",
"type": "fallback",
"credentials": ["a", "b"],
"poll_interval": "30s"
}
```
Uses credentials in order. Falls through to the next when the current one is exhausted.
- `credentials`: ==Required== Ordered list of default credential tags.
- `poll_interval`: How often to poll upstream usage API. Default `60s`.
#### usages_path
Path to the file for storing aggregated API usage statistics.
@@ -133,8 +58,6 @@ Statistics are organized by model and optionally by user when authentication is
The statistics file is automatically saved every minute and upon service shutdown.
Conflict with `credentials`. In multi-credential mode, use `usages_path` on individual default credentials.
#### users
List of authorized users for token authentication.
@@ -146,8 +69,7 @@ Object format:
```json
{
"name": "",
"token": "",
"credential": ""
"token": ""
}
```
@@ -155,7 +77,6 @@ Object fields:
- `name`: Username identifier for tracking purposes.
- `token`: Bearer token for authentication. Clients authenticate by setting the `Authorization: Bearer <token>` header.
- `credential`: Credential tag to use for this user. ==Required== when `credentials` is set.
#### headers
@@ -167,8 +88,6 @@ These headers will override any existing headers with the same name.
Outbound tag for connecting to the OpenAI API.
Conflict with `credentials`. In multi-credential mode, use `detour` on individual default credentials.
#### tls
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
@@ -264,52 +183,3 @@ Then run:
```bash
codex --profile ocm
```
### Example with Multiple Credentials
#### Server
```json
{
"services": [
{
"type": "ocm",
"listen": "0.0.0.0",
"listen_port": 8080,
"credentials": [
{
"tag": "a",
"credential_path": "/home/user/.codex-a/auth.json",
"usages_path": "/data/usages-a.json",
"reserve_5h": 20,
"reserve_weekly": 20
},
{
"tag": "b",
"credential_path": "/home/user/.codex-b/auth.json",
"reserve_5h": 10,
"reserve_weekly": 10
},
{
"tag": "pool",
"type": "balancer",
"poll_interval": "60s",
"credentials": ["a", "b"]
}
],
"users": [
{
"name": "alice",
"token": "sk-ocm-hello-world",
"credential": "pool"
},
{
"name": "bob",
"token": "sk-ocm-hello-bob",
"credential": "a"
}
]
}
]
}
```

View File

@@ -10,11 +10,6 @@ OCMOpenAI Codex 多路复用器)服务是一个多路复用服务,允许
它在本地机器上处理与 OpenAI API 的 OAuth 身份验证,同时允许远程客户端使用自定义令牌进行身份验证。
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [credentials](#credentials)
:material-alert: [users](#users)
### 结构
```json
@@ -24,7 +19,6 @@ OCMOpenAI Codex 多路复用器)服务是一个多路复用服务,允许
... // 监听字段
"credential_path": "",
"credentials": [],
"usages_path": "",
"users": [],
"headers": {},
@@ -49,75 +43,6 @@ OpenAI OAuth 凭据文件的路径。
刷新的令牌会自动写回相同位置。
`credential_path` 指向文件时,即使文件尚不存在,服务也可以启动。文件被创建或更新后,凭据会自动变为可用;如果文件之后被删除或变为无效,该凭据会立即变为不可用。
`credentials` 冲突。
#### credentials
!!! question "自 sing-box 1.14.0 起"
多凭据模式的凭据配置列表。
设置后,顶层 `credential_path``usages_path``detour` 被禁止。每个用户必须指定 `credential` 标签。
每个凭据有一个 `type` 字段(`default``balancer``fallback`)和一个必填的 `tag` 字段。
##### 默认凭据
```json
{
"tag": "a",
"credential_path": "/path/to/auth.json",
"usages_path": "/path/to/usages.json",
"detour": "",
"reserve_5h": 20,
"reserve_weekly": 20
}
```
单个 OAuth 凭据文件。`type` 字段可以省略(默认为 `default`)。即使文件尚不存在,服务也可以启动,并会自动重载文件更新。
- `credential_path`:凭据文件的路径。默认值与顶层 `credential_path` 相同。
- `usages_path`:此凭据的可选使用跟踪文件。
- `detour`:此凭据用于连接 OpenAI API 的出站标签。
- `reserve_5h`主要速率限制窗口的保留阈值1-99。凭据在利用率达到 (100-N)% 时暂停。
- `reserve_weekly`次要每周速率限制窗口的保留阈值1-99。凭据在利用率达到 (100-N)% 时暂停。
##### 均衡凭据
```json
{
"tag": "pool",
"type": "balancer",
"strategy": "",
"credentials": ["a", "b"],
"poll_interval": "60s"
}
```
根据选择的策略将会话分配给默认凭据。会话保持粘性,直到分配的凭据触发速率限制。
- `strategy`:选择策略。可选值:`least_used` `round_robin` `random`。默认使用 `least_used`
- `credentials`==必填== 默认凭据标签列表。
- `poll_interval`:轮询上游使用 API 的间隔。默认 `60s`
##### 回退凭据
```json
{
"tag": "backup",
"type": "fallback",
"credentials": ["a", "b"],
"poll_interval": "30s"
}
```
按顺序使用凭据。当前凭据耗尽后切换到下一个。
- `credentials`==必填== 有序的默认凭据标签列表。
- `poll_interval`:轮询上游使用 API 的间隔。默认 `60s`
#### usages_path
用于存储聚合 API 使用统计信息的文件路径。
@@ -133,8 +58,6 @@ OpenAI OAuth 凭据文件的路径。
统计文件每分钟自动保存一次,并在服务关闭时保存。
`credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `usages_path`
#### users
用于令牌身份验证的授权用户列表。
@@ -146,8 +69,7 @@ OpenAI OAuth 凭据文件的路径。
```json
{
"name": "",
"token": "",
"credential": ""
"token": ""
}
```
@@ -155,7 +77,6 @@ OpenAI OAuth 凭据文件的路径。
- `name`:用于跟踪的用户名标识符。
- `token`:用于身份验证的 Bearer 令牌。客户端通过设置 `Authorization: Bearer <token>` 头进行身份验证。
- `credential`:此用户使用的凭据标签。设置 `credentials` 时==必填==。
#### headers
@@ -167,11 +88,9 @@ OpenAI OAuth 凭据文件的路径。
用于连接 OpenAI API 的出站标签。
`credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `detour`
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
### 示例
@@ -265,52 +184,3 @@ model_provider = "ocm"
```bash
codex --profile ocm
```
### 多凭据示例
#### 服务端
```json
{
"services": [
{
"type": "ocm",
"listen": "0.0.0.0",
"listen_port": 8080,
"credentials": [
{
"tag": "a",
"credential_path": "/home/user/.codex-a/auth.json",
"usages_path": "/data/usages-a.json",
"reserve_5h": 20,
"reserve_weekly": 20
},
{
"tag": "b",
"credential_path": "/home/user/.codex-b/auth.json",
"reserve_5h": 10,
"reserve_weekly": 10
},
{
"tag": "pool",
"type": "balancer",
"poll_interval": "60s",
"credentials": ["a", "b"]
}
],
"users": [
{
"name": "alice",
"token": "sk-ocm-hello-world",
"credential": "pool"
},
{
"name": "bob",
"token": "sk-ocm-hello-bob",
"credential": "a"
}
]
}
]
}
```

View File

@@ -55,4 +55,4 @@ SSM API 服务是一个用于管理 Shadowsocks 服务器的 RESTful API 服务
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。

View File

@@ -173,7 +173,7 @@ TCP keep alive 间隔。
用于设置解析域名的域名解析器。
此选项的格式与 [路由 DNS 规则动作](/configuration/dns/rule_action/#route) 相同,但不包含 `action` 字段。
此选项的格式与 [路由 DNS 规则动作](/zh/configuration/dns/rule_action/#route) 相同,但不包含 `action` 字段。
若直接将此选项设置为字符串,则等同于设置该选项的 `server` 字段。
@@ -246,7 +246,7 @@ TCP keep alive 间隔。
!!! failure "已在 sing-box 1.12.0 废弃"
`domain_strategy` 已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-outbound-domain-strategy-option-to-domain-resolver)。
`domain_strategy` 已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移出站域名策略选项到域名解析器)。
可选值:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`

View File

@@ -145,13 +145,13 @@ UDP NAT 过期时间。
如果设置,连接将被转发到指定的入站。
需要目标入站支持,参阅 [注入支持](/zh/configuration/inbound/#_3)。
需要目标入站支持,参阅 [注入支持](/zh/configuration/inbound/#字段)。
#### sniff
!!! failure "已在 sing-box 1.11.0 废弃"
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作).
启用协议探测。
@@ -171,7 +171,7 @@ UDP NAT 过期时间。
!!! failure "已在 sing-box 1.11.0 废弃"
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作).
探测超时时间。
@@ -181,7 +181,7 @@ UDP NAT 过期时间。
!!! failure "已在 sing-box 1.11.0 废弃"
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作).
可选值: `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`
@@ -193,7 +193,7 @@ UDP NAT 过期时间。
!!! failure "已在 sing-box 1.11.0 废弃"
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作).
如果启用,对于地址为域的 UDP 代理请求,将在响应中发送原始包地址而不是映射的域。

View File

@@ -22,13 +22,13 @@ icon: material/new-box
以 TCP RST / ICMP 不可达拒绝。
详情参阅 [reject](/configuration/route/rule_action/#reject)。
详情参阅 [reject](/zh/configuration/route/rule_action/#reject)。
#### route
将 ICMP 连接路由到指定出站以直接回复。
详情参阅 [route](/configuration/route/rule_action/#route)。
详情参阅 [route](/zh/configuration/route/rule_action/#route)。
#### bypass
@@ -44,4 +44,4 @@ icon: material/new-box
对于其他所有场景,指定了 `outbound` 的 bypass 行为与 `route` 相同。
详情参阅 [bypass](/configuration/route/rule_action/#bypass)。
详情参阅 [bypass](/zh/configuration/route/rule_action/#bypass)。

View File

@@ -426,7 +426,7 @@ echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/
其实现行为无法通过简单复制握手格式来复现,其行为细节必然存在差异,使得检测成为可能。
此外,此库缺乏积极维护,且代码质量较差,不建议用于反审查场景。
如需 TLS 指纹抵抗,请改用 [NaiveProxy](/configuration/inbound/naive/)。
如需 TLS 指纹抵抗,请改用 [NaiveProxy](/zh/configuration/inbound/naive/)。
uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻力。

View File

@@ -144,7 +144,7 @@ HTTP 请求的额外标头
!!! note ""
默认安装不包含标准 gRPC (兼容性好,但性能较差), 参阅 [安装](/zh/installation/build-from-source/#_5)。
默认安装不包含标准 gRPC (兼容性好,但性能较差), 参阅 [安装](/zh/installation/build-from-source/#构建标记)。
```json
{

View File

@@ -4,7 +4,7 @@ icon: material/new-box
# Wi-Fi 状态
!!! quote "sing-box 1.13.0 的变更"
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: Linux 支持
:material-plus: Windows 支持

View File

@@ -7,7 +7,7 @@ icon: material/delete-alert
#### 旧的 DNS 服务器格式
DNS 服务器已重构,
参阅 [迁移指南](/migration/#migrate-to-new-dns-servers).
参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式).
对旧格式的兼容性将在 sing-box 1.14.0 中被移除。
@@ -15,7 +15,7 @@ DNS 服务器已重构,
旧的 `outbound` DNS 规则已废弃,
且可被拨号字段代替,
参阅 [迁移指南](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver).
参阅 [迁移指南](/zh/migration/#迁移-outbound-dns-规则项到域解析选项).
#### 旧的 ECH 字段
@@ -31,28 +31,28 @@ ECH 支持已在 sing-box 1.12.0 迁移至使用标准库,但标准库不支
#### 旧的特殊出站
旧的特殊出站(`block` / `dns`)已废弃且可以通过规则动作替代,
参阅 [迁移指南](/migration/#migrate-legacy-special-outbounds-to-rule-actions)。
参阅 [迁移指南](/zh/migration/#迁移旧的特殊出站到规则动作)。
旧字段将在 sing-box 1.13.0 中被移除。
#### 旧的入站字段
旧的入站字段(`inbound.<sniff/domain_strategy/...>`)已废弃且可以通过规则动作替代,
参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions)。
参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作)。
旧字段将在 sing-box 1.13.0 中被移除。
#### direct 出站中的目标地址覆盖字段
direct 出站中的目标地址覆盖字段(`override_address` / `override_port`)已废弃且可以通过规则动作替代,
参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。
参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。
旧字段将在 sing-box 1.13.0 中被移除。
#### WireGuard 出站
WireGuard 出站已废弃且可以通过端点替代,
参阅 [迁移指南](/migration/#migrate-wireguard-outbound-to-endpoint)。
参阅 [迁移指南](/zh/migration/#迁移-wireguard-出站到端点)。
旧出站将在 sing-box 1.13.0 中被移除。
@@ -86,7 +86,7 @@ GSO 对透明代理场景没有优势,已废弃且在 TUN 中不再起作用
#### Clash API 中的 Cache file 及相关功能
Clash API 中的 `cache_file` 及相关功能已废弃且已迁移到独立的 `cache_file` 设置,
参阅 [迁移指南](/zh/migration/#clash-api)。
参阅 [迁移指南](/zh/migration/#将缓存文件从-clash-api-迁移到独立选项)。
#### GeoIP
@@ -96,7 +96,7 @@ maxmind GeoIP 国家数据库作为 IP 分类数据库,不完全适合流量
且现有的实现均存在内存使用大与管理困难的问题。
sing-box 1.8.0 引入了[规则集](/zh/configuration/rule-set/)
可以完全替代 GeoIP 参阅 [迁移指南](/zh/migration/#geoip)。
可以完全替代 GeoIP 参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
#### Geosite
@@ -106,7 +106,7 @@ Geosite即由 V2Ray 维护的 domain-list-community 项目,作为早期流
存在着包括缺少维护、规则不准确和管理困难内的大量问题。
sing-box 1.8.0 引入了[规则集](/zh/configuration/rule-set/)
可以完全替代 Geosite参阅 [迁移指南](/zh/migration/#geosite)。
可以完全替代 Geosite参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。
## 1.6.0

View File

@@ -51,20 +51,20 @@ go build -tags "tag_a tag_b" ./cmd/sing-box
| 构建标记 | 默认启动 | 说明 |
|------------------------------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `with_quic` | :material-check: | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/configuration/dns/server/), [Naive inbound](/configuration/inbound/naive/), [Hysteria Inbound](/configuration/inbound/hysteria/), [Hysteria Outbound](/configuration/outbound/hysteria/) and [V2Ray Transport#QUIC](/configuration/shared/v2ray-transport#quic). |
| `with_grpc` | :material-close: | Build with standard gRPC support, see [V2Ray Transport#gRPC](/configuration/shared/v2ray-transport#grpc). |
| `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/configuration/dns/server/). |
| `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/configuration/outbound/wireguard/). |
| `with_utls` | :material-check: | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/configuration/shared/tls#utls). |
| `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls/). |
| `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). |
| `with_v2ray_api` | :material-close: | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). |
| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). |
| `with_embedded_tor` (CGO required) | :material-close: | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). |
| `with_tailscale` | :material-check: | 构建 Tailscale 支持,参阅 [Tailscale 端点](/configuration/endpoint/tailscale)。 |
| `with_quic` | :material-check: | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/zh/configuration/dns/server/), [Naive inbound](/zh/configuration/inbound/naive/), [Hysteria Inbound](/zh/configuration/inbound/hysteria/), [Hysteria Outbound](/zh/configuration/outbound/hysteria/) and [V2Ray Transport#QUIC](/zh/configuration/shared/v2ray-transport#quic). |
| `with_grpc` | :material-close: | Build with standard gRPC support, see [V2Ray Transport#gRPC](/zh/configuration/shared/v2ray-transport#grpc). |
| `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/zh/configuration/dns/server/). |
| `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/zh/configuration/outbound/wireguard/). |
| `with_utls` | :material-check: | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/zh/configuration/shared/tls#utls). |
| `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/zh/configuration/shared/tls/). |
| `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/zh/configuration/experimental#clash-api-fields). |
| `with_v2ray_api` | :material-close: | Build with V2Ray API support, see [Experimental](/zh/configuration/experimental#v2ray-api-fields). |
| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/zh/configuration/inbound/tun#stack) and [WireGuard outbound](/zh/configuration/outbound/wireguard#system_interface). |
| `with_embedded_tor` (CGO required) | :material-close: | Build with embedded Tor support, see [Tor outbound](/zh/configuration/outbound/tor/). |
| `with_tailscale` | :material-check: | 构建 Tailscale 支持,参阅 [Tailscale 端点](/zh/configuration/endpoint/tailscale)。 |
| `with_ccm` | :material-check: | 构建 Claude Code Multiplexer 服务支持。 |
| `with_ocm` | :material-check: | 构建 OpenAI Codex Multiplexer 服务支持。 |
| `with_naive_outbound` | :material-check: | 构建 NaiveProxy 出站支持,参阅 [NaiveProxy 出站](/configuration/outbound/naive/)。 |
| `with_naive_outbound` | :material-check: | 构建 NaiveProxy 出站支持,参阅 [NaiveProxy 出站](/zh/configuration/outbound/naive/)。 |
| `badlinkname` | :material-check: | 启用 `go:linkname` 以访问标准库内部函数。Go 标准库未提供本项目需要的许多底层 API且在外部重新实现不切实际。用于 kTLS内核 TLS 卸载)和原始 TLS 记录操作。 |
| `tfogo_checklinkname0` | :material-check: | `badlinkname` 的伴随标记。Go 1.23+ 链接器强制限制 `go:linkname` 使用;此标记表示构建使用 `-checklinkname=0` 以绕过该限制。 |

View File

@@ -518,9 +518,9 @@ DNS 服务器已经重构。
!!! info "参考"
[DNS 规则](/configuration/dns/rule/#outbound) /
[拨号字段](/configuration/shared/dial/#domain_resolver) /
[路由](/configuration/route/#default_domain_resolver)
[DNS 规则](/zh/configuration/dns/rule/#outbound) /
[拨号字段](/zh/configuration/shared/dial/#domain_resolver) /
[路由](/zh/configuration/route/#default_domain_resolver)
=== ":material-card-remove: 废弃的"
@@ -596,7 +596,7 @@ DNS 服务器已经重构。
!!! info "参考"
[拨号字段](/configuration/shared/dial/#domain_strategy)
[拨号字段](/zh/configuration/shared/dial/#domain_strategy)
=== ":material-card-remove: 弃用的"

View File

@@ -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 {

View File

@@ -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{

View 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
}

View File

@@ -31,10 +31,18 @@ type NeighborUpdateListener 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 {

View File

@@ -202,10 +202,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
}

6
go.mod
View File

@@ -35,16 +35,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.20260311155444-d39eb42a9f69
github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde
github.com/sagernet/sing-mux v0.3.4
github.com/sagernet/sing-quic v0.6.0
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-0.20260311132553-5485872f601f
github.com/sagernet/sing-tun v0.8.7-0.20260323120017-8eb4e8acfc2d
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

12
go.sum
View File

@@ -236,8 +236,8 @@ 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.20260311155444-d39eb42a9f69 h1:h6UF2emeydBQMAso99Nr3APV6YustOs+JszVuCkcFy0=
github.com/sagernet/sing v0.8.3-0.20260311155444-d39eb42a9f69/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
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-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=
@@ -248,14 +248,14 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq
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-0.20260311132553-5485872f601f h1:uj3rzedphq1AiL0PpuVoob5RtKsPBcMRd8aqo+q0rqA=
github.com/sagernet/sing-tun v0.8.3-0.20260311132553-5485872f601f/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs=
github.com/sagernet/sing-tun v0.8.7-0.20260323120017-8eb4e8acfc2d h1:vi0j6301f6H8t2GYgAC2PA2AdnGdMwkP34B4+N03Qt4=
github.com/sagernet/sing-tun v0.8.7-0.20260323120017-8eb4e8acfc2d/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=

View File

@@ -168,11 +168,7 @@ func FormatDuration(duration time.Duration) string {
return F.ToString(duration.Milliseconds(), "ms")
} else if duration < time.Minute {
return F.ToString(int64(duration.Seconds()), ".", int64(duration.Seconds()*100)%100, "s")
} else if duration < time.Hour {
return F.ToString(int64(duration.Minutes()), "m", int64(duration.Seconds())%60, "s")
} else if duration < 24*time.Hour {
return F.ToString(int64(duration.Hours()), "h", int64(duration.Minutes())%60, "m")
} else {
return F.ToString(int64(duration.Hours())/24, "d", int64(duration.Hours())%24, "h")
return F.ToString(int64(duration.Minutes()), "m", int64(duration.Seconds())%60, "s")
}
}

View File

@@ -183,6 +183,10 @@ nav:
- CCM: configuration/service/ccm.md
- OCM: configuration/service/ocm.md
markdown_extensions:
- toc:
slugify: !!python/object/apply:pymdownx.slugs.slugify
kwds:
case: lower
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.superfences

View File

@@ -1,9 +1,6 @@
package option
import (
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/json/badoption"
)
@@ -11,7 +8,6 @@ type CCMServiceOptions struct {
ListenOptions
InboundTLSOptionsContainer
CredentialPath string `json:"credential_path,omitempty"`
Credentials []CCMCredential `json:"credentials,omitempty"`
Users []CCMUser `json:"users,omitempty"`
Headers badoption.HTTPHeader `json:"headers,omitempty"`
Detour string `json:"detour,omitempty"`
@@ -19,94 +15,6 @@ type CCMServiceOptions struct {
}
type CCMUser struct {
Name string `json:"name,omitempty"`
Token string `json:"token,omitempty"`
Credential string `json:"credential,omitempty"`
ExternalCredential string `json:"external_credential,omitempty"`
AllowExternalUsage bool `json:"allow_external_usage,omitempty"`
}
type _CCMCredential struct {
Type string `json:"type,omitempty"`
Tag string `json:"tag"`
DefaultOptions CCMDefaultCredentialOptions `json:"-"`
ExternalOptions CCMExternalCredentialOptions `json:"-"`
BalancerOptions CCMBalancerCredentialOptions `json:"-"`
FallbackOptions CCMFallbackCredentialOptions `json:"-"`
}
type CCMCredential _CCMCredential
func (c CCMCredential) MarshalJSON() ([]byte, error) {
var v any
switch c.Type {
case "", "default":
c.Type = ""
v = c.DefaultOptions
case "external":
v = c.ExternalOptions
case "balancer":
v = c.BalancerOptions
case "fallback":
v = c.FallbackOptions
default:
return nil, E.New("unknown credential type: ", c.Type)
}
return badjson.MarshallObjects((_CCMCredential)(c), v)
}
func (c *CCMCredential) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_CCMCredential)(c))
if err != nil {
return err
}
if c.Tag == "" {
return E.New("missing credential tag")
}
var v any
switch c.Type {
case "", "default":
c.Type = "default"
v = &c.DefaultOptions
case "external":
v = &c.ExternalOptions
case "balancer":
v = &c.BalancerOptions
case "fallback":
v = &c.FallbackOptions
default:
return E.New("unknown credential type: ", c.Type)
}
return badjson.UnmarshallExcluded(bytes, (*_CCMCredential)(c), v)
}
type CCMDefaultCredentialOptions struct {
CredentialPath string `json:"credential_path,omitempty"`
UsagesPath string `json:"usages_path,omitempty"`
Detour string `json:"detour,omitempty"`
Reserve5h uint8 `json:"reserve_5h"`
ReserveWeekly uint8 `json:"reserve_weekly"`
Limit5h uint8 `json:"limit_5h,omitempty"`
LimitWeekly uint8 `json:"limit_weekly,omitempty"`
}
type CCMBalancerCredentialOptions struct {
Strategy string `json:"strategy,omitempty"`
Credentials badoption.Listable[string] `json:"credentials"`
PollInterval badoption.Duration `json:"poll_interval,omitempty"`
}
type CCMExternalCredentialOptions struct {
URL string `json:"url,omitempty"`
ServerOptions
Token string `json:"token"`
Reverse bool `json:"reverse,omitempty"`
Detour string `json:"detour,omitempty"`
UsagesPath string `json:"usages_path,omitempty"`
PollInterval badoption.Duration `json:"poll_interval,omitempty"`
}
type CCMFallbackCredentialOptions struct {
Credentials badoption.Listable[string] `json:"credentials"`
PollInterval badoption.Duration `json:"poll_interval,omitempty"`
Name string `json:"name,omitempty"`
Token string `json:"token,omitempty"`
}

View File

@@ -339,7 +339,7 @@ func (o DNSServerAddressOptions) Build() M.Socksaddr {
}
func (o DNSServerAddressOptions) ServerIsDomain() bool {
return M.IsDomainName(o.Server)
return o.Build().IsDomain()
}
func (o *DNSServerAddressOptions) TakeServerOptions() ServerOptions {

View File

@@ -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
}

View File

@@ -1,9 +1,6 @@
package option
import (
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/json/badoption"
)
@@ -11,7 +8,6 @@ type OCMServiceOptions struct {
ListenOptions
InboundTLSOptionsContainer
CredentialPath string `json:"credential_path,omitempty"`
Credentials []OCMCredential `json:"credentials,omitempty"`
Users []OCMUser `json:"users,omitempty"`
Headers badoption.HTTPHeader `json:"headers,omitempty"`
Detour string `json:"detour,omitempty"`
@@ -19,94 +15,6 @@ type OCMServiceOptions struct {
}
type OCMUser struct {
Name string `json:"name,omitempty"`
Token string `json:"token,omitempty"`
Credential string `json:"credential,omitempty"`
ExternalCredential string `json:"external_credential,omitempty"`
AllowExternalUsage bool `json:"allow_external_usage,omitempty"`
}
type _OCMCredential struct {
Type string `json:"type,omitempty"`
Tag string `json:"tag"`
DefaultOptions OCMDefaultCredentialOptions `json:"-"`
ExternalOptions OCMExternalCredentialOptions `json:"-"`
BalancerOptions OCMBalancerCredentialOptions `json:"-"`
FallbackOptions OCMFallbackCredentialOptions `json:"-"`
}
type OCMCredential _OCMCredential
func (c OCMCredential) MarshalJSON() ([]byte, error) {
var v any
switch c.Type {
case "", "default":
c.Type = ""
v = c.DefaultOptions
case "external":
v = c.ExternalOptions
case "balancer":
v = c.BalancerOptions
case "fallback":
v = c.FallbackOptions
default:
return nil, E.New("unknown credential type: ", c.Type)
}
return badjson.MarshallObjects((_OCMCredential)(c), v)
}
func (c *OCMCredential) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_OCMCredential)(c))
if err != nil {
return err
}
if c.Tag == "" {
return E.New("missing credential tag")
}
var v any
switch c.Type {
case "", "default":
c.Type = "default"
v = &c.DefaultOptions
case "external":
v = &c.ExternalOptions
case "balancer":
v = &c.BalancerOptions
case "fallback":
v = &c.FallbackOptions
default:
return E.New("unknown credential type: ", c.Type)
}
return badjson.UnmarshallExcluded(bytes, (*_OCMCredential)(c), v)
}
type OCMDefaultCredentialOptions struct {
CredentialPath string `json:"credential_path,omitempty"`
UsagesPath string `json:"usages_path,omitempty"`
Detour string `json:"detour,omitempty"`
Reserve5h uint8 `json:"reserve_5h"`
ReserveWeekly uint8 `json:"reserve_weekly"`
Limit5h uint8 `json:"limit_5h,omitempty"`
LimitWeekly uint8 `json:"limit_weekly,omitempty"`
}
type OCMBalancerCredentialOptions struct {
Strategy string `json:"strategy,omitempty"`
Credentials badoption.Listable[string] `json:"credentials"`
PollInterval badoption.Duration `json:"poll_interval,omitempty"`
}
type OCMExternalCredentialOptions struct {
URL string `json:"url,omitempty"`
ServerOptions
Token string `json:"token"`
Reverse bool `json:"reverse,omitempty"`
Detour string `json:"detour,omitempty"`
UsagesPath string `json:"usages_path,omitempty"`
PollInterval badoption.Duration `json:"poll_interval,omitempty"`
}
type OCMFallbackCredentialOptions struct {
Credentials badoption.Listable[string] `json:"credentials"`
PollInterval badoption.Duration `json:"poll_interval,omitempty"`
Name string `json:"name,omitempty"`
Token string `json:"token,omitempty"`
}

View File

@@ -155,7 +155,7 @@ func (o ServerOptions) Build() M.Socksaddr {
}
func (o ServerOptions) ServerIsDomain() bool {
return M.IsDomainName(o.Server)
return o.Build().IsDomain()
}
func (o *ServerOptions) TakeServerOptions() ServerOptions {

View File

@@ -61,7 +61,7 @@ func (d DERPVerifyClientURLOptions) ServerIsDomain() bool {
if err != nil {
return false
}
return M.IsDomainName(verifyURL.Host)
return M.ParseSocksaddr(verifyURL.Hostname()).IsDomain()
}
func (d DERPVerifyClientURLOptions) MarshalJSON() ([]byte, error) {

Some files were not shown because too many files have changed in this diff Show More