mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-13 20:28:32 +10:00
Compare commits
41 Commits
870d9d3a57
...
renovate/g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9f911316e | ||
|
|
5b29fd3be4 | ||
|
|
016e5e1b12 | ||
|
|
92b3bde862 | ||
|
|
64f7349fca | ||
|
|
527372ba74 | ||
|
|
c6c07cb52f | ||
|
|
913f033d1a | ||
|
|
688f8cc4ef | ||
|
|
de51879ae9 | ||
|
|
2943e8e5f0 | ||
|
|
e2b2af8322 | ||
|
|
c8d8d0a3e7 | ||
|
|
8cd7713ca9 | ||
|
|
566abb00cd | ||
|
|
ae7550b465 | ||
|
|
63d4cdffef | ||
|
|
5516d7b045 | ||
|
|
c639c27cdb | ||
|
|
f0022f59a2 | ||
|
|
9e7d863ee7 | ||
|
|
d5c6c6aed2 | ||
|
|
4d89d732e2 | ||
|
|
f6821be8a3 | ||
|
|
03b01efe49 | ||
|
|
16aeba8ec0 | ||
|
|
283a5aacee | ||
|
|
8d852bba9b | ||
|
|
29c8794f45 | ||
|
|
c8d593503f | ||
|
|
a8934be7cd | ||
|
|
7aef716ebc | ||
|
|
7df171ff20 | ||
|
|
46eda3e96f | ||
|
|
727a9d18d6 | ||
|
|
20f60b8c7b | ||
|
|
84b0ddff7f | ||
|
|
811ea13b73 | ||
|
|
bdb90f0a01 | ||
|
|
c9ab6458fa | ||
|
|
16a249f672 |
26
.github/workflows/build.yml
vendored
26
.github/workflows/build.yml
vendored
@@ -41,13 +41,13 @@ jobs:
|
||||
version: ${{ steps.outputs.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.25.8
|
||||
go-version: ~1.26.0
|
||||
- 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
if: ${{ ! matrix.legacy_win7 }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.25.8
|
||||
go-version: ~1.26.0
|
||||
- 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
@@ -551,7 +551,7 @@ jobs:
|
||||
- { arch: arm64, naive: true }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
@@ -634,14 +634,14 @@ jobs:
|
||||
- calculate_version
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.25.8
|
||||
go-version: ~1.26.0
|
||||
- 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.25.8
|
||||
go-version: ~1.26.0
|
||||
- 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
@@ -830,7 +830,7 @@ jobs:
|
||||
if: matrix.if
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.25.8
|
||||
go-version: ~1.26.0
|
||||
- name: Set tag
|
||||
if: matrix.if
|
||||
run: |-
|
||||
@@ -976,7 +976,7 @@ jobs:
|
||||
- build_apple
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Cache ghr
|
||||
|
||||
6
.github/workflows/docker.yml
vendored
6
.github/workflows/docker.yml
vendored
@@ -49,14 +49,14 @@ jobs:
|
||||
echo "ref=$ref"
|
||||
echo "ref=$ref" >> $GITHUB_OUTPUT
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
ref: ${{ steps.ref.outputs.ref }}
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.25.8
|
||||
go-version: ~1.26.0
|
||||
- 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
ref: ${{ steps.ref.outputs.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
|
||||
10
.github/workflows/linux.yml
vendored
10
.github/workflows/linux.yml
vendored
@@ -29,13 +29,13 @@ jobs:
|
||||
version: ${{ steps.outputs.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.25.8
|
||||
go-version: ~1.26.0
|
||||
- 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.25.8
|
||||
go-version: ~1.26.0
|
||||
- name: Clone cronet-go
|
||||
if: matrix.naive
|
||||
run: |
|
||||
@@ -236,7 +236,7 @@ jobs:
|
||||
- build
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set tag
|
||||
|
||||
@@ -104,10 +104,6 @@ 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
|
||||
|
||||
@@ -51,11 +51,11 @@ type FindConnectionOwnerRequest struct {
|
||||
}
|
||||
|
||||
type ConnectionOwner struct {
|
||||
ProcessID uint32
|
||||
UserId int32
|
||||
UserName string
|
||||
ProcessPath string
|
||||
AndroidPackageNames []string
|
||||
ProcessID uint32
|
||||
UserId int32
|
||||
UserName string
|
||||
ProcessPath string
|
||||
AndroidPackageName string
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
|
||||
Submodule clients/android updated: 82ffed6cd7...0d31ac467f
Submodule clients/apple updated: bcdd385b79...22dcf646ce
@@ -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.IsDomain() {
|
||||
} else if address.IsFqdn() {
|
||||
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.dialer4.Dialer
|
||||
} else {
|
||||
return d.dialer6.Dialer
|
||||
} else {
|
||||
return d.dialer4.Dialer
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ func (d *resolveDialer) DialContext(ctx context.Context, network string, destina
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !destination.IsDomain() {
|
||||
if !destination.IsFqdn() {
|
||||
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.IsDomain() {
|
||||
if !destination.IsFqdn() {
|
||||
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.IsDomain() {
|
||||
if !destination.IsFqdn() {
|
||||
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.IsDomain() {
|
||||
if !destination.IsFqdn() {
|
||||
return d.dialer.ListenPacket(ctx, destination)
|
||||
}
|
||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func (c *Conn) Read(b []byte) (int, error) {
|
||||
@@ -230,7 +229,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)((*[2]unsafe.Pointer)(unsafe.Pointer(&err))[1])))
|
||||
err = c.rawConn.In.SetErrorLocked(c.sendAlert(uint8(err.(tls.AlertError))))
|
||||
return
|
||||
}
|
||||
return
|
||||
|
||||
@@ -14,7 +14,6 @@ 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")
|
||||
@@ -29,7 +28,7 @@ func FindProcessInfo(searcher Searcher, ctx context.Context, network string, sou
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.UserId != -1 && info.UserName == "" {
|
||||
if info.UserId != -1 {
|
||||
osUser, _ := user.LookupId(F.ToString(info.UserId))
|
||||
if osUser != nil {
|
||||
info.UserName = osUser.Username
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
var _ Searcher = (*androidSearcher)(nil)
|
||||
@@ -19,30 +18,22 @@ 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) {
|
||||
family, protocol, err := socketDiagSettings(network, source)
|
||||
_, uid, err := resolveSocketByNetlink(network, source, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, uid, err := querySocketDiagOnce(family, protocol, 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
|
||||
}
|
||||
appID := uid % 100000
|
||||
var packageNames []string
|
||||
if sharedPackage, loaded := s.packageManager.SharedPackageByID(appID); loaded {
|
||||
packageNames = append(packageNames, sharedPackage)
|
||||
if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded {
|
||||
return &adapter.ConnectionOwner{
|
||||
UserId: int32(uid),
|
||||
AndroidPackageName: packageName,
|
||||
}, 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
|
||||
return &adapter.ConnectionOwner{UserId: int32(uid)}, nil
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
//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)
|
||||
@@ -20,12 +24,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) {
|
||||
return FindDarwinConnectionOwner(network, source, destination)
|
||||
processName, err := findProcessName(network, source.Addr(), int(source.Port()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &adapter.ConnectionOwner{ProcessPath: processName, UserId: -1}, nil
|
||||
}
|
||||
|
||||
var structSize = func() int {
|
||||
@@ -43,3 +47,107 @@ 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]))
|
||||
}
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
//go:build darwin
|
||||
|
||||
package process
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
darwinSnapshotTTL = 200 * time.Millisecond
|
||||
|
||||
darwinXinpgenSize = 24
|
||||
darwinXsocketOffset = 104
|
||||
darwinXinpcbForeignPort = 16
|
||||
darwinXinpcbLocalPort = 18
|
||||
darwinXinpcbVFlag = 44
|
||||
darwinXinpcbForeignAddr = 48
|
||||
darwinXinpcbLocalAddr = 64
|
||||
darwinXinpcbIPv4Addr = 12
|
||||
darwinXsocketUID = 64
|
||||
darwinXsocketLastPID = 68
|
||||
darwinTCPExtraStructSize = 208
|
||||
)
|
||||
|
||||
type darwinConnectionEntry struct {
|
||||
localAddr netip.Addr
|
||||
remoteAddr netip.Addr
|
||||
localPort uint16
|
||||
remotePort uint16
|
||||
pid uint32
|
||||
uid int32
|
||||
}
|
||||
|
||||
type darwinConnectionMatchKind uint8
|
||||
|
||||
const (
|
||||
darwinConnectionMatchExact darwinConnectionMatchKind = iota
|
||||
darwinConnectionMatchLocalFallback
|
||||
darwinConnectionMatchWildcardFallback
|
||||
)
|
||||
|
||||
type darwinSnapshot struct {
|
||||
createdAt time.Time
|
||||
entries []darwinConnectionEntry
|
||||
}
|
||||
|
||||
type darwinConnectionFinder struct {
|
||||
access sync.Mutex
|
||||
ttl time.Duration
|
||||
snapshots map[string]darwinSnapshot
|
||||
builder func(string) (darwinSnapshot, error)
|
||||
}
|
||||
|
||||
var sharedDarwinConnectionFinder = newDarwinConnectionFinder(darwinSnapshotTTL)
|
||||
|
||||
func newDarwinConnectionFinder(ttl time.Duration) *darwinConnectionFinder {
|
||||
return &darwinConnectionFinder{
|
||||
ttl: ttl,
|
||||
snapshots: make(map[string]darwinSnapshot),
|
||||
builder: buildDarwinSnapshot,
|
||||
}
|
||||
}
|
||||
|
||||
func FindDarwinConnectionOwner(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
return sharedDarwinConnectionFinder.find(network, source, destination)
|
||||
}
|
||||
|
||||
func (f *darwinConnectionFinder) find(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
networkName := N.NetworkName(network)
|
||||
source = normalizeDarwinAddrPort(source)
|
||||
destination = normalizeDarwinAddrPort(destination)
|
||||
var lastOwner *adapter.ConnectionOwner
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
snapshot, fromCache, err := f.loadSnapshot(networkName, attempt > 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry, matchKind, err := matchDarwinConnectionEntry(snapshot.entries, networkName, source, destination)
|
||||
if err != nil {
|
||||
if err == ErrNotFound && fromCache {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if fromCache && matchKind != darwinConnectionMatchExact {
|
||||
continue
|
||||
}
|
||||
owner := &adapter.ConnectionOwner{
|
||||
UserId: entry.uid,
|
||||
}
|
||||
lastOwner = owner
|
||||
if entry.pid == 0 {
|
||||
return owner, nil
|
||||
}
|
||||
processPath, err := getExecPathFromPID(entry.pid)
|
||||
if err == nil {
|
||||
owner.ProcessPath = processPath
|
||||
return owner, nil
|
||||
}
|
||||
if fromCache {
|
||||
continue
|
||||
}
|
||||
return owner, nil
|
||||
}
|
||||
if lastOwner != nil {
|
||||
return lastOwner, nil
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
func (f *darwinConnectionFinder) loadSnapshot(network string, forceRefresh bool) (darwinSnapshot, bool, error) {
|
||||
f.access.Lock()
|
||||
defer f.access.Unlock()
|
||||
if !forceRefresh {
|
||||
if snapshot, loaded := f.snapshots[network]; loaded && time.Since(snapshot.createdAt) < f.ttl {
|
||||
return snapshot, true, nil
|
||||
}
|
||||
}
|
||||
snapshot, err := f.builder(network)
|
||||
if err != nil {
|
||||
return darwinSnapshot{}, false, err
|
||||
}
|
||||
f.snapshots[network] = snapshot
|
||||
return snapshot, false, nil
|
||||
}
|
||||
|
||||
func buildDarwinSnapshot(network string) (darwinSnapshot, error) {
|
||||
spath, itemSize, err := darwinSnapshotSettings(network)
|
||||
if err != nil {
|
||||
return darwinSnapshot{}, err
|
||||
}
|
||||
value, err := unix.SysctlRaw(spath)
|
||||
if err != nil {
|
||||
return darwinSnapshot{}, err
|
||||
}
|
||||
return darwinSnapshot{
|
||||
createdAt: time.Now(),
|
||||
entries: parseDarwinSnapshot(value, itemSize),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func darwinSnapshotSettings(network string) (string, int, error) {
|
||||
itemSize := structSize
|
||||
switch network {
|
||||
case N.NetworkTCP:
|
||||
return "net.inet.tcp.pcblist_n", itemSize + darwinTCPExtraStructSize, nil
|
||||
case N.NetworkUDP:
|
||||
return "net.inet.udp.pcblist_n", itemSize, nil
|
||||
default:
|
||||
return "", 0, os.ErrInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func parseDarwinSnapshot(buf []byte, itemSize int) []darwinConnectionEntry {
|
||||
entries := make([]darwinConnectionEntry, 0, (len(buf)-darwinXinpgenSize)/itemSize)
|
||||
for i := darwinXinpgenSize; i+itemSize <= len(buf); i += itemSize {
|
||||
inp := i
|
||||
so := i + darwinXsocketOffset
|
||||
entry, ok := parseDarwinConnectionEntry(buf[inp:so], buf[so:so+structSize-darwinXsocketOffset])
|
||||
if ok {
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func parseDarwinConnectionEntry(inp []byte, so []byte) (darwinConnectionEntry, bool) {
|
||||
if len(inp) < darwinXsocketOffset || len(so) < structSize-darwinXsocketOffset {
|
||||
return darwinConnectionEntry{}, false
|
||||
}
|
||||
entry := darwinConnectionEntry{
|
||||
remotePort: binary.BigEndian.Uint16(inp[darwinXinpcbForeignPort : darwinXinpcbForeignPort+2]),
|
||||
localPort: binary.BigEndian.Uint16(inp[darwinXinpcbLocalPort : darwinXinpcbLocalPort+2]),
|
||||
pid: binary.NativeEndian.Uint32(so[darwinXsocketLastPID : darwinXsocketLastPID+4]),
|
||||
uid: int32(binary.NativeEndian.Uint32(so[darwinXsocketUID : darwinXsocketUID+4])),
|
||||
}
|
||||
flag := inp[darwinXinpcbVFlag]
|
||||
switch {
|
||||
case flag&0x1 != 0:
|
||||
entry.remoteAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr : darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr+4]))
|
||||
entry.localAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr : darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr+4]))
|
||||
return entry, true
|
||||
case flag&0x2 != 0:
|
||||
entry.remoteAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbForeignAddr : darwinXinpcbForeignAddr+16]))
|
||||
entry.localAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbLocalAddr : darwinXinpcbLocalAddr+16]))
|
||||
return entry, true
|
||||
default:
|
||||
return darwinConnectionEntry{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func matchDarwinConnectionEntry(entries []darwinConnectionEntry, network string, source netip.AddrPort, destination netip.AddrPort) (darwinConnectionEntry, darwinConnectionMatchKind, error) {
|
||||
sourceAddr := source.Addr()
|
||||
if !sourceAddr.IsValid() {
|
||||
return darwinConnectionEntry{}, darwinConnectionMatchExact, os.ErrInvalid
|
||||
}
|
||||
var localFallback darwinConnectionEntry
|
||||
var hasLocalFallback bool
|
||||
var wildcardFallback darwinConnectionEntry
|
||||
var hasWildcardFallback bool
|
||||
for _, entry := range entries {
|
||||
if entry.localPort != source.Port() || sourceAddr.BitLen() != entry.localAddr.BitLen() {
|
||||
continue
|
||||
}
|
||||
if entry.localAddr == sourceAddr && destination.IsValid() && entry.remotePort == destination.Port() && entry.remoteAddr == destination.Addr() {
|
||||
return entry, darwinConnectionMatchExact, nil
|
||||
}
|
||||
if !destination.IsValid() && entry.localAddr == sourceAddr {
|
||||
return entry, darwinConnectionMatchExact, nil
|
||||
}
|
||||
if network != N.NetworkUDP {
|
||||
continue
|
||||
}
|
||||
if !hasLocalFallback && entry.localAddr == sourceAddr {
|
||||
hasLocalFallback = true
|
||||
localFallback = entry
|
||||
}
|
||||
if !hasWildcardFallback && entry.localAddr.IsUnspecified() {
|
||||
hasWildcardFallback = true
|
||||
wildcardFallback = entry
|
||||
}
|
||||
}
|
||||
if hasLocalFallback {
|
||||
return localFallback, darwinConnectionMatchLocalFallback, nil
|
||||
}
|
||||
if hasWildcardFallback {
|
||||
return wildcardFallback, darwinConnectionMatchWildcardFallback, nil
|
||||
}
|
||||
return darwinConnectionEntry{}, darwinConnectionMatchExact, ErrNotFound
|
||||
}
|
||||
|
||||
func normalizeDarwinAddrPort(addrPort netip.AddrPort) netip.AddrPort {
|
||||
if !addrPort.IsValid() {
|
||||
return addrPort
|
||||
}
|
||||
return netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port())
|
||||
}
|
||||
|
||||
func getExecPathFromPID(pid uint32) (string, error) {
|
||||
const (
|
||||
procpidpathinfo = 0xb
|
||||
procpidpathinfosize = 1024
|
||||
proccallnumpidinfo = 0x2
|
||||
)
|
||||
buf := make([]byte, procpidpathinfosize)
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PROC_INFO,
|
||||
proccallnumpidinfo,
|
||||
uintptr(pid),
|
||||
procpidpathinfo,
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&buf[0])),
|
||||
procpidpathinfosize)
|
||||
if errno != 0 {
|
||||
return "", errno
|
||||
}
|
||||
return unix.ByteSliceToString(buf), nil
|
||||
}
|
||||
@@ -4,82 +4,33 @@ 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
|
||||
diagConns [4]*socketDiagConn
|
||||
processPathCache *uidProcessPathCache
|
||||
logger log.ContextLogger
|
||||
}
|
||||
|
||||
func NewSearcher(config Config) (Searcher, error) {
|
||||
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...)
|
||||
return &linuxSearcher{config.Logger}, nil
|
||||
}
|
||||
|
||||
func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
inode, uid, err := s.resolveSocketByNetlink(network, source, destination)
|
||||
inode, uid, err := resolveSocketByNetlink(network, source, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
processInfo := &adapter.ConnectionOwner{
|
||||
UserId: int32(uid),
|
||||
}
|
||||
processPath, err := s.processPathCache.findProcessPath(inode, uid)
|
||||
processPath, err := resolveProcessNameByProcSearch(inode, uid)
|
||||
if err != nil {
|
||||
s.logger.DebugContext(ctx, "find process path: ", err)
|
||||
} else {
|
||||
processInfo.ProcessPath = processPath
|
||||
}
|
||||
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)
|
||||
return &adapter.ConnectionOwner{
|
||||
UserId: int32(uid),
|
||||
ProcessPath: processPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -3,67 +3,43 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unicode"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
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 (
|
||||
sizeOfSocketDiagRequestData = 56
|
||||
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + sizeOfSocketDiagRequestData
|
||||
socketDiagResponseMinSize = 72
|
||||
socketDiagByFamily = 20
|
||||
pathProc = "/proc"
|
||||
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48
|
||||
socketDiagByFamily = 20
|
||||
pathProc = "/proc"
|
||||
)
|
||||
|
||||
type socketDiagConn struct {
|
||||
access sync.Mutex
|
||||
family uint8
|
||||
protocol uint8
|
||||
fd int
|
||||
}
|
||||
func resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
||||
var family uint8
|
||||
var protocol uint8
|
||||
|
||||
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
|
||||
@@ -72,308 +48,151 @@ func socketDiagSettings(network string, source netip.AddrPort) (family, protocol
|
||||
default:
|
||||
return 0, 0, os.ErrInvalid
|
||||
}
|
||||
switch {
|
||||
case source.Addr().Is4():
|
||||
|
||||
if source.Addr().Is4() {
|
||||
family = syscall.AF_INET
|
||||
case source.Addr().Is6():
|
||||
} else {
|
||||
family = syscall.AF_INET6
|
||||
default:
|
||||
return 0, 0, os.ErrInvalid
|
||||
}
|
||||
return family, protocol, nil
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
req := packSocketDiagRequest(family, protocol, source)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
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()
|
||||
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(fd)
|
||||
return querySocketDiag(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true))
|
||||
}
|
||||
defer syscall.Close(socket)
|
||||
|
||||
func (c *socketDiagConn) ensureOpenLocked() error {
|
||||
if c.fd != -1 {
|
||||
return nil
|
||||
}
|
||||
fd, err := openSocketDiag()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.fd = fd
|
||||
return nil
|
||||
}
|
||||
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})
|
||||
|
||||
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{
|
||||
err = syscall.Connect(socket, &syscall.SockaddrNetlink{
|
||||
Family: syscall.AF_NETLINK,
|
||||
Pad: 0,
|
||||
Pid: 0,
|
||||
Groups: 0,
|
||||
}); err != nil {
|
||||
syscall.Close(fd)
|
||||
return -1, err
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
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)
|
||||
_, err = syscall.Write(socket, req)
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "write netlink request")
|
||||
}
|
||||
buffer := make([]byte, 64<<10)
|
||||
n, err := syscall.Read(fd, buffer)
|
||||
|
||||
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")
|
||||
}
|
||||
messages, err := syscall.ParseNetlinkMessage(buffer[:n])
|
||||
|
||||
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")
|
||||
}
|
||||
return unpackSocketDiagMessages(messages)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 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 unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
|
||||
if len(msg.Data) < socketDiagResponseMinSize {
|
||||
if len(msg.Data) < 72 {
|
||||
return 0, 0
|
||||
}
|
||||
uid = binary.NativeEndian.Uint32(msg.Data[64:68])
|
||||
inode = binary.NativeEndian.Uint32(msg.Data[68:72])
|
||||
return inode, uid
|
||||
|
||||
data := msg.Data
|
||||
|
||||
uid = nativeEndian.Uint32(data[64:68])
|
||||
inode = nativeEndian.Uint32(data[68:72])
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) {
|
||||
files, err := os.ReadDir(pathProc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
buffer := make([]byte, syscall.PathMax)
|
||||
processPaths := make(map[uint32]string)
|
||||
for _, file := range files {
|
||||
if !file.IsDir() || !isPid(file.Name()) {
|
||||
socket := []byte(fmt.Sprintf("socket:[%d]", inode))
|
||||
|
||||
for _, f := range files {
|
||||
if !f.IsDir() || !isPid(f.Name()) {
|
||||
continue
|
||||
}
|
||||
info, err := file.Info()
|
||||
|
||||
info, err := f.Info()
|
||||
if err != nil {
|
||||
if isIgnorableProcError(err) {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
if info.Sys().(*syscall.Stat_t).Uid != uid {
|
||||
continue
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
processPath := path.Join(pathProc, f.Name())
|
||||
fdPath := path.Join(processPath, "fd")
|
||||
|
||||
fds, err := os.ReadDir(fdPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, fd := range fds {
|
||||
n, err := syscall.Readlink(filepath.Join(fdPath, fd.Name()), buffer)
|
||||
n, err := syscall.Readlink(path.Join(fdPath, fd.Name()), buffer)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
inode, ok := parseSocketInode(buffer[:n])
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if _, loaded := processPaths[inode]; !loaded {
|
||||
processPaths[inode] = exePath
|
||||
|
||||
if bytes.Equal(buffer[:n], socket) {
|
||||
return os.Readlink(path.Join(processPath, "exe"))
|
||||
}
|
||||
}
|
||||
}
|
||||
return processPaths, nil
|
||||
}
|
||||
|
||||
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
|
||||
return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode)
|
||||
}
|
||||
|
||||
func isPid(s string) bool {
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
//go:build linux
|
||||
|
||||
package process
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestQuerySocketDiagUDPExact(t *testing.T) {
|
||||
t.Parallel()
|
||||
server, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||
require.NoError(t, err)
|
||||
defer server.Close()
|
||||
|
||||
client, err := net.DialUDP("udp4", nil, server.LocalAddr().(*net.UDPAddr))
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
err = client.SetDeadline(time.Now().Add(time.Second))
|
||||
require.NoError(t, err)
|
||||
_, err = client.Write([]byte{0})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = server.SetReadDeadline(time.Now().Add(time.Second))
|
||||
require.NoError(t, err)
|
||||
buffer := make([]byte, 1)
|
||||
_, _, err = server.ReadFromUDP(buffer)
|
||||
require.NoError(t, err)
|
||||
|
||||
source := addrPortFromUDPAddr(t, client.LocalAddr())
|
||||
destination := addrPortFromUDPAddr(t, client.RemoteAddr())
|
||||
|
||||
fd, err := openSocketDiag()
|
||||
require.NoError(t, err)
|
||||
defer syscall.Close(fd)
|
||||
|
||||
inode, uid, err := querySocketDiag(fd, packSocketDiagRequest(syscall.AF_INET, syscall.IPPROTO_UDP, source, destination, false))
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, inode)
|
||||
require.EqualValues(t, os.Getuid(), uid)
|
||||
}
|
||||
|
||||
func addrPortFromUDPAddr(t *testing.T, addr net.Addr) netip.AddrPort {
|
||||
t.Helper()
|
||||
|
||||
udpAddr, ok := addr.(*net.UDPAddr)
|
||||
require.True(t, ok)
|
||||
|
||||
ip, ok := netip.AddrFromSlice(udpAddr.IP)
|
||||
require.True(t, ok)
|
||||
|
||||
return netip.AddrPortFrom(ip.Unmap(), uint16(udpAddr.Port))
|
||||
}
|
||||
@@ -28,10 +28,6 @@ func initWin32API() error {
|
||||
return winiphlpapi.LoadExtendedTable()
|
||||
}
|
||||
|
||||
func (s *windowsSearcher) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
pid, err := winiphlpapi.FindPid(network, source)
|
||||
if err != nil {
|
||||
|
||||
@@ -168,7 +168,7 @@ func (s *StartedService) waitForStarted(ctx context.Context) error {
|
||||
func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error {
|
||||
s.serviceAccess.Lock()
|
||||
switch s.serviceStatus.Status {
|
||||
case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING, ServiceStatus_FATAL:
|
||||
case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING:
|
||||
default:
|
||||
s.serviceAccess.Unlock()
|
||||
return os.ErrInvalid
|
||||
@@ -226,14 +226,13 @@ func (s *StartedService) CloseService() error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
s.updateStatus(ServiceStatus_STOPPING)
|
||||
instance := s.instance
|
||||
s.instance = nil
|
||||
if instance != nil {
|
||||
err := instance.Close()
|
||||
if s.instance != nil {
|
||||
err := s.instance.Close()
|
||||
if err != nil {
|
||||
return s.updateStatusError(err)
|
||||
}
|
||||
}
|
||||
s.instance = nil
|
||||
s.startedAt = time.Time{}
|
||||
s.updateStatus(ServiceStatus_IDLE)
|
||||
s.serviceAccess.Unlock()
|
||||
@@ -950,11 +949,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,
|
||||
PackageNames: metadata.Metadata.ProcessInfo.AndroidPackageNames,
|
||||
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,
|
||||
}
|
||||
}
|
||||
return &Connection{
|
||||
|
||||
@@ -1460,7 +1460,7 @@ type ProcessInfo struct {
|
||||
UserId int32 `protobuf:"varint,2,opt,name=userId,proto3" json:"userId,omitempty"`
|
||||
UserName string `protobuf:"bytes,3,opt,name=userName,proto3" json:"userName,omitempty"`
|
||||
ProcessPath string `protobuf:"bytes,4,opt,name=processPath,proto3" json:"processPath,omitempty"`
|
||||
PackageNames []string `protobuf:"bytes,5,rep,name=packageNames,proto3" json:"packageNames,omitempty"`
|
||||
PackageName string `protobuf:"bytes,5,opt,name=packageName,proto3" json:"packageName,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -1523,11 +1523,11 @@ func (x *ProcessInfo) GetProcessPath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ProcessInfo) GetPackageNames() []string {
|
||||
func (x *ProcessInfo) GetPackageName() string {
|
||||
if x != nil {
|
||||
return x.PackageNames
|
||||
return x.PackageName
|
||||
}
|
||||
return nil
|
||||
return ""
|
||||
}
|
||||
|
||||
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\"\xa5\x01\n" +
|
||||
"\vprocessInfo\x18\x16 \x01(\v2\x13.daemon.ProcessInfoR\vprocessInfo\"\xa3\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" +
|
||||
"\fpackageNames\x18\x05 \x03(\tR\fpackageNames\"(\n" +
|
||||
"\vprocessPath\x18\x04 \x01(\tR\vprocessPath\x12 \n" +
|
||||
"\vpackageName\x18\x05 \x01(\tR\vpackageName\"(\n" +
|
||||
"\x16CloseConnectionRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\"K\n" +
|
||||
"\x12DeprecatedWarnings\x125\n" +
|
||||
|
||||
@@ -195,7 +195,7 @@ message ProcessInfo {
|
||||
int32 userId = 2;
|
||||
string userName = 3;
|
||||
string processPath = 4;
|
||||
repeated string packageNames = 5;
|
||||
string packageName = 5;
|
||||
}
|
||||
|
||||
message CloseConnectionRequest {
|
||||
|
||||
@@ -55,12 +55,6 @@ 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 {
|
||||
@@ -106,37 +100,41 @@ func (c *Connector[T]) Get(ctx context.Context) (T, error) {
|
||||
return zero, err
|
||||
}
|
||||
|
||||
connecting := make(chan struct{})
|
||||
c.connecting = connecting
|
||||
dialContext := context.WithValue(ctx, contextKeyConnecting{}, c)
|
||||
dialResult := make(chan connectorDialResult[T], 1)
|
||||
c.connecting = make(chan struct{})
|
||||
c.access.Unlock()
|
||||
|
||||
go func() {
|
||||
connection, cancel, err := c.dialWithCancellation(dialContext)
|
||||
dialResult <- connectorDialResult[T]{
|
||||
connection: connection,
|
||||
cancel: cancel,
|
||||
err: err,
|
||||
}
|
||||
}()
|
||||
dialContext := context.WithValue(ctx, contextKeyConnecting{}, c)
|
||||
connection, cancel, err := c.dialWithCancellation(dialContext)
|
||||
|
||||
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)
|
||||
}()
|
||||
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()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,38 +143,6 @@ 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 {
|
||||
|
||||
@@ -188,157 +188,13 @@ func TestConnectorCanceledRequestDoesNotCacheConnection(t *testing.T) {
|
||||
err := <-result
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
require.EqualValues(t, 1, dialCount.Load())
|
||||
require.Eventually(t, func() bool {
|
||||
return closeCount.Load() == 1
|
||||
}, time.Second, 10*time.Millisecond)
|
||||
require.EqualValues(t, 1, closeCount.Load())
|
||||
|
||||
_, 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()
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ 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"
|
||||
@@ -39,6 +40,13 @@ func (t *Transport) exchangeParallel(ctx context.Context, servers []M.Socksaddr,
|
||||
results := make(chan queryResult)
|
||||
startRacer := func(ctx context.Context, fqdn string) {
|
||||
response, err := t.tryOneName(ctx, servers, fqdn, message)
|
||||
if err == nil {
|
||||
if response.Rcode != mDNS.RcodeSuccess {
|
||||
err = dns.RcodeError(response.Rcode)
|
||||
} else if len(dns.MessageToAddresses(response)) == 0 {
|
||||
err = dns.RcodeSuccess
|
||||
}
|
||||
}
|
||||
select {
|
||||
case results <- queryResult{response, err}:
|
||||
case <-returned:
|
||||
|
||||
@@ -7,6 +7,7 @@ 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"
|
||||
@@ -48,6 +49,13 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
|
||||
results := make(chan queryResult)
|
||||
startRacer := func(ctx context.Context, fqdn string) {
|
||||
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
|
||||
if err == nil {
|
||||
if response.Rcode != mDNS.RcodeSuccess {
|
||||
err = dns.RcodeError(response.Rcode)
|
||||
} else if len(dns.MessageToAddresses(response)) == 0 {
|
||||
err = E.New(fqdn, ": empty result")
|
||||
}
|
||||
}
|
||||
select {
|
||||
case results <- queryResult{response, err}:
|
||||
case <-returned:
|
||||
|
||||
@@ -2,15 +2,7 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.14.0-alpha.4
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.4-beta.1
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.3
|
||||
#### 1.14.0-alpha.2
|
||||
|
||||
* Add OpenWrt and Alpine APK packages to release **1**
|
||||
* Backport to macOS 10.13 High Sierra **2**
|
||||
@@ -34,11 +26,7 @@ from [SagerNet/go](https://github.com/SagerNet/go).
|
||||
|
||||
See [OCM](/configuration/service/ocm).
|
||||
|
||||
#### 1.12.24
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.14.0-alpha.2
|
||||
#### 1.13.3-beta.1
|
||||
|
||||
* Add OpenWrt and Alpine APK packages to release **1**
|
||||
* Backport to macOS 10.13 High Sierra **2**
|
||||
|
||||
@@ -4,7 +4,7 @@ icon: material/delete-clock
|
||||
|
||||
!!! failure "已在 sing-box 1.12.0 废弃"
|
||||
|
||||
旧的 fake-ip 配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
|
||||
旧的 fake-ip 配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-to-new-dns-servers)。
|
||||
|
||||
### 结构
|
||||
|
||||
|
||||
@@ -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 中被移除,参阅 [迁移指南](/zh/migration/#迁移-outbound-dns-规则项到域解析选项)。
|
||||
`outbound` 规则项已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver)。
|
||||
|
||||
匹配出站。
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -64,7 +64,7 @@ DNS 服务器的路径。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ DNS 服务器的路径。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ icon: material/delete-clock
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.12.0"
|
||||
|
||||
旧的 DNS 服务器配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
|
||||
旧的 DNS 服务器配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-to-new-dns-servers)。
|
||||
|
||||
!!! quote "sing-box 1.9.0 中的更改"
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ DNS 服务器的端口。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ DNS 服务器的端口。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
将拒绝的 DNS 响应缓存存储在缓存文件中。
|
||||
|
||||
[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#地址筛选字段) 的检查结果将被缓存至过期。
|
||||
[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#_3) 的检查结果将被缓存至过期。
|
||||
|
||||
#### rdrc_timeout
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
!!! quote ""
|
||||
|
||||
默认安装不包含 V2Ray API,参阅 [安装](/zh/installation/build-from-source/#构建标记)。
|
||||
默认安装不包含 V2Ray API,参阅 [安装](/zh/installation/build-from-source/#_5)。
|
||||
|
||||
### 结构
|
||||
|
||||
|
||||
@@ -58,4 +58,4 @@ AnyTLS 填充方案行数组。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
|
||||
#### users
|
||||
|
||||
|
||||
@@ -104,4 +104,4 @@ base64 编码的认证密码。
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
@@ -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/#入站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
|
||||
#### masquerade
|
||||
|
||||
|
||||
@@ -60,4 +60,4 @@ QUIC 拥塞控制算法。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
@@ -93,4 +93,4 @@
|
||||
|
||||
#### multiplex
|
||||
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
|
||||
|
||||
@@ -43,7 +43,7 @@ Trojan 用户。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
|
||||
#### fallback
|
||||
|
||||
@@ -61,7 +61,7 @@ TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
#### multiplex
|
||||
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
|
||||
|
||||
#### transport
|
||||
|
||||
|
||||
@@ -75,4 +75,4 @@ QUIC 拥塞控制算法
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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 中的更改"
|
||||
|
||||
@@ -48,11 +48,11 @@ VLESS 子协议。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
|
||||
#### multiplex
|
||||
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
|
||||
|
||||
#### transport
|
||||
|
||||
|
||||
@@ -43,11 +43,11 @@ VMess 用户。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
|
||||
#### multiplex
|
||||
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
|
||||
|
||||
#### transport
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ AnyTLS 密码。
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ icon: material/alert-decagram
|
||||
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
:material-delete-clock: [override_address](#override_address)
|
||||
:material-delete-clock: [override_port](#override_port)
|
||||
:material-alert-decagram: [override_address](#override_address)
|
||||
:material-alert-decagram: [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 中被移除,参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。
|
||||
目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。
|
||||
|
||||
覆盖连接目标地址。
|
||||
|
||||
@@ -37,7 +37,7 @@ icon: material/alert-decagram
|
||||
|
||||
!!! failure "已在 sing-box 1.11.0 废弃"
|
||||
|
||||
目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。
|
||||
目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。
|
||||
|
||||
覆盖连接目标端口。
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ icon: material/delete-clock
|
||||
|
||||
!!! failure "已在 sing-box 1.11.0 废弃"
|
||||
|
||||
旧的特殊出站已被弃用,且将在 sing-box 1.13.0 中被移除, 参阅 [迁移指南](/zh/migration/#迁移旧的特殊出站到规则动作).
|
||||
旧的特殊出站已被弃用,且将在 sing-box 1.13.0 中被移除, 参阅 [迁移指南](/migration/#migrate-legacy-special-outbounds-to-rule-actions).
|
||||
|
||||
`dns` 出站是一个内部 DNS 服务器。
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ HTTP 请求的额外标头。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ base64 编码的认证密码。
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
|
||||
|
||||
### 拨号字段
|
||||
|
||||
@@ -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/#出站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
|
||||
#### brutal_debug
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ QUIC 拥塞控制算法。
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
|
||||
只有 `server_name`、`certificate`、`certificate_path` 和 `ech` 是被支持的。
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
!!! quote ""
|
||||
|
||||
选择器目前只能通过 [Clash API](/zh/configuration/experimental/clash-api/) 来控制。
|
||||
选择器目前只能通过 [Clash API](/zh/configuration/experimental#clash-api) 来控制。
|
||||
|
||||
### 字段
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ UDP over TCP 配置。
|
||||
|
||||
#### multiplex
|
||||
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ ShadowTLS 协议版本。
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
!!! info ""
|
||||
|
||||
默认安装不包含嵌入式 Tor, 参阅 [安装](/zh/installation/build-from-source/#构建标记)。
|
||||
默认安装不包含嵌入式 Tor, 参阅 [安装](/zh/installation/build-from-source/#_5)。
|
||||
|
||||
### 字段
|
||||
|
||||
|
||||
@@ -47,11 +47,11 @@ Trojan 密码。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
|
||||
#### multiplex
|
||||
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
|
||||
|
||||
#### transport
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ UDP 包中继模式
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ VLESS 子协议。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
|
||||
#### packet_encoding
|
||||
|
||||
@@ -71,7 +71,7 @@ UDP 包编码,默认使用 xudp。
|
||||
|
||||
#### multiplex
|
||||
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
|
||||
|
||||
#### transport
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ VMess 用户 ID。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
|
||||
#### packet_encoding
|
||||
|
||||
@@ -96,7 +96,7 @@ UDP 包编码。
|
||||
|
||||
#### multiplex
|
||||
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
|
||||
|
||||
#### transport
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ icon: material/delete-clock
|
||||
|
||||
!!! failure "已在 sing-box 1.11.0 废弃"
|
||||
|
||||
WireGuard 出站已被弃用,且将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-wireguard-出站到端点)。
|
||||
WireGuard 出站已被弃用,且将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-wireguard-outbound-to-endpoint)。
|
||||
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
|
||||
@@ -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)。
|
||||
|
||||
### 结构
|
||||
|
||||
|
||||
@@ -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)。
|
||||
|
||||
### 结构
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ icon: material/alert-decagram
|
||||
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
:material-plus: [default_network_strategy](#default_network_strategy)
|
||||
:material-plus: [network_strategy](#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 起"
|
||||
|
||||
详情参阅 [拨号字段](/zh/configuration/shared/dial/#domain_resolver)。
|
||||
详情参阅 [拨号字段](/configuration/shared/dial/#domain_resolver)。
|
||||
|
||||
可以被 `outbound.domain_resolver` 覆盖。
|
||||
|
||||
@@ -158,7 +158,7 @@ icon: material/alert-decagram
|
||||
|
||||
!!! question "自 sing-box 1.11.0 起"
|
||||
|
||||
详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_strategy)。
|
||||
详情参阅 [拨号字段](/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 起"
|
||||
|
||||
详情参阅 [拨号字段](/zh/configuration/shared/dial/#default_network_type)。
|
||||
详情参阅 [拨号字段](/configuration/shared/dial/#default_network_type)。
|
||||
|
||||
#### default_fallback_network_type
|
||||
|
||||
!!! question "自 sing-box 1.11.0 起"
|
||||
|
||||
详情参阅 [拨号字段](/zh/configuration/shared/dial/#default_fallback_network_type)。
|
||||
详情参阅 [拨号字段](/configuration/shared/dial/#default_fallback_network_type)。
|
||||
|
||||
#### default_fallback_delay
|
||||
|
||||
!!! question "自 sing-box 1.11.0 起"
|
||||
|
||||
详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_delay)。
|
||||
详情参阅 [拨号字段](/configuration/shared/dial/#fallback_delay)。
|
||||
|
||||
@@ -27,7 +27,6 @@ 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 中的更改"
|
||||
@@ -266,7 +265,7 @@ icon: material/new-box
|
||||
|
||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||
|
||||
Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。
|
||||
Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geosite)。
|
||||
|
||||
匹配 Geosite。
|
||||
|
||||
@@ -274,7 +273,7 @@ icon: material/new-box
|
||||
|
||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||
|
||||
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
|
||||
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。
|
||||
|
||||
匹配源 GeoIP。
|
||||
|
||||
@@ -282,7 +281,7 @@ icon: material/new-box
|
||||
|
||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||
|
||||
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
|
||||
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。
|
||||
|
||||
匹配 GeoIP。
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ icon: material/new-box
|
||||
|
||||
目标出站的标签。
|
||||
|
||||
如果未指定,规则仅在来自 auto redirect 的[预匹配](/zh/configuration/shared/pre-match/)中匹配,在其他场景中将被跳过。
|
||||
如果未指定,规则仅在来自 auto redirect 的[预匹配](/configuration/shared/pre-match/)中匹配,在其他场景中将被跳过。
|
||||
|
||||
#### route-options 字段
|
||||
|
||||
@@ -154,22 +154,22 @@ icon: material/new-box
|
||||
|
||||
#### network_strategy
|
||||
|
||||
详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_strategy)。
|
||||
详情参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。
|
||||
|
||||
仅当出站为 `direct` 且 `outbound.bind_interface`, `outbound.inet4_bind_address`
|
||||
且 `outbound.inet6_bind_address` 未设置时生效。
|
||||
|
||||
#### network_type
|
||||
|
||||
详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_type)。
|
||||
详情参阅 [拨号字段](/configuration/shared/dial/#network_type)。
|
||||
|
||||
#### fallback_network_type
|
||||
|
||||
详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_network_type)。
|
||||
详情参阅 [拨号字段](/configuration/shared/dial/#fallback_network_type)。
|
||||
|
||||
#### fallback_delay
|
||||
|
||||
详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_delay)。
|
||||
详情参阅 [拨号字段](/configuration/shared/dial/#fallback_delay)。
|
||||
|
||||
#### udp_disable_domain_unmapping
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ icon: material/new-box
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
:material-plus: [network_type](#network_type)
|
||||
:material-plus: [network_is_expensive](#network_is_expensive)
|
||||
:material-plus: [network_is_constrained](#network_is_constrained)
|
||||
:material-alert: [network_is_expensive](#network_is_expensive)
|
||||
:material-alert: [network_is_constrained](#network_is_constrained)
|
||||
|
||||
### 结构
|
||||
|
||||
|
||||
@@ -10,6 +10,11 @@ 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
|
||||
@@ -19,6 +24,7 @@ It handles OAuth authentication with Claude's API on your local machine while al
|
||||
... // Listen Fields
|
||||
|
||||
"credential_path": "",
|
||||
"credentials": [],
|
||||
"usages_path": "",
|
||||
"users": [],
|
||||
"headers": {},
|
||||
@@ -45,6 +51,77 @@ 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.
|
||||
@@ -60,6 +137,8 @@ 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.
|
||||
@@ -71,7 +150,8 @@ Object format:
|
||||
```json
|
||||
{
|
||||
"name": "",
|
||||
"token": ""
|
||||
"token": "",
|
||||
"credential": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -79,6 +159,7 @@ 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
|
||||
|
||||
@@ -90,6 +171,8 @@ 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).
|
||||
@@ -129,3 +212,52 @@ 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -10,6 +10,11 @@ CCM(Claude Code 多路复用器)服务是一个多路复用服务,允许
|
||||
|
||||
它在本地机器上处理与 Claude API 的 OAuth 身份验证,同时允许远程 Claude Code 通过 `ANTHROPIC_AUTH_TOKEN` 环境变量使用认证令牌进行身份验证。
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [credentials](#credentials)
|
||||
:material-alert: [users](#users)
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
@@ -19,6 +24,7 @@ CCM(Claude Code 多路复用器)服务是一个多路复用服务,允许
|
||||
... // 监听字段
|
||||
|
||||
"credential_path": "",
|
||||
"credentials": [],
|
||||
"usages_path": "",
|
||||
"users": [],
|
||||
"headers": {},
|
||||
@@ -45,6 +51,77 @@ 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 使用统计信息的文件路径。
|
||||
@@ -60,6 +137,8 @@ Claude Code OAuth 凭据文件的路径。
|
||||
|
||||
统计文件每分钟自动保存一次,并在服务关闭时保存。
|
||||
|
||||
与 `credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `usages_path`。
|
||||
|
||||
#### users
|
||||
|
||||
用于令牌身份验证的授权用户列表。
|
||||
@@ -71,7 +150,8 @@ Claude Code OAuth 凭据文件的路径。
|
||||
```json
|
||||
{
|
||||
"name": "",
|
||||
"token": ""
|
||||
"token": "",
|
||||
"credential": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -79,6 +159,7 @@ Claude Code OAuth 凭据文件的路径。
|
||||
|
||||
- `name`:用于跟踪的用户名标识符。
|
||||
- `token`:用于身份验证的 Bearer 令牌。Claude Code 通过设置 `ANTHROPIC_AUTH_TOKEN` 环境变量为其令牌值进行身份验证。
|
||||
- `credential`:此用户使用的凭据标签。设置 `credentials` 时==必填==。
|
||||
|
||||
#### headers
|
||||
|
||||
@@ -90,9 +171,11 @@ Claude Code OAuth 凭据文件的路径。
|
||||
|
||||
用于连接 Claude API 的出站标签。
|
||||
|
||||
与 `credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `detour`。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
|
||||
### 示例
|
||||
|
||||
@@ -129,3 +212,52 @@ 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -36,7 +36,7 @@ DERP 服务是一个 Tailscale DERP 服务器,类似于 [derper](https://pkg.g
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
|
||||
#### config_path
|
||||
|
||||
@@ -96,7 +96,7 @@ Derper 配置文件路径。
|
||||
- `server`:**必填** DERP 服务器地址。
|
||||
- `server_port`:**必填** DERP 服务器端口。
|
||||
- `host`:自定义 DERP 主机名。
|
||||
- `tls`:[TLS](/zh/configuration/shared/tls/#出站)
|
||||
- `tls`:[TLS](/zh/configuration/shared/tls/#outbound)
|
||||
- `拨号字段`:[拨号字段](/zh/configuration/shared/dial/)
|
||||
|
||||
#### mesh_psk
|
||||
|
||||
@@ -10,6 +10,11 @@ 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
|
||||
@@ -19,6 +24,7 @@ It handles OAuth authentication with OpenAI's API on your local machine while al
|
||||
... // Listen Fields
|
||||
|
||||
"credential_path": "",
|
||||
"credentials": [],
|
||||
"usages_path": "",
|
||||
"users": [],
|
||||
"headers": {},
|
||||
@@ -43,6 +49,75 @@ 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.
|
||||
@@ -58,6 +133,8 @@ 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.
|
||||
@@ -69,7 +146,8 @@ Object format:
|
||||
```json
|
||||
{
|
||||
"name": "",
|
||||
"token": ""
|
||||
"token": "",
|
||||
"credential": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -77,6 +155,7 @@ 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
|
||||
|
||||
@@ -88,6 +167,8 @@ 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).
|
||||
@@ -183,3 +264,52 @@ 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -10,6 +10,11 @@ OCM(OpenAI Codex 多路复用器)服务是一个多路复用服务,允许
|
||||
|
||||
它在本地机器上处理与 OpenAI API 的 OAuth 身份验证,同时允许远程客户端使用自定义令牌进行身份验证。
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [credentials](#credentials)
|
||||
:material-alert: [users](#users)
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
@@ -19,6 +24,7 @@ OCM(OpenAI Codex 多路复用器)服务是一个多路复用服务,允许
|
||||
... // 监听字段
|
||||
|
||||
"credential_path": "",
|
||||
"credentials": [],
|
||||
"usages_path": "",
|
||||
"users": [],
|
||||
"headers": {},
|
||||
@@ -43,6 +49,75 @@ 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 使用统计信息的文件路径。
|
||||
@@ -58,6 +133,8 @@ OpenAI OAuth 凭据文件的路径。
|
||||
|
||||
统计文件每分钟自动保存一次,并在服务关闭时保存。
|
||||
|
||||
与 `credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `usages_path`。
|
||||
|
||||
#### users
|
||||
|
||||
用于令牌身份验证的授权用户列表。
|
||||
@@ -69,7 +146,8 @@ OpenAI OAuth 凭据文件的路径。
|
||||
```json
|
||||
{
|
||||
"name": "",
|
||||
"token": ""
|
||||
"token": "",
|
||||
"credential": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -77,6 +155,7 @@ OpenAI OAuth 凭据文件的路径。
|
||||
|
||||
- `name`:用于跟踪的用户名标识符。
|
||||
- `token`:用于身份验证的 Bearer 令牌。客户端通过设置 `Authorization: Bearer <token>` 头进行身份验证。
|
||||
- `credential`:此用户使用的凭据标签。设置 `credentials` 时==必填==。
|
||||
|
||||
#### headers
|
||||
|
||||
@@ -88,9 +167,11 @@ OpenAI OAuth 凭据文件的路径。
|
||||
|
||||
用于连接 OpenAI API 的出站标签。
|
||||
|
||||
与 `credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `detour`。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
|
||||
### 示例
|
||||
|
||||
@@ -184,3 +265,52 @@ 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -55,4 +55,4 @@ SSM API 服务是一个用于管理 Shadowsocks 服务器的 RESTful API 服务
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
@@ -173,7 +173,7 @@ TCP keep alive 间隔。
|
||||
|
||||
用于设置解析域名的域名解析器。
|
||||
|
||||
此选项的格式与 [路由 DNS 规则动作](/zh/configuration/dns/rule_action/#route) 相同,但不包含 `action` 字段。
|
||||
此选项的格式与 [路由 DNS 规则动作](/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 中被移除,参阅 [迁移指南](/zh/migration/#迁移出站域名策略选项到域名解析器)。
|
||||
`domain_strategy` 已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-outbound-domain-strategy-option-to-domain-resolver)。
|
||||
|
||||
可选值:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。
|
||||
|
||||
|
||||
@@ -145,13 +145,13 @@ UDP NAT 过期时间。
|
||||
|
||||
如果设置,连接将被转发到指定的入站。
|
||||
|
||||
需要目标入站支持,参阅 [注入支持](/zh/configuration/inbound/#字段)。
|
||||
需要目标入站支持,参阅 [注入支持](/zh/configuration/inbound/#_3)。
|
||||
|
||||
#### sniff
|
||||
|
||||
!!! failure "已在 sing-box 1.11.0 废弃"
|
||||
|
||||
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作).
|
||||
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
|
||||
|
||||
启用协议探测。
|
||||
|
||||
@@ -171,7 +171,7 @@ UDP NAT 过期时间。
|
||||
|
||||
!!! failure "已在 sing-box 1.11.0 废弃"
|
||||
|
||||
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作).
|
||||
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
|
||||
|
||||
探测超时时间。
|
||||
|
||||
@@ -181,7 +181,7 @@ UDP NAT 过期时间。
|
||||
|
||||
!!! failure "已在 sing-box 1.11.0 废弃"
|
||||
|
||||
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作).
|
||||
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
|
||||
|
||||
可选值: `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 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作).
|
||||
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
|
||||
|
||||
如果启用,对于地址为域的 UDP 代理请求,将在响应中发送原始包地址而不是映射的域。
|
||||
|
||||
|
||||
@@ -22,13 +22,13 @@ icon: material/new-box
|
||||
|
||||
以 TCP RST / ICMP 不可达拒绝。
|
||||
|
||||
详情参阅 [reject](/zh/configuration/route/rule_action/#reject)。
|
||||
详情参阅 [reject](/configuration/route/rule_action/#reject)。
|
||||
|
||||
#### route
|
||||
|
||||
将 ICMP 连接路由到指定出站以直接回复。
|
||||
|
||||
详情参阅 [route](/zh/configuration/route/rule_action/#route)。
|
||||
详情参阅 [route](/configuration/route/rule_action/#route)。
|
||||
|
||||
#### bypass
|
||||
|
||||
@@ -44,4 +44,4 @@ icon: material/new-box
|
||||
|
||||
对于其他所有场景,指定了 `outbound` 的 bypass 行为与 `route` 相同。
|
||||
|
||||
详情参阅 [bypass](/zh/configuration/route/rule_action/#bypass)。
|
||||
详情参阅 [bypass](/configuration/route/rule_action/#bypass)。
|
||||
|
||||
@@ -426,7 +426,7 @@ echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/
|
||||
其实现行为无法通过简单复制握手格式来复现,其行为细节必然存在差异,使得检测成为可能。
|
||||
此外,此库缺乏积极维护,且代码质量较差,不建议用于反审查场景。
|
||||
|
||||
如需 TLS 指纹抵抗,请改用 [NaiveProxy](/zh/configuration/inbound/naive/)。
|
||||
如需 TLS 指纹抵抗,请改用 [NaiveProxy](/configuration/inbound/naive/)。
|
||||
|
||||
uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻力。
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ HTTP 请求的额外标头
|
||||
|
||||
!!! note ""
|
||||
|
||||
默认安装不包含标准 gRPC (兼容性好,但性能较差), 参阅 [安装](/zh/installation/build-from-source/#构建标记)。
|
||||
默认安装不包含标准 gRPC (兼容性好,但性能较差), 参阅 [安装](/zh/installation/build-from-source/#_5)。
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -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 支持
|
||||
|
||||
@@ -7,7 +7,7 @@ icon: material/delete-alert
|
||||
#### 旧的 DNS 服务器格式
|
||||
|
||||
DNS 服务器已重构,
|
||||
参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式).
|
||||
参阅 [迁移指南](/migration/#migrate-to-new-dns-servers).
|
||||
|
||||
对旧格式的兼容性将在 sing-box 1.14.0 中被移除。
|
||||
|
||||
@@ -15,7 +15,7 @@ DNS 服务器已重构,
|
||||
|
||||
旧的 `outbound` DNS 规则已废弃,
|
||||
且可被拨号字段代替,
|
||||
参阅 [迁移指南](/zh/migration/#迁移-outbound-dns-规则项到域解析选项).
|
||||
参阅 [迁移指南](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver).
|
||||
|
||||
#### 旧的 ECH 字段
|
||||
|
||||
@@ -31,28 +31,28 @@ ECH 支持已在 sing-box 1.12.0 迁移至使用标准库,但标准库不支
|
||||
#### 旧的特殊出站
|
||||
|
||||
旧的特殊出站(`block` / `dns`)已废弃且可以通过规则动作替代,
|
||||
参阅 [迁移指南](/zh/migration/#迁移旧的特殊出站到规则动作)。
|
||||
参阅 [迁移指南](/migration/#migrate-legacy-special-outbounds-to-rule-actions)。
|
||||
|
||||
旧字段将在 sing-box 1.13.0 中被移除。
|
||||
|
||||
#### 旧的入站字段
|
||||
|
||||
旧的入站字段(`inbound.<sniff/domain_strategy/...>`)已废弃且可以通过规则动作替代,
|
||||
参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作)。
|
||||
参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions)。
|
||||
|
||||
旧字段将在 sing-box 1.13.0 中被移除。
|
||||
|
||||
#### direct 出站中的目标地址覆盖字段
|
||||
|
||||
direct 出站中的目标地址覆盖字段(`override_address` / `override_port`)已废弃且可以通过规则动作替代,
|
||||
参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。
|
||||
参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。
|
||||
|
||||
旧字段将在 sing-box 1.13.0 中被移除。
|
||||
|
||||
#### WireGuard 出站
|
||||
|
||||
WireGuard 出站已废弃且可以通过端点替代,
|
||||
参阅 [迁移指南](/zh/migration/#迁移-wireguard-出站到端点)。
|
||||
参阅 [迁移指南](/migration/#migrate-wireguard-outbound-to-endpoint)。
|
||||
|
||||
旧出站将在 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
|
||||
|
||||
|
||||
@@ -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](/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_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_ccm` | :material-check: | 构建 Claude Code Multiplexer 服务支持。 |
|
||||
| `with_ocm` | :material-check: | 构建 OpenAI Codex Multiplexer 服务支持。 |
|
||||
| `with_naive_outbound` | :material-check: | 构建 NaiveProxy 出站支持,参阅 [NaiveProxy 出站](/zh/configuration/outbound/naive/)。 |
|
||||
| `with_naive_outbound` | :material-check: | 构建 NaiveProxy 出站支持,参阅 [NaiveProxy 出站](/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` 以绕过该限制。 |
|
||||
|
||||
|
||||
@@ -518,9 +518,9 @@ DNS 服务器已经重构。
|
||||
|
||||
!!! info "参考"
|
||||
|
||||
[DNS 规则](/zh/configuration/dns/rule/#outbound) /
|
||||
[拨号字段](/zh/configuration/shared/dial/#domain_resolver) /
|
||||
[路由](/zh/configuration/route/#default_domain_resolver)
|
||||
[DNS 规则](/configuration/dns/rule/#outbound) /
|
||||
[拨号字段](/configuration/shared/dial/#domain_resolver) /
|
||||
[路由](/configuration/route/#default_domain_resolver)
|
||||
|
||||
=== ":material-card-remove: 废弃的"
|
||||
|
||||
@@ -596,7 +596,7 @@ DNS 服务器已经重构。
|
||||
|
||||
!!! info "参考"
|
||||
|
||||
[拨号字段](/zh/configuration/shared/dial/#domain_strategy)
|
||||
[拨号字段](/configuration/shared/dial/#domain_strategy)
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
|
||||
@@ -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 len(t.Metadata.ProcessInfo.AndroidPackageNames) > 0 {
|
||||
processPath = t.Metadata.ProcessInfo.AndroidPackageNames[0]
|
||||
} else if t.Metadata.ProcessInfo.AndroidPackageName != "" {
|
||||
processPath = t.Metadata.ProcessInfo.AndroidPackageName
|
||||
}
|
||||
if processPath == "" {
|
||||
if t.Metadata.ProcessInfo.UserId != -1 {
|
||||
|
||||
@@ -239,15 +239,11 @@ func (c *Connections) Iterator() ConnectionIterator {
|
||||
}
|
||||
|
||||
type ProcessInfo struct {
|
||||
ProcessID int64
|
||||
UserID int32
|
||||
UserName string
|
||||
ProcessPath string
|
||||
packageNames []string
|
||||
}
|
||||
|
||||
func (p *ProcessInfo) PackageNames() StringIterator {
|
||||
return newIterator(p.packageNames)
|
||||
ProcessID int64
|
||||
UserID int32
|
||||
UserName string
|
||||
ProcessPath string
|
||||
PackageName string
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
@@ -343,11 +339,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,
|
||||
packageNames: conn.ProcessInfo.PackageNames,
|
||||
ProcessID: int64(conn.ProcessInfo.ProcessId),
|
||||
UserID: conn.ProcessInfo.UserId,
|
||||
UserName: conn.ProcessInfo.UserName,
|
||||
ProcessPath: conn.ProcessInfo.ProcessPath,
|
||||
PackageName: conn.ProcessInfo.PackageName,
|
||||
}
|
||||
}
|
||||
return Connection{
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -31,18 +31,10 @@ type NeighborUpdateListener interface {
|
||||
}
|
||||
|
||||
type ConnectionOwner struct {
|
||||
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)
|
||||
UserId int32
|
||||
UserName string
|
||||
ProcessPath string
|
||||
AndroidPackageName string
|
||||
}
|
||||
|
||||
type InterfaceUpdateListener interface {
|
||||
|
||||
@@ -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,
|
||||
AndroidPackageNames: result.androidPackageNames,
|
||||
UserId: result.UserId,
|
||||
UserName: result.UserName,
|
||||
ProcessPath: result.ProcessPath,
|
||||
AndroidPackageName: result.AndroidPackageName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
6
go.mod
6
go.mod
@@ -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.20260315153529-ed51f65fbfde
|
||||
github.com/sagernet/sing v0.8.3-0.20260311155444-d39eb42a9f69
|
||||
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.7-0.20260323120017-8eb4e8acfc2d
|
||||
github.com/sagernet/sing-tun v0.8.3-0.20260311132553-5485872f601f
|
||||
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.7
|
||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e
|
||||
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
12
go.sum
@@ -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.20260315153529-ed51f65fbfde h1:RNQzlpnsXIuu1HGts/fIzJ1PR7RhrzaNlU52MDyiX1c=
|
||||
github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing v0.8.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-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.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-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-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.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/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/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=
|
||||
|
||||
@@ -168,7 +168,11 @@ 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 {
|
||||
} 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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,10 +183,6 @@ 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
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -8,6 +11,7 @@ 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"`
|
||||
@@ -15,6 +19,94 @@ type CCMServiceOptions struct {
|
||||
}
|
||||
|
||||
type CCMUser struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -339,7 +339,7 @@ func (o DNSServerAddressOptions) Build() M.Socksaddr {
|
||||
}
|
||||
|
||||
func (o DNSServerAddressOptions) ServerIsDomain() bool {
|
||||
return o.Build().IsDomain()
|
||||
return M.IsDomainName(o.Server)
|
||||
}
|
||||
|
||||
func (o *DNSServerAddressOptions) TakeServerOptions() ServerOptions {
|
||||
|
||||
@@ -44,12 +44,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -8,6 +11,7 @@ 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"`
|
||||
@@ -15,6 +19,94 @@ type OCMServiceOptions struct {
|
||||
}
|
||||
|
||||
type OCMUser struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ func (o ServerOptions) Build() M.Socksaddr {
|
||||
}
|
||||
|
||||
func (o ServerOptions) ServerIsDomain() bool {
|
||||
return o.Build().IsDomain()
|
||||
return M.IsDomainName(o.Server)
|
||||
}
|
||||
|
||||
func (o *ServerOptions) TakeServerOptions() ServerOptions {
|
||||
|
||||
@@ -61,7 +61,7 @@ func (d DERPVerifyClientURLOptions) ServerIsDomain() bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return M.ParseSocksaddr(verifyURL.Hostname()).IsDomain()
|
||||
return M.IsDomainName(verifyURL.Host)
|
||||
}
|
||||
|
||||
func (d DERPVerifyClientURLOptions) MarshalJSON() ([]byte, error) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user