Compare commits

..
Author SHA1 Message Date
世界 d46daeb790 documentation: Bump version 2024-07-13 17:47:57 +08:00
世界 fafc7e76ba Add inline rule-set & Add reload for local rule-set 2024-07-13 17:47:41 +08:00
世界 ec167cf4d2 Unique rule-set names 2024-07-13 17:47:41 +08:00
世界 9f20431d9c Add accept empty DNS rule option 2024-07-13 17:47:40 +08:00
世界 4fc69e5ce6 Add custom options for TUN auto-route and auto-redirect 2024-07-13 17:47:40 +08:00
世界 0e69a92f14 Improve base DNS transports 2024-07-13 17:47:39 +08:00
世界 5945bec4c8 Add auto-redirect & Improve auto-route 2024-07-13 17:47:39 +08:00
世界 9f91a0ce2b Add rule-set decompile command 2024-07-13 17:47:39 +08:00
世界 317d498a12 Add IP address support for rule-set match match 2024-07-13 17:47:39 +08:00
世界 9e99fc1764 WTF is this 2024-07-13 17:47:39 +08:00
世界 8f991720a6 platform: Fix clash server reload on android 2024-07-13 17:47:39 +08:00
世界 bc6ae998c0 platform: Add log update interval 2024-07-13 17:47:38 +08:00
世界 27f0705968 platform: Prepare connections list 2024-07-13 17:47:38 +08:00
世界 2feb4b71e9 Drop support for go1.18 and go1.19 2024-07-13 17:47:38 +08:00
世界 d41dad9a22 Introduce DTLS sniffer 2024-07-13 17:47:38 +08:00
iosmanthusand世界 4c0ddf250f Introduce bittorrent related protocol sniffers
* Introduce bittorrent related protocol sniffers

including, sniffers of
1. BitTorrent Protocol (TCP)
2. uTorrent Transport Protocol (UDP)

Signed-off-by: iosmanthus <myosmanthustree@gmail.com>
Co-authored-by: 世界 <i@sekai.icu>
2024-07-13 17:47:38 +08:00
142 changed files with 4685 additions and 1997 deletions
-20
View File
@@ -33,26 +33,6 @@ jobs:
- name: Run Test
run: |
go test -v ./...
build_go118:
name: Debug build (Go 1.18)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.18
- name: Cache go module
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
key: go118-${{ hashFiles('**/go.sum') }}
- name: Run Test
run: make ci_build_go118
build_go120:
name: Debug build (Go 1.20)
runs-on: ubuntu-latest
+25 -96
View File
@@ -1,93 +1,16 @@
name: Publish Docker Images
name: Build Docker Images
on:
release:
types:
- published
- released
workflow_dispatch:
inputs:
tag:
description: "The tag version you want to build"
env:
REGISTRY_IMAGE: ghcr.io/sagernet/sing-box
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
platform:
- linux/amd64
- linux/arm/v6
- linux/arm/v7
- linux/arm64
- linux/386
- linux/ppc64le
- linux/riscv64
- linux/s390x
steps:
- name: Get commit to build
id: ref
run: |-
if [[ -z "${{ github.event.inputs.tag }}" ]]; then
ref="${{ github.ref_name }}"
else
ref="${{ github.event.inputs.tag }}"
fi
echo "ref=$ref"
echo "ref=$ref" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
with:
ref: ${{ steps.ref.outputs.ref }}
fetch-depth: 0
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
platforms: ${{ matrix.platform }}
context: .
build-args: |
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: ubuntu-latest
needs:
- build
steps:
- name: Get commit to build
id: ref
@@ -106,28 +29,34 @@ jobs:
fi
echo "latest=$latest"
echo "latest=$latest" >> $GITHUB_OUTPUT
- name: Download digests
uses: actions/download-artifact@v4
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Set up Docker Buildx
ref: ${{ steps.ref.outputs.ref }}
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Setup QEMU for Docker Buildx
uses: docker/setup-qemu-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}" \
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}
- name: Docker metadata
id: metadata
uses: docker/metadata-action@v5
with:
images: ghcr.io/sagernet/sing-box
- name: Build and release Docker images
uses: docker/build-push-action@v6
with:
platforms: linux/386,linux/amd64,linux/arm64,linux/s390x
context: .
target: dist
build-args: |
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
tags: |
ghcr.io/sagernet/sing-box:${{ steps.ref.outputs.latest }}
ghcr.io/sagernet/sing-box:${{ steps.ref.outputs.ref }}
push: true
+1 -2
View File
@@ -12,5 +12,4 @@ jobs:
with:
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
days-before-stale: 60
days-before-close: 5
exempt-issue-labels: 'bug,enhancement'
days-before-close: 5
+9 -11
View File
@@ -6,7 +6,14 @@ linters:
- gci
- staticcheck
- paralleltest
- ineffassign
run:
skip-dirs:
- transport/simple-obfs
- transport/clashssr
- transport/cloudflaretls
- transport/shadowtls/tls
- transport/shadowtls/tls_go119
linters-settings:
gci:
@@ -16,13 +23,4 @@ linters-settings:
- prefix(github.com/sagernet/)
- default
staticcheck:
checks:
- all
- -SA1003
run:
go: "1.23"
issues:
exclude-dirs:
- transport/simple-obfs
go: '1.20'
-1
View File
@@ -26,7 +26,6 @@ builds:
- linux_arm_7
- linux_s390x
- linux_riscv64
- linux_mips64le
mod_timestamp: '{{ .CommitTimestamp }}'
snapshot:
name_template: "{{ .Version }}.{{ .ShortCommit }}"
+1 -5
View File
@@ -7,9 +7,7 @@ builds:
- -v
- -trimpath
ldflags:
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
- -s
- -buildid=
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
tags:
- with_gvisor
- with_quic
@@ -27,11 +25,9 @@ builds:
- linux_amd64_v1
- linux_amd64_v3
- linux_arm64
- linux_arm_6
- linux_arm_7
- linux_s390x
- linux_riscv64
- linux_mips64le
- windows_amd64_v1
- windows_amd64_v3
- windows_386
+27 -40
View File
@@ -1,7 +1,6 @@
NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD)
TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api
TAGS_GO120 = with_quic,with_utls
TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls
TAGS_GO121 = with_ech
TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121)
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
@@ -20,13 +19,9 @@ PREFIX ?= $(shell go env GOPATH)
build:
go build $(MAIN_PARAMS) $(MAIN)
ci_build_go118:
go build $(PARAMS) $(MAIN)
go build $(PARAMS) -tags "$(TAGS_GO118)" $(MAIN)
ci_build_go120:
go build $(PARAMS) $(MAIN)
go build $(PARAMS) -tags "$(TAGS_GO118),$(TAGS_GO120)" $(MAIN)
go build $(PARAMS) -tags "$(TAGS_GO120)" $(MAIN)
ci_build:
go build $(PARAMS) $(MAIN)
@@ -104,12 +99,10 @@ publish_android:
publish_android_appcenter:
cd ../sing-box-for-android && ./gradlew :app:appCenterAssembleAndUploadPlayRelease
# TODO: find why and remove `-destination 'generic/platform=iOS'`
build_ios:
cd ../sing-box-for-apple && \
rm -rf build/SFI.xcarchive && \
xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -allowProvisioningUpdates
xcodebuild archive -scheme SFI -configuration Release -archivePath build/SFI.xcarchive
upload_ios_app_store:
cd ../sing-box-for-apple && \
@@ -120,61 +113,55 @@ release_ios: build_ios upload_ios_app_store
build_macos:
cd ../sing-box-for-apple && \
rm -rf build/SFM.xcarchive && \
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -allowProvisioningUpdates
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive
upload_macos_app_store:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath build/SFM.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
xcodebuild -exportArchive -archivePath build/SFM.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
release_macos: build_macos upload_macos_app_store
build_macos_standalone:
build_macos_independent:
cd ../sing-box-for-apple && \
rm -rf build/SFM.System.xcarchive && \
xcodebuild archive -scheme SFM.System -configuration Release -archivePath build/SFM.System.xcarchive -allowProvisioningUpdates
rm -rf build/SFT.System.xcarchive && \
xcodebuild archive -scheme SFM.System -configuration Release -archivePath build/SFM.System.xcarchive
build_macos_dmg:
notarize_macos_independent:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath "build/SFM.System.xcarchive" -exportOptionsPlist SFM.System/Upload.plist -allowProvisioningUpdates
wait_notarize_macos_independent:
sleep 60
export_macos_independent:
rm -rf dist/SFM
mkdir -p dist/SFM
cd ../sing-box-for-apple && \
rm -rf build/SFM.System && \
rm -rf build/SFM.dmg && \
xcodebuild -exportArchive \
-archivePath "build/SFM.System.xcarchive" \
-exportOptionsPlist SFM.System/Export.plist -allowProvisioningUpdates \
-exportPath "build/SFM.System" && \
create-dmg \
--volname "sing-box" \
--volicon "build/SFM.System/SFM.app/Contents/Resources/AppIcon.icns" \
--icon "SFM.app" 0 0 \
--hide-extension "SFM.app" \
--app-drop-link 0 0 \
--skip-jenkins \
--notarize "notarytool-password" \
"../sing-box/dist/SFM/SFM.dmg" "build/SFM.System/SFM.app"
xcodebuild -exportNotarizedApp -archivePath build/SFM.System.xcarchive -exportPath "../sing-box/dist/SFM"
upload_macos_dmg:
upload_macos_independent:
cd dist/SFM && \
cp SFM.dmg "SFM-${VERSION}-universal.dmg" && \
ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.dmg"
rm -f *.zip && \
zip -ry "SFM-${VERSION}-universal.zip" SFM.app && \
ghr --replace --draft --prerelease "v${VERSION}" *.zip
release_macos_standalone: build_macos_standalone build_macos_dmg upload_macos_dmg
release_macos_independent: build_macos_independent notarize_macos_independent wait_notarize_macos_independent export_macos_independent upload_macos_independent
build_tvos:
cd ../sing-box-for-apple && \
rm -rf build/SFT.xcarchive && \
xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive -allowProvisioningUpdates
xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive
upload_tvos_app_store:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
release_tvos: build_tvos upload_tvos_app_store
update_apple_version:
go run ./cmd/internal/update_apple_version
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_independent
release_apple_beta: update_apple_version release_ios release_macos release_tvos
@@ -201,8 +188,8 @@ lib:
go run ./cmd/internal/build_libbox -target ios
lib_install:
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.4
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.4
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.3
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.3
docs:
venv/bin/mkdocs serve
+4
View File
@@ -8,6 +8,10 @@ The universal proxy platform.
https://sing-box.sagernet.org
## Support
https://community.sagernet.org/c/sing-box/
## License
```
+5 -12
View File
@@ -4,14 +4,13 @@ import (
"bytes"
"context"
"encoding/binary"
"io"
"net"
"time"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-dns"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/varbin"
)
type ClashServer interface {
@@ -56,16 +55,15 @@ func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
if err != nil {
return nil, err
}
err = rw.WriteUVariant(&buffer, uint64(len(s.Content)))
err = varbin.Write(&buffer, binary.BigEndian, s.Content)
if err != nil {
return nil, err
}
buffer.Write(s.Content)
err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix())
if err != nil {
return nil, err
}
err = rw.WriteVString(&buffer, s.LastEtag)
err = varbin.Write(&buffer, binary.BigEndian, s.LastEtag)
if err != nil {
return nil, err
}
@@ -79,12 +77,7 @@ func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
if err != nil {
return err
}
contentLen, err := rw.ReadUVariant(reader)
if err != nil {
return err
}
s.Content = make([]byte, contentLen)
_, err = io.ReadFull(reader, s.Content)
err = varbin.Read(reader, binary.BigEndian, &s.Content)
if err != nil {
return err
}
@@ -94,7 +87,7 @@ func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
return err
}
s.LastUpdated = time.Unix(lastUpdated, 0)
s.LastEtag, err = rw.ReadVString(reader)
err = varbin.Read(reader, binary.BigEndian, &s.LastEtag)
if err != nil {
return err
}
+4 -1
View File
@@ -51,7 +51,9 @@ type InboundContext struct {
// rule cache
IPCIDRMatchSource bool
IPCIDRMatchSource bool
IPCIDRAcceptEmpty bool
SourceAddressMatch bool
SourcePortMatch bool
DestinationAddressMatch bool
@@ -62,6 +64,7 @@ type InboundContext struct {
func (c *InboundContext) ResetRuleCache() {
c.IPCIDRMatchSource = false
c.IPCIDRAcceptEmpty = false
c.SourceAddressMatch = false
c.SourcePortMatch = false
c.DestinationAddressMatch = false
+16 -1
View File
@@ -10,15 +10,18 @@ import (
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service"
mdns "github.com/miekg/dns"
"go4.org/netipx"
)
type Router interface {
Service
PreStarter
PostStarter
Cleanup() error
Outbounds() []Outbound
Outbound(tag string) (Outbound, bool)
@@ -45,7 +48,9 @@ type Router interface {
DefaultInterface() string
AutoDetectInterface() bool
AutoDetectInterfaceFunc() control.Func
DefaultMark() int
DefaultMark() uint32
RegisterAutoRedirectOutputMark(mark uint32) error
AutoRedirectOutputMark() uint32
NetworkMonitor() tun.NetworkUpdateMonitor
InterfaceMonitor() tun.DefaultInterfaceMonitor
PackageManager() tun.PackageManager
@@ -92,12 +97,22 @@ type DNSRule interface {
}
type RuleSet interface {
Name() string
StartContext(ctx context.Context, startContext RuleSetStartContext) error
PostStart() error
Metadata() RuleSetMetadata
ExtractIPSet() []*netipx.IPSet
IncRef()
DecRef()
Cleanup()
RegisterCallback(callback RuleSetUpdateCallback) *list.Element[RuleSetUpdateCallback]
UnregisterCallback(element *list.Element[RuleSetUpdateCallback])
Close() error
HeadlessRule
}
type RuleSetUpdateCallback func(it RuleSet)
type RuleSetMetadata struct {
ContainsProcessRule bool
ContainsWIFIRule bool
+23 -6
View File
@@ -111,6 +111,7 @@ func New(options Options) (*Box, error) {
ctx,
router,
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
tag,
inboundOptions,
options.PlatformInterface,
)
@@ -302,7 +303,11 @@ func (s *Box) start() error {
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
}
}
return s.postStart()
err = s.postStart()
if err != nil {
return err
}
return s.router.Cleanup()
}
func (s *Box) postStart() error {
@@ -312,16 +317,28 @@ func (s *Box) postStart() error {
return E.Cause(err, "start ", serviceName)
}
}
for _, outbound := range s.outbounds {
if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound {
// TODO: reorganize ALL start order
for _, out := range s.outbounds {
if lateOutbound, isLateOutbound := out.(adapter.PostStarter); isLateOutbound {
err := lateOutbound.PostStart()
if err != nil {
return E.Cause(err, "post-start outbound/", outbound.Tag())
return E.Cause(err, "post-start outbound/", out.Tag())
}
}
}
return s.router.PostStart()
err := s.router.PostStart()
if err != nil {
return err
}
for _, in := range s.inbounds {
if lateInbound, isLateInbound := in.(adapter.PostStarter); isLateInbound {
err = lateInbound.PostStart()
if err != nil {
return E.Cause(err, "post-start inbound/", in.Tag())
}
}
}
return nil
}
func (s *Box) Close() error {
+3 -1
View File
@@ -45,7 +45,9 @@ func (s *Box) startOutbounds() error {
}
started[outboundTag] = true
canContinue = true
if starter, isStarter := outboundToStart.(common.Starter); isStarter {
if starter, isStarter := outboundToStart.(interface {
Start() error
}); isStarter {
monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
err := starter.Start()
monitor.Finish()
+2 -2
View File
@@ -93,7 +93,7 @@ func buildAndroid() {
const name = "libbox.aar"
copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs")
if rw.FileExists(copyPath) {
if rw.IsDir(copyPath) {
copyPath, _ = filepath.Abs(copyPath)
err = rw.CopyFile(name, filepath.Join(copyPath, name))
if err != nil {
@@ -134,7 +134,7 @@ func buildiOS() {
}
copyPath := filepath.Join("..", "sing-box-for-apple")
if rw.FileExists(copyPath) {
if rw.IsDir(copyPath) {
targetDir := filepath.Join(copyPath, "Libbox.xcframework")
targetDir, _ = filepath.Abs(targetDir)
os.RemoveAll(targetDir)
+5 -5
View File
@@ -30,7 +30,7 @@ func FindSDK() {
}
for _, path := range searchPath {
path = os.ExpandEnv(path)
if rw.FileExists(filepath.Join(path, "licenses", "android-sdk-license")) {
if rw.IsFile(filepath.Join(path, "licenses", "android-sdk-license")) {
androidSDKPath = path
break
}
@@ -60,7 +60,7 @@ func FindSDK() {
func findNDK() bool {
const fixedVersion = "26.2.11394342"
const versionFile = "source.properties"
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.FileExists(filepath.Join(fixedPath, versionFile)) {
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
androidNDKPath = fixedPath
return true
}
@@ -86,7 +86,7 @@ func findNDK() bool {
})
for _, versionName := range versionNames {
currentNDKPath := filepath.Join(androidSDKPath, "ndk", versionName)
if rw.FileExists(filepath.Join(androidSDKPath, versionFile)) {
if rw.IsFile(filepath.Join(androidSDKPath, versionFile)) {
androidNDKPath = currentNDKPath
log.Warn("reproducibility warning: using NDK version " + versionName + " instead of " + fixedVersion)
return true
@@ -100,11 +100,11 @@ var GoBinPath string
func FindMobile() {
goBin := filepath.Join(build.Default.GOPATH, "bin")
if runtime.GOOS == "windows" {
if !rw.FileExists(filepath.Join(goBin, "gobind.exe")) {
if !rw.IsFile(filepath.Join(goBin, "gobind.exe")) {
log.Fatal("missing gomobile installation")
}
} else {
if !rw.FileExists(filepath.Join(goBin, "gobind")) {
if !rw.IsFile(filepath.Join(goBin, "gobind")) {
log.Fatal("missing gomobile installation")
}
}
+2 -2
View File
@@ -26,8 +26,8 @@ func main() {
common.Must(decoder.Decode(&project))
objectsMap := project["objects"].(map[string]any)
projectContent := string(common.Must1(os.ReadFile("sing-box.xcodeproj/project.pbxproj")))
newContent, updated0 := findAndReplace(objectsMap, projectContent, []string{"io.nekohasekai.sfavt"}, newVersion.VersionString())
newContent, updated1 := findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfavt.standalone", "io.nekohasekai.sfavt.system"}, newVersion.String())
newContent, updated0 := findAndReplace(objectsMap, projectContent, []string{"io.nekohasekai.sfa"}, newVersion.VersionString())
newContent, updated1 := findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfa.independent", "io.nekohasekai.sfa.system"}, newVersion.String())
if updated0 || updated1 {
log.Info("updated version to ", newVersion.VersionString(), " (", newVersion.String(), ")")
}
+5 -1
View File
@@ -54,7 +54,11 @@ func merge(outputPath string) error {
return nil
}
}
err = rw.WriteFile(outputPath, buffer.Bytes())
err = rw.MkdirParent(outputPath)
if err != nil {
return err
}
err = os.WriteFile(outputPath, buffer.Bytes(), 0o644)
if err != nil {
return err
}
+1 -1
View File
@@ -6,7 +6,7 @@ import (
var commandRuleSet = &cobra.Command{
Use: "rule-set",
Short: "Manage rule sets",
Short: "Manage rule-sets",
}
func init() {
+1 -1
View File
@@ -55,10 +55,10 @@ func compileRuleSet(sourcePath string) error {
if err != nil {
return err
}
ruleSet, err := plainRuleSet.Upgrade()
if err != nil {
return err
}
ruleSet := plainRuleSet.Upgrade()
var outputPath string
if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".json") {
+83
View File
@@ -0,0 +1,83 @@
package main
import (
"io"
"os"
"strings"
"github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json"
"github.com/spf13/cobra"
)
var flagRuleSetDecompileOutput string
const flagRuleSetDecompileDefaultOutput = "<file_name>.json"
var commandRuleSetDecompile = &cobra.Command{
Use: "decompile [binary-path]",
Short: "Decompile rule-set binary to json",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := decompileRuleSet(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandRuleSet.AddCommand(commandRuleSetDecompile)
commandRuleSetDecompile.Flags().StringVarP(&flagRuleSetDecompileOutput, "output", "o", flagRuleSetDecompileDefaultOutput, "Output file")
}
func decompileRuleSet(sourcePath string) error {
var (
reader io.Reader
err error
)
if sourcePath == "stdin" {
reader = os.Stdin
} else {
reader, err = os.Open(sourcePath)
if err != nil {
return err
}
}
plainRuleSet, err := srs.Read(reader, true)
if err != nil {
return err
}
ruleSet := option.PlainRuleSetCompat{
Version: C.RuleSetVersion1,
Options: plainRuleSet,
}
var outputPath string
if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".srs") {
outputPath = sourcePath[:len(sourcePath)-4] + ".json"
} else {
outputPath = sourcePath + ".json"
}
} else {
outputPath = flagRuleSetDecompileOutput
}
outputFile, err := os.Create(outputPath)
if err != nil {
return err
}
encoder := json.NewEncoder(outputFile)
encoder.SetIndent("", " ")
err = encoder.Encode(ruleSet)
if err != nil {
outputFile.Close()
os.Remove(outputPath)
return err
}
outputFile.Close()
return nil
}
+16 -7
View File
@@ -14,6 +14,7 @@ import (
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json"
M "github.com/sagernet/sing/common/metadata"
"github.com/spf13/cobra"
)
@@ -21,8 +22,8 @@ import (
var flagRuleSetMatchFormat string
var commandRuleSetMatch = &cobra.Command{
Use: "match <rule-set path> <domain>",
Short: "Check if a domain matches the rule set",
Use: "match <rule-set path> <IP address/domain>",
Short: "Check if an IP address or a domain matches the rule-set",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
err := ruleSetMatch(args[0], args[1])
@@ -62,14 +63,24 @@ func ruleSetMatch(sourcePath string, domain string) error {
if err != nil {
return err
}
plainRuleSet = compat.Upgrade()
plainRuleSet, err = compat.Upgrade()
if err != nil {
return err
}
case C.RuleSetFormatBinary:
plainRuleSet, err = srs.Read(bytes.NewReader(content), false)
if err != nil {
return err
}
default:
return E.New("unknown rule set format: ", flagRuleSetMatchFormat)
return E.New("unknown rule-set format: ", flagRuleSetMatchFormat)
}
ipAddress := M.ParseAddr(domain)
var metadata adapter.InboundContext
if ipAddress.IsValid() {
metadata.Destination = M.SocksaddrFrom(ipAddress, 0)
} else {
metadata.Domain = domain
}
for i, ruleOptions := range plainRuleSet.Rules {
var currentRule adapter.HeadlessRule
@@ -77,9 +88,7 @@ func ruleSetMatch(sourcePath string, domain string) error {
if err != nil {
return E.Cause(err, "parse rule_set.rules.[", i, "]")
}
if currentRule.Match(&adapter.InboundContext{
Domain: domain,
}) {
if currentRule.Match(&metadata) {
println(F.ToString("match rules.[", i, "]: ", currentRule))
}
}
+1 -1
View File
@@ -109,7 +109,7 @@ func readConfigAndMerge() (option.Options, error) {
}
var mergedMessage json.RawMessage
for _, options := range optionsList {
mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage)
mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage, false)
if err != nil {
return option.Options{}, E.Cause(err, "merge config at ", options.path)
}
+28 -4
View File
@@ -9,8 +9,10 @@ import (
"net/url"
"os"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"github.com/spf13/cobra"
@@ -32,7 +34,10 @@ func init() {
commandTools.AddCommand(commandFetch)
}
var httpClient *http.Client
var (
httpClient *http.Client
http3Client *http.Client
)
func fetch(args []string) error {
instance, err := createPreStartedClient()
@@ -53,8 +58,16 @@ func fetch(args []string) error {
},
}
defer httpClient.CloseIdleConnections()
if C.WithQUIC {
err = initializeHTTP3Client(instance)
if err != nil {
return err
}
defer http3Client.CloseIdleConnections()
}
for _, urlString := range args {
parsedURL, err := url.Parse(urlString)
var parsedURL *url.URL
parsedURL, err = url.Parse(urlString)
if err != nil {
return err
}
@@ -63,16 +76,27 @@ func fetch(args []string) error {
parsedURL.Scheme = "http"
fallthrough
case "http", "https":
err = fetchHTTP(parsedURL)
err = fetchHTTP(httpClient, parsedURL)
if err != nil {
return err
}
case "http3":
if !C.WithQUIC {
return C.ErrQUICNotIncluded
}
parsedURL.Scheme = "https"
err = fetchHTTP(http3Client, parsedURL)
if err != nil {
return err
}
default:
return E.New("unsupported scheme: ", parsedURL.Scheme)
}
}
return nil
}
func fetchHTTP(parsedURL *url.URL) error {
func fetchHTTP(httpClient *http.Client, parsedURL *url.URL) error {
request, err := http.NewRequest("GET", parsedURL.String(), nil)
if err != nil {
return err
+36
View File
@@ -0,0 +1,36 @@
//go:build with_quic
package main
import (
"context"
"crypto/tls"
"net/http"
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3"
box "github.com/sagernet/sing-box"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func initializeHTTP3Client(instance *box.Box) error {
dialer, err := createDialer(instance, N.NetworkUDP, commandToolsFlagOutbound)
if err != nil {
return err
}
http3Client = &http.Client{
Transport: &http3.RoundTripper{
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
destination := M.ParseSocksaddr(addr)
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
if dErr != nil {
return nil, dErr
}
return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsCfg, cfg)
},
},
}
return nil
}
@@ -0,0 +1,18 @@
//go:build !with_quic
package main
import (
"net/url"
"os"
box "github.com/sagernet/sing-box"
)
func initializeHTTP3Client(instance *box.Box) error {
return os.ErrInvalid
}
func fetchHTTP3(parsedURL *url.URL) error {
return os.ErrInvalid
}
+16 -2
View File
@@ -50,12 +50,26 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
}
if options.RoutingMark != 0 {
var autoRedirectOutputMark uint32
if router != nil {
autoRedirectOutputMark = router.AutoRedirectOutputMark()
}
if autoRedirectOutputMark > 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark))
}
if options.RoutingMark > 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark))
} else if router != nil && router.DefaultMark() != 0 {
if autoRedirectOutputMark > 0 {
return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `routing_mark`")
}
} else if router != nil && router.DefaultMark() > 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(router.DefaultMark()))
listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark()))
if autoRedirectOutputMark > 0 {
return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `default_mark`")
}
}
if options.ReuseAddr {
listener.Control = control.Append(listener.Control, control.ReuseAddr())
+1 -1
View File
@@ -5,7 +5,7 @@ package dialer
import (
"net"
"github.com/metacubex/tfo-go"
"github.com/sagernet/tfo-go"
)
type tcpDialer = tfo.Dialer
+3 -2
View File
@@ -28,12 +28,13 @@ func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error)
} else {
dialer = NewDetour(router, options.Detour)
}
if options.Detour == "" {
domainStrategy := dns.DomainStrategy(options.DomainStrategy)
if domainStrategy != dns.DomainStrategyAsIS || options.Detour == "" {
dialer = NewResolveDialer(
router,
dialer,
options.Detour == "" && !options.TCPFastOpen,
dns.DomainStrategy(options.DomainStrategy),
domainStrategy,
time.Duration(options.FallbackDelay))
}
return dialer, nil
+1 -2
View File
@@ -15,8 +15,7 @@ import (
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/metacubex/tfo-go"
"github.com/sagernet/tfo-go"
)
type slowOpenConn struct {
+38 -42
View File
@@ -1,11 +1,15 @@
package geosite
import (
"bufio"
"encoding/binary"
"io"
"os"
"sync/atomic"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/varbin"
)
type Reader struct {
@@ -34,45 +38,36 @@ func Open(path string) (*Reader, []string, error) {
return reader, codes, nil
}
type geositeMetadata struct {
Code string
Index uint64
Length uint64
}
func (r *Reader) readMetadata() error {
version, err := rw.ReadByte(r.reader)
reader := bufio.NewReader(r.reader)
version, err := reader.ReadByte()
if err != nil {
return err
}
if version != 0 {
return E.New("unknown version")
}
entryLength, err := rw.ReadUVariant(r.reader)
metadataEntries, err := varbin.ReadValue[[]geositeMetadata](reader, binary.BigEndian)
if err != nil {
return err
}
keys := make([]string, entryLength)
domainIndex := make(map[string]int)
domainLength := make(map[string]int)
for i := 0; i < int(entryLength); i++ {
var (
code string
codeIndex uint64
codeLength uint64
)
code, err = rw.ReadVString(r.reader)
if err != nil {
return err
}
keys[i] = code
codeIndex, err = rw.ReadUVariant(r.reader)
if err != nil {
return err
}
codeLength, err = rw.ReadUVariant(r.reader)
if err != nil {
return err
}
domainIndex[code] = int(codeIndex)
domainLength[code] = int(codeLength)
for _, entry := range metadataEntries {
domainIndex[entry.Code] = int(entry.Index)
domainLength[entry.Code] = int(entry.Length)
}
r.domainIndex = domainIndex
r.domainLength = domainLength
if reader.Buffered() > 0 {
return common.Error(r.reader.Seek(int64(-reader.Buffered()), io.SeekCurrent))
}
return nil
}
@@ -85,27 +80,28 @@ func (r *Reader) Read(code string) ([]Item, error) {
if err != nil {
return nil, err
}
counter := &rw.ReadCounter{Reader: r.reader}
domain := make([]Item, r.domainLength[code])
for i := range domain {
var (
item Item
err error
)
item.Type, err = rw.ReadByte(counter)
if err != nil {
return nil, err
}
item.Value, err = rw.ReadVString(counter)
if err != nil {
return nil, err
}
domain[i] = item
counter := &readCounter{Reader: r.reader}
domain, err := varbin.ReadValue[[]Item](bufio.NewReader(counter), binary.BigEndian)
if err != nil {
return nil, err
}
_, err = r.reader.Seek(int64(-index)-counter.Count(), io.SeekCurrent)
_, err = r.reader.Seek(int64(-index)-counter.count, io.SeekCurrent)
return domain, err
}
func (r *Reader) Upstream() any {
return r.reader
}
type readCounter struct {
io.Reader
count int64
}
func (r *readCounter) Read(p []byte) (n int, err error) {
n, err = r.Reader.Read(p)
if n > 0 {
atomic.AddInt64(&r.count, int64(n))
}
return
}
+15 -26
View File
@@ -2,13 +2,14 @@ package geosite
import (
"bytes"
"io"
"encoding/binary"
"sort"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/varbin"
)
func Write(writer io.Writer, domains map[string][]Item) error {
func Write(writer varbin.Writer, domains map[string][]Item) error {
keys := make([]string, 0, len(domains))
for code := range domains {
keys = append(keys, code)
@@ -19,40 +20,28 @@ func Write(writer io.Writer, domains map[string][]Item) error {
index := make(map[string]int)
for _, code := range keys {
index[code] = content.Len()
for _, domain := range domains[code] {
content.WriteByte(domain.Type)
err := rw.WriteVString(content, domain.Value)
if err != nil {
return err
}
err := varbin.Write(content, binary.BigEndian, domains[code])
if err != nil {
return err
}
}
err := rw.WriteByte(writer, 0)
err := writer.WriteByte(0)
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(keys)))
err = varbin.Write(writer, binary.BigEndian, common.Map(keys, func(it string) *geositeMetadata {
return &geositeMetadata{
Code: it,
Index: uint64(index[it]),
Length: uint64(len(domains[it])),
}
}))
if err != nil {
return err
}
for _, code := range keys {
err = rw.WriteVString(writer, code)
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(index[code]))
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(domains[code])))
if err != nil {
return err
}
}
_, err = writer.Write(content.Bytes())
if err != nil {
return err
+2 -2
View File
@@ -60,12 +60,12 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
isIPv4 := ip.Is4()
value, err := unix.SysctlRaw(spath)
value, err := syscall.Sysctl(spath)
if err != nil {
return "", err
}
buf := value
buf := []byte(value)
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
// size/offset are round up (aligned) to 8 bytes in darwin
+113
View File
@@ -0,0 +1,113 @@
package sniff
import (
"bytes"
"context"
"encoding/binary"
"io"
"os"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
)
const (
trackerConnectFlag = iota
trackerAnnounceFlag
trackerScrapeFlag
trackerProtocolID = 0x41727101980
trackerConnectMinSize = 16
trackerAnnounceMinSize = 20
trackerScrapeMinSize = 8
)
// BitTorrent detects if the stream is a BitTorrent connection.
// For the BitTorrent protocol specification, see https://www.bittorrent.org/beps/bep_0003.html
func BitTorrent(_ context.Context, reader io.Reader) (*adapter.InboundContext, error) {
var first byte
err := binary.Read(reader, binary.BigEndian, &first)
if err != nil {
return nil, err
}
if first != 19 {
return nil, os.ErrInvalid
}
var protocol [19]byte
_, err = reader.Read(protocol[:])
if err != nil {
return nil, err
}
if string(protocol[:]) != "BitTorrent protocol" {
return nil, os.ErrInvalid
}
return &adapter.InboundContext{
Protocol: C.ProtocolBitTorrent,
}, nil
}
// UTP detects if the packet is a uTP connection packet.
// For the uTP protocol specification, see
// 1. https://www.bittorrent.org/beps/bep_0029.html
// 2. https://github.com/bittorrent/libutp/blob/2b364cbb0650bdab64a5de2abb4518f9f228ec44/utp_internal.cpp#L112
func UTP(_ context.Context, packet []byte) (*adapter.InboundContext, error) {
// A valid uTP packet must be at least 20 bytes long.
if len(packet) < 20 {
return nil, os.ErrInvalid
}
version := packet[0] & 0x0F
ty := packet[0] >> 4
if version != 1 || ty > 4 {
return nil, os.ErrInvalid
}
// Validate the extensions
extension := packet[1]
reader := bytes.NewReader(packet[20:])
for extension != 0 {
err := binary.Read(reader, binary.BigEndian, &extension)
if err != nil {
return nil, err
}
var length byte
err = binary.Read(reader, binary.BigEndian, &length)
if err != nil {
return nil, err
}
_, err = reader.Seek(int64(length), io.SeekCurrent)
if err != nil {
return nil, err
}
}
return &adapter.InboundContext{
Protocol: C.ProtocolBitTorrent,
}, nil
}
// UDPTracker detects if the packet is a UDP Tracker Protocol packet.
// For the UDP Tracker Protocol specification, see https://www.bittorrent.org/beps/bep_0015.html
func UDPTracker(_ context.Context, packet []byte) (*adapter.InboundContext, error) {
switch {
case len(packet) >= trackerConnectMinSize &&
binary.BigEndian.Uint64(packet[:8]) == trackerProtocolID &&
binary.BigEndian.Uint32(packet[8:12]) == trackerConnectFlag:
fallthrough
case len(packet) >= trackerAnnounceMinSize &&
binary.BigEndian.Uint32(packet[8:12]) == trackerAnnounceFlag:
fallthrough
case len(packet) >= trackerScrapeMinSize &&
binary.BigEndian.Uint32(packet[8:12]) == trackerScrapeFlag:
return &adapter.InboundContext{
Protocol: C.ProtocolBitTorrent,
}, nil
default:
return nil, os.ErrInvalid
}
}
+81
View File
@@ -0,0 +1,81 @@
package sniff_test
import (
"bytes"
"context"
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require"
)
func TestSniffBittorrent(t *testing.T) {
t.Parallel()
packets := []string{
"13426974546f7272656e742070726f746f636f6c0000000000100000e21ea9569b69bab33c97851d0298bdfa89bc90922d5554313631302dea812fcd6a3563e3be40c1d1",
"13426974546f7272656e742070726f746f636f6c00000000001000052aa4f5a7e209e54b32803d43670971c4c8caaa052d5452333030302d653369733079647675763638",
"13426974546f7272656e742070726f746f636f6c00000000001000052aa4f5a7e209e54b32803d43670971c4c8caaa052d5452343035302d6f7a316c6e79377931716130",
}
for _, pkt := range packets {
pkt, err := hex.DecodeString(pkt)
require.NoError(t, err)
metadata, err := sniff.BitTorrent(context.TODO(), bytes.NewReader(pkt))
require.NoError(t, err)
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
}
}
func TestSniffUTP(t *testing.T) {
t.Parallel()
packets := []string{
"010041a282d7ee7b583afb160004000006d8318da776968f92d666f7963f32dae23ba0d2c810d8b8209cc4939f54fde9eeaa521c2c20c9ba7f43f4fb0375f28de06643b5e3ca4685ab7ac76adca99783be72ef05ed59ef4234f5712b75b4c7c0d7bee8fe2ca20ad626ba5bb0ffcc16bf06790896f888048cf72716419a07db1a3dca4550fbcea75b53e97235168a221cf3e553dfbb723961bd719fab038d86e0ecb74747f5a2cd669de1c4b9ad375f3a492d09d98cdfad745435625401315bbba98d35d32086299801377b93495a63a9efddb8d05f5b37a5c5b1c0a25e917f12007bb5e05013ada8aff544fab8cadf61d80ddb0b60f12741e44515a109d144fd53ef845acb4b5ccf0d6fc302d7003d76df3fc3423bb0237301c9e88f900c2d392a8e0fdb36d143cf7527a93fd0a2638b746e72f6699fffcd4fd15348fce780d4caa04382fd9faf1ca0ae377ca805da7536662b84f5ee18dd3ae38fcb095a7543e55f9069ae92c8cf54ae44e97b558d35e2545c66601ed2149cbc32bd6df199a2be7cf0da8b2ff137e0d23e776bc87248425013876d3a3cc31a83b424b752bd0346437f24b532978005d8f5b1b0be1a37a2489c32a18a9ad3118e3f9d30eb299bffae18e1f0677c2a5c185e62519093fe6bc2b7339299ea50a587989f726ca6443a75dd5bb936f6367c6355d80fae53ff529d740b2e5576e3eefdf1fdbfc69c3c8d8ac750512635de63e054bee1d3b689bc1b2bc3d2601e42a00b5c89066d173d4ae7ffedfd2274e5cf6d868fbe640aedb69b8246142f00b32d459974287537ddd5373460dcbc92f5cfdd7a3ed6020822ae922d947893752ca1983d0d32977374c384ac8f5ab566859019b7351526b9f13e932037a55bb052d9deb3b3c23317e0784fdc51a64f2159bfea3b069cf5caf02ee2c3c1a6b6b427bb16165713e8802d95b5c8ed77953690e994bd38c9ae113fedaf6ee7fc2b96c032ceafc2a530ad0422e84546b9c6ad8ef6ea02fa508abddd1805c38a7b42e9b7c971b1b636865ebec06ed754bb404cd6b4e6cc8cb77bd4a0c43410d5cd5ef8fe853a66d49b3b9e06cb141236cdbfdd5761601dc54d1250b86c660e0f898fe62526fdd9acf0eab60a3bbbb2151970461f28f10b31689594bea646c4b03ee197d63bdef4e5a7c22716b3bb9494a83b78ecd81b338b80ac6c09c43485b1b09ba41c74343832c78f0520c1d659ac9eb1502094141e82fb9e5e620970ebc0655514c43c294a7714cbf9a499d277daf089f556398a01589a77494bec8bfb60a108f3813b55368672b88c1af40f6b3c8b513f7c70c3e0efce85228b8b9ec67ba0393f9f7305024d8e2da6a26cf85613d14f249170ce1000089df4c9c260df7f8292aa2ecb5d5bac97656d59aa248caedea2d198e51ce87baece338716d114b458de02d65c9ff808ca5b5b73723b4d1e962d9ac2d98176544dc9984cf8554d07820ef3dd0861cfe57b478328046380de589adad94ee44743ffac73bb7361feca5d56f07cf8ce75080e261282ae30350d7882679b15cab9e7e53ddf93310b33f7390ae5d318bb53f387e6af5d0ef4f947fc9cb8e7e38b52c7f8d772ece6156b38d88796ea19df02c53723b44df7c76315a0de9462f27287e682d2b4cda1a68fe00d7e48c51ee981be44e1ca940fb5190c12655edb4a83c3a4f33e48a015692df4f0b3d61656e362aca657b5ae8c12db5a0db3db1e45135ee918b66918f40e53c4f83e9da0cddfe63f736ae751ab3837a30ae3220d8e8e311487093a7b90c7e7e40dd54ca750e19452f9193aa892aa6a6229ab493dadae988b1724f7898ee69c36d3eb7364c4adbeca811cfe2065873e78c2b6dfdf1595f7a7831c07e03cda82e4f86f76438dfb2b07c13638ce7b509cfa71b88b5102b39a203b423202088e1c2103319cb32c13c1e546ff8612fa194c95a7808ab767c265a1bd5fa0efed5c8ec1701876a00ec8",
"01001ecb68176f215d04326300100000dbcf30292d14b54e9ee2d115ee5b8ebc7fad3e882d4fcdd0c14c6b917c11cb4c6a9f410b52a33ae97c2ac77c7a2b122b8955e09af3c5c595f1b2e79ca57cfe44c44e069610773b9bc9ba223d7f6b383e3adddd03fb88a8476028e30979c2ef321ffc97c5c132bcf9ac5b410bbb5ec6cefca3c7209202a14c5ae922b6b157b0a80249d13ffe5b996af0bc8e54ba576d148372494303e7ead0602b05b9c8fc97d48508a028a04d63a1fd28b0edfcd5c51715f63188b53eefede98a76912dca98518551a8856567307a56a702cbfcc115ea0c755b418bc2c7b57721239b82f09fb24328a4b0ce0f109bcb2a64e04b8aadb1f8487585425acdf8fc4ec8ea93cfcec5ac098bb29d42ddef6e46b03f34a5de28316726699b7cb5195c33e5c48abe87d591d63f9991c84c30819d186d6e0e95fd83c8dff07aa669c4430989bcaccfeacb9bcadbdb4d8f1964dbeb9687745656edd30b21c66cc0a1d742a78717d134a19a7f02d285a4973b1a198c00cfdff4676608dc4f3e817e3463c3b4e2c80d3e8d4fbac541a58a2fb7ad6939f607f8144eff6c8b0adc28ee5609ea158987519892fb",
"21001ecb6817f2805d044fd700100000dbd03029",
"410277ef0b1fb1f60000000000040000c233000000080000000000000000",
}
for _, pkt := range packets {
pkt, err := hex.DecodeString(pkt)
require.NoError(t, err)
metadata, err := sniff.UTP(context.TODO(), pkt)
require.NoError(t, err)
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
}
}
func TestSniffUDPTracker(t *testing.T) {
t.Parallel()
connectPackets := []string{
// connect packets
"00000417271019800000000078e90560",
"00000417271019800000000022c5d64d",
"000004172710198000000000b3863541",
// announce packets
"3d7592ead4b8c9e300000001b871a3820000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002092f616e6e6f756e6365",
"3d7592ead4b8c9e30000000188deed1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002092f616e6e6f756e6365",
"3d7592ead4b8c9e300000001ceb948ad0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a3362cdb7020ff920e5aa642c3d4066950dd1f01f4d00000000000000000000000000000000000000000000000000000000000000000000000000000000000002092f616e6e6f756e6365",
// scrape packets
"3d7592ead4b8c9e300000002d2f4bba5a94a8fe5ccb19ba61c4c0873d391e987982fbbd3",
"3d7592ead4b8c9e300000002441243292aae6c35c94fcfb415dbe95f408b9ce91ee846ed",
"3d7592ead4b8c9e300000002b2aa461b1ad1fa9661cf3fe45fb2504ad52ec6c67758e294",
}
for _, pkt := range connectPackets {
pkt, err := hex.DecodeString(pkt)
require.NoError(t, err)
metadata, err := sniff.UDPTracker(context.TODO(), pkt)
require.NoError(t, err)
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
}
}
+31
View File
@@ -0,0 +1,31 @@
package sniff
import (
"context"
"os"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
)
func DTLSRecord(ctx context.Context, packet []byte) (*adapter.InboundContext, error) {
const fixedHeaderSize = 13
if len(packet) < fixedHeaderSize {
return nil, os.ErrInvalid
}
contentType := packet[0]
switch contentType {
case 20, 21, 22, 23, 25:
default:
return nil, os.ErrInvalid
}
versionMajor := packet[1]
if versionMajor != 0xfe {
return nil, os.ErrInvalid
}
versionMinor := packet[2]
if versionMinor != 0xff && versionMinor != 0xfd {
return nil, os.ErrInvalid
}
return &adapter.InboundContext{Protocol: C.ProtocolDTLS}, nil
}
+30
View File
@@ -0,0 +1,30 @@
package sniff_test
import (
"context"
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require"
)
func TestSniffDTLSClientHello(t *testing.T) {
t.Parallel()
packet, err := hex.DecodeString("16fefd0000000000000000007e010000720000000000000072fefd668a43523798e064bd806d0c87660de9c611a59bbdfc3892c4e072d94f2cafc40000000cc02bc02fc00ac014c02cc0300100003c000d0010000e0403050306030401050106010807ff01000100000a00080006001d00170018000b00020100000e000900060008000700010000170000")
require.NoError(t, err)
metadata, err := sniff.DTLSRecord(context.Background(), packet)
require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolDTLS)
}
func TestSniffDTLSClientApplicationData(t *testing.T) {
t.Parallel()
packet, err := hex.DecodeString("17fefd000100000000000100440001000000000001a4f682b77ecadd10f3f3a2f78d90566212366ff8209fd77314f5a49352f9bb9bd12f4daba0b4736ae29e46b9714d3b424b3e6d0234736619b5aa0d3f")
require.NoError(t, err)
metadata, err := sniff.DTLSRecord(context.Background(), packet)
require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolDTLS)
}
+4 -21
View File
@@ -18,45 +18,28 @@ type (
PacketSniffer = func(ctx context.Context, packet []byte) (*adapter.InboundContext, error)
)
func Skip(metadata adapter.InboundContext) bool {
// skip server first protocols
switch metadata.Destination.Port {
case 25, 465, 587:
// SMTP
return true
case 143, 993:
// IMAP
return true
case 110, 995:
// POP3
return true
}
return false
}
func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) (*adapter.InboundContext, error) {
if timeout == 0 {
timeout = C.ReadPayloadTimeout
}
deadline := time.Now().Add(timeout)
var errors []error
for i := 0; ; i++ {
for i := 0; i < 3; i++ {
err := conn.SetReadDeadline(deadline)
if err != nil {
return nil, E.Cause(err, "set read deadline")
}
_, err = buffer.ReadOnceFrom(conn)
_ = conn.SetReadDeadline(time.Time{})
err = E.Errors(err, conn.SetReadDeadline(time.Time{}))
if err != nil {
if i > 0 {
break
}
return nil, E.Cause(err, "read payload")
}
errors = nil
var metadata *adapter.InboundContext
for _, sniffer := range sniffers {
metadata, err = sniffer(ctx, bytes.NewReader(buffer.Bytes()))
metadata, err := sniffer(ctx, bytes.NewReader(buffer.Bytes()))
if metadata != nil {
return metadata, nil
}
+45 -78
View File
@@ -1,6 +1,7 @@
package srs
import (
"bufio"
"compress/zlib"
"encoding/binary"
"io"
@@ -11,7 +12,7 @@ import (
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/domain"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/varbin"
"go4.org/netipx"
)
@@ -38,14 +39,14 @@ const (
ruleItemFinal uint8 = 0xFF
)
func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) {
func Read(reader io.Reader, recover bool) (ruleSet option.PlainRuleSet, err error) {
var magicBytes [3]byte
_, err = io.ReadFull(reader, magicBytes[:])
if err != nil {
return
}
if magicBytes != MagicBytes {
err = E.New("invalid sing-box rule set file")
err = E.New("invalid sing-box rule-set file")
return
}
var version uint8
@@ -60,13 +61,14 @@ func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err err
if err != nil {
return
}
length, err := rw.ReadUVariant(zReader)
bReader := bufio.NewReader(zReader)
length, err := binary.ReadUvarint(bReader)
if err != nil {
return
}
ruleSet.Rules = make([]option.HeadlessRule, length)
for i := uint64(0); i < length; i++ {
ruleSet.Rules[i], err = readRule(zReader, recovery)
ruleSet.Rules[i], err = readRule(bReader, recover)
if err != nil {
err = E.Cause(err, "read rule[", i, "]")
return
@@ -88,20 +90,25 @@ func Write(writer io.Writer, ruleSet option.PlainRuleSet) error {
if err != nil {
return err
}
err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules)))
bWriter := bufio.NewWriter(zWriter)
_, err = varbin.WriteUvarint(bWriter, uint64(len(ruleSet.Rules)))
if err != nil {
return err
}
for _, rule := range ruleSet.Rules {
err = writeRule(zWriter, rule)
err = writeRule(bWriter, rule)
if err != nil {
return err
}
}
err = bWriter.Flush()
if err != nil {
return err
}
return zWriter.Close()
}
func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) {
func readRule(reader varbin.Reader, recover bool) (rule option.HeadlessRule, err error) {
var ruleType uint8
err = binary.Read(reader, binary.BigEndian, &ruleType)
if err != nil {
@@ -110,17 +117,17 @@ func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err er
switch ruleType {
case 0:
rule.Type = C.RuleTypeDefault
rule.DefaultOptions, err = readDefaultRule(reader, recovery)
rule.DefaultOptions, err = readDefaultRule(reader, recover)
case 1:
rule.Type = C.RuleTypeLogical
rule.LogicalOptions, err = readLogicalRule(reader, recovery)
rule.LogicalOptions, err = readLogicalRule(reader, recover)
default:
err = E.New("unknown rule type: ", ruleType)
}
return
}
func writeRule(writer io.Writer, rule option.HeadlessRule) error {
func writeRule(writer varbin.Writer, rule option.HeadlessRule) error {
switch rule.Type {
case C.RuleTypeDefault:
return writeDefaultRule(writer, rule.DefaultOptions)
@@ -131,7 +138,7 @@ func writeRule(writer io.Writer, rule option.HeadlessRule) error {
}
}
func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) {
func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHeadlessRule, err error) {
var lastItemType uint8
for {
var itemType uint8
@@ -158,6 +165,9 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle
return
}
rule.DomainMatcher = matcher
if recover {
rule.Domain, rule.DomainSuffix = matcher.Dump()
}
case ruleItemDomainKeyword:
rule.DomainKeyword, err = readRuleItemString(reader)
case ruleItemDomainRegex:
@@ -167,7 +177,7 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle
if err != nil {
return
}
if recovery {
if recover {
rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String)
}
case ruleItemIPCIDR:
@@ -175,7 +185,7 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle
if err != nil {
return
}
if recovery {
if recover {
rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String)
}
case ruleItemSourcePort:
@@ -209,7 +219,7 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle
}
}
func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error {
func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule) error {
err := binary.Write(writer, binary.BigEndian, uint8(0))
if err != nil {
return err
@@ -327,73 +337,31 @@ func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error {
return nil
}
func readRuleItemString(reader io.Reader) ([]string, error) {
length, err := rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
value := make([]string, length)
for i := uint64(0); i < length; i++ {
value[i], err = rw.ReadVString(reader)
if err != nil {
return nil, err
}
}
return value, nil
func readRuleItemString(reader varbin.Reader) ([]string, error) {
return varbin.ReadValue[[]string](reader, binary.BigEndian)
}
func writeRuleItemString(writer io.Writer, itemType uint8, value []string) error {
err := binary.Write(writer, binary.BigEndian, itemType)
func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) error {
err := writer.WriteByte(itemType)
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(value)))
return varbin.Write(writer, binary.BigEndian, value)
}
func readRuleItemUint16(reader varbin.Reader) ([]uint16, error) {
return varbin.ReadValue[[]uint16](reader, binary.BigEndian)
}
func writeRuleItemUint16(writer varbin.Writer, itemType uint8, value []uint16) error {
err := writer.WriteByte(itemType)
if err != nil {
return err
}
for _, item := range value {
err = rw.WriteVString(writer, item)
if err != nil {
return err
}
}
return nil
return varbin.Write(writer, binary.BigEndian, value)
}
func readRuleItemUint16(reader io.Reader) ([]uint16, error) {
length, err := rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
value := make([]uint16, length)
for i := uint64(0); i < length; i++ {
err = binary.Read(reader, binary.BigEndian, &value[i])
if err != nil {
return nil, err
}
}
return value, nil
}
func writeRuleItemUint16(writer io.Writer, itemType uint8, value []uint16) error {
err := binary.Write(writer, binary.BigEndian, itemType)
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(value)))
if err != nil {
return err
}
for _, item := range value {
err = binary.Write(writer, binary.BigEndian, item)
if err != nil {
return err
}
}
return nil
}
func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error {
func writeRuleItemCIDR(writer varbin.Writer, itemType uint8, value []string) error {
var builder netipx.IPSetBuilder
for i, prefixString := range value {
prefix, err := netip.ParsePrefix(prefixString)
@@ -419,9 +387,8 @@ func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error {
return writeIPSet(writer, ipSet)
}
func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) {
var mode uint8
err = binary.Read(reader, binary.BigEndian, &mode)
func readLogicalRule(reader varbin.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) {
mode, err := reader.ReadByte()
if err != nil {
return
}
@@ -434,7 +401,7 @@ func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.Logica
err = E.New("unknown logical mode: ", mode)
return
}
length, err := rw.ReadUVariant(reader)
length, err := binary.ReadUvarint(reader)
if err != nil {
return
}
@@ -453,7 +420,7 @@ func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.Logica
return
}
func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error {
func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRule) error {
err := binary.Write(writer, binary.BigEndian, uint8(1))
if err != nil {
return err
@@ -469,7 +436,7 @@ func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule)
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules)))
_, err = varbin.WriteUvarint(writer, uint64(len(logicalRule.Rules)))
if err != nil {
return err
}
+36 -71
View File
@@ -2,11 +2,13 @@ package srs
import (
"encoding/binary"
"io"
"net/netip"
"os"
"unsafe"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/varbin"
"go4.org/netipx"
)
@@ -20,94 +22,57 @@ type myIPRange struct {
to netip.Addr
}
func readIPSet(reader io.Reader) (*netipx.IPSet, error) {
var version uint8
err := binary.Read(reader, binary.BigEndian, &version)
type myIPRangeData struct {
From []byte
To []byte
}
func readIPSet(reader varbin.Reader) (*netipx.IPSet, error) {
version, err := reader.ReadByte()
if err != nil {
return nil, err
}
if version != 1 {
return nil, os.ErrInvalid
}
// WTF why using uint64 here
var length uint64
err = binary.Read(reader, binary.BigEndian, &length)
if err != nil {
return nil, err
}
mySet := &myIPSet{
rr: make([]myIPRange, length),
ranges := make([]myIPRangeData, length)
err = varbin.Read(reader, binary.BigEndian, &ranges)
if err != nil {
return nil, err
}
for i := uint64(0); i < length; i++ {
var (
fromLen uint64
toLen uint64
fromAddr netip.Addr
toAddr netip.Addr
)
fromLen, err = rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
fromBytes := make([]byte, fromLen)
_, err = io.ReadFull(reader, fromBytes)
if err != nil {
return nil, err
}
err = fromAddr.UnmarshalBinary(fromBytes)
if err != nil {
return nil, err
}
toLen, err = rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
toBytes := make([]byte, toLen)
_, err = io.ReadFull(reader, toBytes)
if err != nil {
return nil, err
}
err = toAddr.UnmarshalBinary(toBytes)
if err != nil {
return nil, err
}
mySet.rr[i] = myIPRange{fromAddr, toAddr}
mySet := &myIPSet{
rr: make([]myIPRange, len(ranges)),
}
for i, rangeData := range ranges {
mySet.rr[i].from = M.AddrFromIP(rangeData.From)
mySet.rr[i].to = M.AddrFromIP(rangeData.To)
}
return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil
}
func writeIPSet(writer io.Writer, set *netipx.IPSet) error {
err := binary.Write(writer, binary.BigEndian, uint8(1))
func writeIPSet(writer varbin.Writer, set *netipx.IPSet) error {
err := writer.WriteByte(1)
if err != nil {
return err
}
mySet := (*myIPSet)(unsafe.Pointer(set))
err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr)))
dataList := common.Map((*myIPSet)(unsafe.Pointer(set)).rr, func(rr myIPRange) myIPRangeData {
return myIPRangeData{
From: rr.from.AsSlice(),
To: rr.to.AsSlice(),
}
})
err = binary.Write(writer, binary.BigEndian, uint64(len(dataList)))
if err != nil {
return err
}
for _, rr := range mySet.rr {
var (
fromBinary []byte
toBinary []byte
)
fromBinary, err = rr.from.MarshalBinary()
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(fromBinary)))
if err != nil {
return err
}
_, err = writer.Write(fromBinary)
if err != nil {
return err
}
toBinary, err = rr.to.MarshalBinary()
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(toBinary)))
if err != nil {
return err
}
_, err = writer.Write(toBinary)
for _, data := range dataList {
err = varbin.Write(writer, binary.BigEndian, data)
if err != nil {
return err
}
+62 -131
View File
@@ -11,12 +11,11 @@ import (
"strings"
cftls "github.com/sagernet/cloudflare-tls"
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
"github.com/fsnotify/fsnotify"
)
type echServerConfig struct {
@@ -26,9 +25,8 @@ type echServerConfig struct {
key []byte
certificatePath string
keyPath string
watcher *fsnotify.Watcher
echKeyPath string
echWatcher *fsnotify.Watcher
watcher *fswatch.Watcher
}
func (c *echServerConfig) ServerName() string {
@@ -66,146 +64,84 @@ func (c *echServerConfig) Clone() Config {
}
func (c *echServerConfig) Start() error {
if c.certificatePath != "" && c.keyPath != "" {
err := c.startWatcher()
if err != nil {
c.logger.Warn("create fsnotify watcher: ", err)
}
}
if c.echKeyPath != "" {
err := c.startECHWatcher()
if err != nil {
c.logger.Warn("create fsnotify watcher: ", err)
}
err := c.startWatcher()
if err != nil {
c.logger.Warn("create credentials watcher: ", err)
}
return nil
}
func (c *echServerConfig) startWatcher() error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
var watchPath []string
if c.certificatePath != "" {
err = watcher.Add(c.certificatePath)
if err != nil {
return err
}
watchPath = append(watchPath, c.certificatePath)
}
if c.keyPath != "" {
err = watcher.Add(c.keyPath)
if err != nil {
return err
}
watchPath = append(watchPath, c.keyPath)
}
if c.echKeyPath != "" {
watchPath = append(watchPath, c.echKeyPath)
}
if len(watchPath) == 0 {
return nil
}
watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: watchPath,
Callback: func(path string) {
err := c.credentialsUpdated(path)
if err != nil {
c.logger.Error(E.Cause(err, "reload credentials from ", path))
}
},
})
if err != nil {
return err
}
c.watcher = watcher
go c.loopUpdate()
return nil
}
func (c *echServerConfig) loopUpdate() {
for {
select {
case event, ok := <-c.watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write != fsnotify.Write {
continue
}
err := c.reloadKeyPair()
func (c *echServerConfig) credentialsUpdated(path string) error {
if path == c.certificatePath || path == c.keyPath {
if path == c.certificatePath {
certificate, err := os.ReadFile(c.certificatePath)
if err != nil {
c.logger.Error(E.Cause(err, "reload TLS key pair"))
return err
}
case err, ok := <-c.watcher.Errors:
if !ok {
return
}
c.logger.Error(E.Cause(err, "fsnotify error"))
}
}
}
func (c *echServerConfig) reloadKeyPair() error {
if c.certificatePath != "" {
certificate, err := os.ReadFile(c.certificatePath)
if err != nil {
return E.Cause(err, "reload certificate from ", c.certificatePath)
}
c.certificate = certificate
}
if c.keyPath != "" {
key, err := os.ReadFile(c.keyPath)
if err != nil {
return E.Cause(err, "reload key from ", c.keyPath)
}
c.key = key
}
keyPair, err := cftls.X509KeyPair(c.certificate, c.key)
if err != nil {
return E.Cause(err, "reload key pair")
}
c.config.Certificates = []cftls.Certificate{keyPair}
c.logger.Info("reloaded TLS certificate")
return nil
}
func (c *echServerConfig) startECHWatcher() error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
err = watcher.Add(c.echKeyPath)
if err != nil {
return err
}
c.echWatcher = watcher
go c.loopECHUpdate()
return nil
}
func (c *echServerConfig) loopECHUpdate() {
for {
select {
case event, ok := <-c.echWatcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write != fsnotify.Write {
continue
}
err := c.reloadECHKey()
c.certificate = certificate
} else {
key, err := os.ReadFile(c.keyPath)
if err != nil {
c.logger.Error(E.Cause(err, "reload ECH key"))
return err
}
case err, ok := <-c.echWatcher.Errors:
if !ok {
return
}
c.logger.Error(E.Cause(err, "fsnotify error"))
c.key = key
}
keyPair, err := cftls.X509KeyPair(c.certificate, c.key)
if err != nil {
return E.Cause(err, "parse key pair")
}
c.config.Certificates = []cftls.Certificate{keyPair}
c.logger.Info("reloaded TLS certificate")
} else {
echKeyContent, err := os.ReadFile(c.echKeyPath)
if err != nil {
return err
}
block, rest := pem.Decode(echKeyContent)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return E.New("invalid ECH keys pem")
}
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
if err != nil {
return E.Cause(err, "parse ECH keys")
}
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
if err != nil {
return E.Cause(err, "create ECH key set")
}
c.config.ServerECHProvider = echKeySet
c.logger.Info("reloaded ECH keys")
}
}
func (c *echServerConfig) reloadECHKey() error {
echKeyContent, err := os.ReadFile(c.echKeyPath)
if err != nil {
return err
}
block, rest := pem.Decode(echKeyContent)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return E.New("invalid ECH keys pem")
}
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
if err != nil {
return E.Cause(err, "parse ECH keys")
}
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
if err != nil {
return E.Cause(err, "create ECH key set")
}
c.config.ServerECHProvider = echKeySet
c.logger.Info("reloaded ECH keys")
return nil
}
@@ -213,12 +149,7 @@ func (c *echServerConfig) Close() error {
var err error
if c.watcher != nil {
err = E.Append(err, c.watcher.Close(), func(err error) error {
return E.Cause(err, "close certificate watcher")
})
}
if c.echWatcher != nil {
err = E.Append(err, c.echWatcher.Close(), func(err error) error {
return E.Cause(err, "close ECH key watcher")
return E.Cause(err, "close credentials watcher")
})
}
return err
+21 -44
View File
@@ -7,14 +7,13 @@ import (
"os"
"strings"
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
"github.com/fsnotify/fsnotify"
)
var errInsecureUnused = E.New("tls: insecure unused")
@@ -27,7 +26,7 @@ type STDServerConfig struct {
key []byte
certificatePath string
keyPath string
watcher *fsnotify.Watcher
watcher *fswatch.Watcher
}
func (c *STDServerConfig) ServerName() string {
@@ -88,59 +87,37 @@ func (c *STDServerConfig) Start() error {
}
func (c *STDServerConfig) startWatcher() error {
watcher, err := fsnotify.NewWatcher()
var watchPath []string
if c.certificatePath != "" {
watchPath = append(watchPath, c.certificatePath)
}
if c.keyPath != "" {
watchPath = append(watchPath, c.keyPath)
}
watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: watchPath,
Callback: func(path string) {
err := c.certificateUpdated(path)
if err != nil {
c.logger.Error(err)
}
},
})
if err != nil {
return err
}
if c.certificatePath != "" {
err = watcher.Add(c.certificatePath)
if err != nil {
return err
}
}
if c.keyPath != "" {
err = watcher.Add(c.keyPath)
if err != nil {
return err
}
}
c.watcher = watcher
go c.loopUpdate()
return nil
}
func (c *STDServerConfig) loopUpdate() {
for {
select {
case event, ok := <-c.watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write != fsnotify.Write {
continue
}
err := c.reloadKeyPair()
if err != nil {
c.logger.Error(E.Cause(err, "reload TLS key pair"))
}
case err, ok := <-c.watcher.Errors:
if !ok {
return
}
c.logger.Error(E.Cause(err, "fsnotify error"))
}
}
}
func (c *STDServerConfig) reloadKeyPair() error {
if c.certificatePath != "" {
func (c *STDServerConfig) certificateUpdated(path string) error {
if path == c.certificatePath {
certificate, err := os.ReadFile(c.certificatePath)
if err != nil {
return E.Cause(err, "reload certificate from ", c.certificatePath)
}
c.certificate = certificate
}
if c.keyPath != "" {
} else if path == c.keyPath {
key, err := os.ReadFile(c.keyPath)
if err != nil {
return E.Cause(err, "reload key from ", c.keyPath)
+3 -3
View File
@@ -13,14 +13,14 @@ var resourcePaths []string
func FindPath(name string) (string, bool) {
name = os.ExpandEnv(name)
if rw.FileExists(name) {
if rw.IsFile(name) {
return name, true
}
for _, dir := range resourcePaths {
if path := filepath.Join(dir, dirName, name); rw.FileExists(path) {
if path := filepath.Join(dir, dirName, name); rw.IsFile(path) {
return path, true
}
if path := filepath.Join(dir, name); rw.FileExists(path) {
if path := filepath.Join(dir, name); rw.IsFile(path) {
return path, true
}
}
+7 -5
View File
@@ -1,9 +1,11 @@
package constant
const (
ProtocolTLS = "tls"
ProtocolHTTP = "http"
ProtocolQUIC = "quic"
ProtocolDNS = "dns"
ProtocolSTUN = "stun"
ProtocolTLS = "tls"
ProtocolHTTP = "http"
ProtocolQUIC = "quic"
ProtocolDNS = "dns"
ProtocolSTUN = "stun"
ProtocolBitTorrent = "bittorrent"
ProtocolDTLS = "dtls"
)
+8
View File
@@ -32,6 +32,12 @@ const (
func ProxyDisplayName(proxyType string) string {
switch proxyType {
case TypeTun:
return "TUN"
case TypeRedirect:
return "Redirect"
case TypeTProxy:
return "TProxy"
case TypeDirect:
return "Direct"
case TypeBlock:
@@ -42,6 +48,8 @@ func ProxyDisplayName(proxyType string) string {
return "SOCKS"
case TypeHTTP:
return "HTTP"
case TypeMixed:
return "Mixed"
case TypeShadowsocks:
return "Shadowsocks"
case TypeVMess:
+1
View File
@@ -11,6 +11,7 @@ const (
)
const (
RuleSetTypeInline = "inline"
RuleSetTypeLocal = "local"
RuleSetTypeRemote = "remote"
RuleSetVersion1 = 1
+168 -45
View File
@@ -2,64 +2,187 @@
icon: material/alert-decagram
---
### 1.9.6
#### 1.10.0-alpha.21
* Fixes and improvements
### 1.9.5
#### 1.10.0-alpha.20
* Update quic-go to v0.47.0
* Fix direct dialer not resolving domain
* Fix no error return when empty DNS cache retrieved
* Fix build with go1.23
* Fix stream sniffer
* Fix bad redirect in clash-api
* Fix wireguard events chan leak
* Fix cached conn eats up read deadlines
* Fix disconnected interface selected as default in windows
* Update Bundle Identifiers for Apple platform clients **1**
* Add DTLS sniffer
* Fixes and improvements
#### 1.10.0-alpha.19
* Add `rule-set decompile` command
* Add IP address support for `rule-set match` command
* Fixes and improvements
#### 1.10.0-alpha.18
* Add new `inline` rule-set type **1**
* Add auto reload support for local rule-set
* Update fsnotify usages **2**
* Fixes and improvements
**1**:
See [Migration](/migration/#bundle-identifier-updates-in-apple-platform-clients).
The new [rule-set] type inline (which also becomes the default type)
allows you to write headless rules directly without creating a rule-set file.
We are still working on getting all sing-box apps back on the App Store.
[rule-set]: /configuration/rule-set/
This work is expected to be completed within a week
(SFI on the App Store and others on TestFlight are already available).
**2**:
### 1.9.4
sing-box now uses fsnotify correctly and will not cancel watching
if the target file is deleted or recreated via rename (e.g. `mv`).
* Update quic-go to v0.46.0
* Update Hysteria2 BBR congestion control
* Filter HTTPS ipv4hint/ipv6hint with domain strategy
* Fix crash on Android when using process rules
* Fix non-IP queries accepted by address filter rules
* Fix UDP server for shadowsocks AEAD multi-user inbounds
* Fix default next protos for v2ray QUIC transport
* Fix default end value of port range configuration options
* Fix reset v2ray transports
* Fix panic caused by rule-set generation of duplicate keys for `domain_suffix`
* Fix UDP connnection leak when sniffing
This affects all path options that support reload, including
`tls.certificate_path`, `tls.key_path`, `tls.ech.key_path` and `rule_set.path`.
#### 1.10.0-alpha.17
* Some chaotic changes **1**
* `rule_set_ipcidr_match_source` rule items are renamed **2**
* Add `rule_set_ip_cidr_accept_empty` DNS address filter rule item **3**
* Update quic-go to v0.45.1
* Fixes and improvements
_Due to problems with our Apple developer account,
sing-box apps on Apple platforms are temporarily unavailable for download or update.
If your company or organization is willing to help us return to the App Store,
please [contact us](mailto:contact@sagernet.org)._
**1**:
Something may be broken, please actively report problems with this version.
**2**:
`rule_set_ipcidr_match_source` route and DNS rule items are renamed to
`rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0.
**3**:
See [DNS Rule](/configuration/dns/rule/#rule_set_ip_cidr_accept_empty).
#### 1.10.0-alpha.16
* Add custom options for `auto-route` and `auto-redirect` **1**
* Fixes and improvements
**1**:
See [iproute2_table_index](/configuration/inbound/tun/#iproute2_table_index),
[iproute2_rule_index](/configuration/inbound/tun/#iproute2_rule_index),
[auto_redirect_input_mark](/configuration/inbound/tun/#auto_redirect_input_mark) and
[auto_redirect_output_mark](/configuration/inbound/tun/#auto_redirect_output_mark).
#### 1.10.0-alpha.13
* TUN address fields are merged **1**
* Add route address set support for auto-redirect **2**
**1**:
See [Migration](/migration/#tun-address-fields-are-merged).
**2**:
The new feature will allow you to configure the destination IP CIDR rules
in the specified rule-sets to the firewall automatically.
Specified or unspecified destinations will bypass the sing-box routes to get better performance
(for example, keep hardware offloading of direct traffics on the router).
See [route_address_set](/configuration/inbound/tun/#route_address_set)
and [route_exclude_address_set](/configuration/inbound/tun/#route_exclude_address_set).
#### 1.10.0-alpha.12
* Fix auto-redirect not configuring nftables forward chain correctly
* Fixes and improvements
### 1.9.3
* Fixes and improvements
#### 1.10.0-alpha.10
* Fixes and improvements
### 1.9.2
* Fixes and improvements
#### 1.10.0-alpha.8
* Drop support for go1.18 and go1.19 **1**
* Update quic-go to v0.45.0
* Update Hysteria2 BBR congestion control
* Fixes and improvements
**1**:
Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile.
### 1.9.1
* Fixes and improvements
#### 1.10.0-alpha.7
* Fixes and improvements
#### 1.10.0-alpha.5
* Improve auto-redirect **1**
**1**:
nftables support and DNS hijacking has been added.
Tun inbounds with `auto_route` and `auto_redirect` now works as expected on routers **without intervention**.
#### 1.10.0-alpha.4
* Fix auto-redirect **1**
* Improve auto-route on linux **2**
**1**:
Tun inbounds with `auto_route` and `auto_redirect` now works as expected on routers.
**2**:
Tun inbounds with `auto_route` and `strict_route` now works as expected on routers and servers,
but the usages of [exclude_interface](/configuration/inbound/tun/#exclude_interface) need to be updated.
#### 1.10.0-alpha.2
* Move auto-redirect to Tun **1**
* Fixes and improvements
**1**:
Linux support are added.
See [Tun](/configuration/inbound/tun/#auto_redirect).
#### 1.10.0-alpha.1
* Add tailing comma support in JSON configuration
* Add simple auto-redirect for Android **1**
* Add BitTorrent sniffer **2**
**1**:
It allows you to use redirect inbound in the sing-box Android client
and automatically configures IPv4 TCP redirection via su.
This may alleviate the symptoms of some OCD patients who think that
redirect can effectively save power compared to the system HTTP Proxy.
See [Redirect](/configuration/inbound/redirect/).
**2**:
See [Protocol Sniff](/configuration/route/sniff/).
### 1.9.0
* Fixes and improvements
@@ -363,7 +486,7 @@ See [Address Filter Fields](/configuration/dns/rule#address-filter-fields).
Important changes since 1.7:
* Migrate cache file from Clash API to independent options **1**
* Introducing [Rule Set](/configuration/rule-set/) **2**
* Introducing [rule-set](/configuration/rule-set/) **2**
* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3**
* Allow nested logical rules **4**
* Independent `source_ip_is_private` and `ip_is_private` rules **5**
@@ -383,7 +506,7 @@ See [Cache File](/configuration/experimental/cache-file/) and
**2**:
Rule set is independent collections of rules that can be compiled into binaries to improve performance.
rule-set is independent collections of rules that can be compiled into binaries to improve performance.
Compared to legacy GeoIP and Geosite resources,
it can include more types of rules, load faster,
use less memory, and update automatically.
@@ -391,16 +514,16 @@ use less memory, and update automatically.
See [Route#rule_set](/configuration/route/#rule_set),
[Route Rule](/configuration/route/rule/),
[DNS Rule](/configuration/dns/rule/),
[Rule Set](/configuration/rule-set/),
[rule-set](/configuration/rule-set/),
[Source Format](/configuration/rule-set/source-format/) and
[Headless Rule](/configuration/rule-set/headless-rule/).
For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and
[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets).
For GEO resources migration, see [Migrate GeoIP to rule-sets](/migration/#migrate-geoip-to-rule-sets) and
[Migrate Geosite to rule-sets](/migration/#migrate-geosite-to-rule-sets).
**3**:
New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets.
New commands manage GeoIP, Geosite and rule-set resources, and help you migrate GEO resources to rule-sets.
**4**:
@@ -597,7 +720,7 @@ This change is intended to break incorrect usage and essentially requires no act
**1**:
Now the rules in the `rule_set` rule item can be logically considered to be merged into the rule using rule sets,
Now the rules in the `rule_set` rule item can be logically considered to be merged into the rule using rule-sets,
rather than completely following the AND logic.
#### 1.8.0-alpha.5
@@ -613,7 +736,7 @@ Since GeoIP was deprecated, we made this rule independent, see [Migration](/migr
#### 1.8.0-alpha.1
* Migrate cache file from Clash API to independent options **1**
* Introducing [Rule Set](/configuration/rule-set/) **2**
* Introducing [rule-set](/configuration/rule-set/) **2**
* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3**
* Allow nested logical rules **4**
@@ -624,7 +747,7 @@ See [Cache File](/configuration/experimental/cache-file/) and
**2**:
Rule set is independent collections of rules that can be compiled into binaries to improve performance.
rule-set is independent collections of rules that can be compiled into binaries to improve performance.
Compared to legacy GeoIP and Geosite resources,
it can include more types of rules, load faster,
use less memory, and update automatically.
@@ -632,16 +755,16 @@ use less memory, and update automatically.
See [Route#rule_set](/configuration/route/#rule_set),
[Route Rule](/configuration/route/rule/),
[DNS Rule](/configuration/dns/rule/),
[Rule Set](/configuration/rule-set/),
[rule-set](/configuration/rule-set/),
[Source Format](/configuration/rule-set/source-format/) and
[Headless Rule](/configuration/rule-set/headless-rule/).
For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and
[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets).
For GEO resources migration, see [Migrate GeoIP to rule-sets](/migration/#migrate-geoip-to-rule-sets) and
[Migrate Geosite to rule-sets](/migration/#migrate-geosite-to-rule-sets).
**3**:
New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets.
New commands manage GeoIP, Geosite and rule-set resources, and help you migrate GEO resources to rule-sets.
**4**:
+4 -4
View File
@@ -14,13 +14,13 @@ platform-specific function implementation, such as TUN transparent proxy impleme
## :material-download: Download
* [App Store](https://apps.apple.com/app/sing-box-vt/id6673731168)
* TestFlight (Beta)
* [App Store](https://apps.apple.com/us/app/sing-box/id6451272673)
* ~~TestFlight (Beta)~~
TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai)
(one-time sponsorships are accepted).
Once you donate, you can get an invitation by join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot)
or sending us your Apple ID [via email](mailto:contact@sagernet.org).
Once you donate, you can get an invitation by sending us your Apple ID [via email](mailto:contact@sagernet.org),
or join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot).
## :material-file-download: Download (macOS standalone version)
+3 -3
View File
@@ -2,11 +2,11 @@
由 Project S 维护,提供统一的体验与平台特定的功能。
| 平台 | 客户端 |
|---------------------------------------|------------------------------------------|
| 平台 | 客户端 |
|---------------------------------------|-----------------------------------------|
| :material-android: Android | [sing-box for Android](./android/) |
| :material-apple: iOS/macOS/Apple tvOS | [sing-box for Apple platforms](./apple/) |
| :material-laptop: Desktop | 施工中 |
| :material-laptop: Desktop | 施工中 |
此处没有列出一些声称使用或以 sing-box 为卖点的第三方项目。此类项目维护者的动机是获得更多用户,即使它们提供友好的商业
VPN 客户端功能, 但代码质量很差且包含广告。
+30 -5
View File
@@ -2,6 +2,12 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.10.0"
: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: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
!!! quote "Changes in sing-box 1.9.0"
:material-plus: [geoip](#geoip)
@@ -117,7 +123,10 @@ icon: material/new-box
"geoip-cn",
"geosite-cn"
],
// deprecated
"rule_set_ipcidr_match_source": false,
"rule_set_ip_cidr_match_source": false,
"rule_set_ip_cidr_accept_empty": false,
"invert": false,
"outbound": [
"direct"
@@ -157,7 +166,7 @@ icon: material/new-box
(`source_port` || `source_port_range`) &&
`other fields`
Additionally, included rule sets can be considered merged rather than as a single rule sub-item.
Additionally, included rule-sets can be considered merged rather than as a single rule sub-item.
#### inbound
@@ -303,13 +312,23 @@ Match WiFi BSSID.
!!! question "Since sing-box 1.8.0"
Match [Rule Set](/configuration/route/#rule_set).
Match [rule-set](/configuration/route/#rule_set).
#### rule_set_ipcidr_match_source
!!! question "Since sing-box 1.9.0"
Make `ipcidr` in rule sets match the source IP.
!!! failure "Deprecated in sing-box 1.10.0"
`rule_set_ipcidr_match_source` is renamed to `rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0.
Make `ip_cidr` rule items in rule-sets match the source IP.
#### rule_set_ip_cidr_match_source
!!! question "Since sing-box 1.10.0"
Make `ip_cidr` rule items in rule-sets match the source IP.
#### invert
@@ -347,11 +366,11 @@ Will overrides `dns.client_subnet` and `servers.[].client_subnet`.
### Address Filter Fields
Only takes effect for IP address requests. When the query results do not match the address filtering rule items, the current rule will be skipped.
Only takes effect for address requests (A/AAAA/HTTPS). When the query results do not match the address filtering rule items, the current rule will be skipped.
!!! info ""
`ip_cidr` items in included rule sets also takes effect as an address filtering field.
`ip_cidr` items in included rule-sets also takes effect as an address filtering field.
!!! note ""
@@ -375,6 +394,12 @@ Match IP CIDR with query response.
Match private IP with query response.
#### rule_set_ip_cidr_accept_empty
!!! question "Since sing-box 1.10.0"
Make `ip_cidr` rules in rule-sets accept empty query response.
### Logical Fields
#### type
+28 -3
View File
@@ -2,6 +2,12 @@
icon: material/new-box
---
!!! quote "sing-box 1.10.0 中的更改"
: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: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
!!! quote "sing-box 1.9.0 中的更改"
:material-plus: [geoip](#geoip)
@@ -117,7 +123,10 @@ icon: material/new-box
"geoip-cn",
"geosite-cn"
],
// 已弃用
"rule_set_ipcidr_match_source": false,
"rule_set_ip_cidr_match_source": false,
"rule_set_ip_cidr_accept_empty": false,
"invert": false,
"outbound": [
"direct"
@@ -307,7 +316,17 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
!!! question "自 sing-box 1.9.0 起"
使规则集中的 `ipcidr` 规则匹配源 IP。
!!! failure "已在 sing-box 1.10.0 废弃"
`rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 移除。
使规则集中的 `ip_cidr` 规则匹配源 IP。
#### rule_set_ip_cidr_match_source
!!! question "自 sing-box 1.10.0 起"
使规则集中的 `ip_cidr` 规则匹配源 IP。
#### invert
@@ -345,7 +364,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
### 地址筛选字段
仅对IP地址请求生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。
仅对地址请求 (A/AAAA/HTTPS) 生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。
!!! info ""
@@ -365,7 +384,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
!!! question "自 sing-box 1.9.0 起"
与查询相应匹配 IP CIDR。
与查询响应匹配 IP CIDR。
#### ip_is_private
@@ -373,6 +392,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
与查询响应匹配非公开 IP。
#### rule_set_ip_cidr_accept_empty
!!! question "自 sing-box 1.10.0 起"
使规则集中的 `ip_cidr` 规则接受空查询响应。
### 逻辑字段
#### type
+18 -18
View File
@@ -15,24 +15,24 @@
### Fields
| Type | Format | Injectable |
|---------------|-------------------------------|------------------|
| `direct` | [Direct](./direct/) | :material-close: |
| `mixed` | [Mixed](./mixed/) | TCP |
| `socks` | [SOCKS](./socks/) | TCP |
| `http` | [HTTP](./http/) | TCP |
| `shadowsocks` | [Shadowsocks](./shadowsocks/) | TCP |
| `vmess` | [VMess](./vmess/) | TCP |
| `trojan` | [Trojan](./trojan/) | TCP |
| `naive` | [Naive](./naive/) | :material-close: |
| `hysteria` | [Hysteria](./hysteria/) | :material-close: |
| `shadowtls` | [ShadowTLS](./shadowtls/) | TCP |
| `tuic` | [TUIC](./tuic/) | :material-close: |
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
| `vless` | [VLESS](./vless/) | TCP |
| `tun` | [Tun](./tun/) | :material-close: |
| `redirect` | [Redirect](./redirect/) | :material-close: |
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
| Type | Format | Injectable |
|---------------|-------------------------------|------------|
| `direct` | [Direct](./direct/) | X |
| `mixed` | [Mixed](./mixed/) | TCP |
| `socks` | [SOCKS](./socks/) | TCP |
| `http` | [HTTP](./http/) | TCP |
| `shadowsocks` | [Shadowsocks](./shadowsocks/) | TCP |
| `vmess` | [VMess](./vmess/) | TCP |
| `trojan` | [Trojan](./trojan/) | TCP |
| `naive` | [Naive](./naive/) | X |
| `hysteria` | [Hysteria](./hysteria/) | X |
| `shadowtls` | [ShadowTLS](./shadowtls/) | TCP |
| `tuic` | [TUIC](./tuic/) | X |
| `hysteria2` | [Hysteria2](./hysteria2/) | X |
| `vless` | [VLESS](./vless/) | TCP |
| `tun` | [Tun](./tun/) | X |
| `redirect` | [Redirect](./redirect/) | X |
| `tproxy` | [TProxy](./tproxy/) | X |
#### tag
+18 -18
View File
@@ -15,24 +15,24 @@
### 字段
| 类型 | 格式 | 注入支持 |
|---------------|-------------------------------|------------------|
| `direct` | [Direct](./direct/) | :material-close: |
| `mixed` | [Mixed](./mixed/) | TCP |
| `socks` | [SOCKS](./socks/) | TCP |
| `http` | [HTTP](./http/) | TCP |
| `shadowsocks` | [Shadowsocks](./shadowsocks/) | TCP |
| `vmess` | [VMess](./vmess/) | TCP |
| `trojan` | [Trojan](./trojan/) | TCP |
| `naive` | [Naive](./naive/) | :material-close: |
| `hysteria` | [Hysteria](./hysteria/) | :material-close: |
| `shadowtls` | [ShadowTLS](./shadowtls/) | TCP |
| `tuic` | [TUIC](./tuic/) | :material-close: |
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
| `vless` | [VLESS](./vless/) | TCP |
| `tun` | [Tun](./tun/) | :material-close: |
| `redirect` | [Redirect](./redirect/) | :material-close: |
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
| 类型 | 格式 | 注入支持 |
|---------------|------------------------------|------|
| `direct` | [Direct](./direct/) | X |
| `mixed` | [Mixed](./mixed/) | TCP |
| `socks` | [SOCKS](./socks/) | TCP |
| `http` | [HTTP](./http/) | TCP |
| `shadowsocks` | [Shadowsocks](./shadowsocks/) | TCP |
| `vmess` | [VMess](./vmess/) | TCP |
| `trojan` | [Trojan](./trojan/) | TCP |
| `naive` | [Naive](./naive/) | X |
| `hysteria` | [Hysteria](./hysteria/) | X |
| `shadowtls` | [ShadowTLS](./shadowtls/) | TCP |
| `tuic` | [TUIC](./tuic/) | X |
| `hysteria2` | [Hysteria2](./hysteria2/) | X |
| `vless` | [VLESS](./vless/) | TCP |
| `tun` | [Tun](./tun/) | X |
| `redirect` | [Redirect](./redirect/) | X |
| `tproxy` | [TProxy](./tproxy/) | X |
#### tag
+187 -7
View File
@@ -2,6 +2,25 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.10.0"
:material-plus: [address](#address)
:material-delete-clock: [inet4_address](#inet4_address)
:material-delete-clock: [inet6_address](#inet6_address)
:material-plus: [route_address](#route_address)
:material-delete-clock: [inet4_route_address](#inet4_route_address)
:material-delete-clock: [inet6_route_address](#inet6_route_address)
:material-plus: [route_exclude_address](#route_address)
:material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address)
:material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address)
:material-plus: [iproute2_table_index](#iproute2_table_index)
:material-plus: [iproute2_rule_index](#iproute2_table_index)
:material-plus: [auto_redirect](#auto_redirect)
:material-plus: [auto_redirect_input_mark](#auto_redirect_input_mark)
:material-plus: [auto_redirect_output_mark](#auto_redirect_output_mark)
:material-plus: [route_address_set](#route_address_set)
:material-plus: [route_exclude_address_set](#route_address_set)
!!! quote "Changes in sing-box 1.9.0"
:material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain)
@@ -23,26 +42,61 @@ icon: material/new-box
"type": "tun",
"tag": "tun-in",
"interface_name": "tun0",
"inet4_address": "172.19.0.1/30",
"inet6_address": "fdfe:dcba:9876::1/126",
"address": [
"172.18.0.1/30",
"fdfe:dcba:9876::1/126"
],
// deprecated
"inet4_address": [
"172.19.0.1/30"
],
// deprecated
"inet6_address": [
"fdfe:dcba:9876::1/126"
],
"mtu": 9000,
"gso": false,
"auto_route": true,
"iproute2_table_index": 2022,
"iproute2_rule_index": 9000,
"auto_redirect": false,
"auto_redirect_input_mark": "0x2023",
"auto_redirect_output_mark": "0x2024",
"strict_route": true,
"route_address": [
"0.0.0.0/1",
"128.0.0.0/1",
"::/1",
"8000::/1"
],
// deprecated
"inet4_route_address": [
"0.0.0.0/1",
"128.0.0.0/1"
],
// deprecated
"inet6_route_address": [
"::/1",
"8000::/1"
],
"route_exclude_address": [
"192.168.0.0/16",
"fc00::/7"
],
// deprecated
"inet4_route_exclude_address": [
"192.168.0.0/16"
],
// deprecated
"inet6_route_exclude_address": [
"fc00::/7"
],
"route_address_set": [
"geoip-cloudflare"
],
"route_exclude_address_set": [
"geoip-cn"
],
"endpoint_independent_nat": false,
"udp_timeout": "5m",
"stack": "system",
@@ -83,8 +137,8 @@ icon: material/new-box
"match_domain": []
}
},
... // Listen Fields
...
// Listen Fields
}
```
@@ -102,14 +156,26 @@ icon: material/new-box
Virtual device name, automatically selected if empty.
#### address
!!! question "Since sing-box 1.10.0"
IPv4 and IPv6 prefix for the tun interface.
#### inet4_address
==Required==
!!! failure "Deprecated in sing-box 1.10.0"
`inet4_address` is merged to `address` and will be removed in sing-box 1.11.0.
IPv4 prefix for the tun interface.
#### inet6_address
!!! failure "Deprecated in sing-box 1.10.0"
`inet6_address` is merged to `address` and will be removed in sing-box 1.11.0.
IPv6 prefix for the tun interface.
#### mtu
@@ -122,7 +188,7 @@ The maximum transmission unit.
!!! quote ""
Only supported on Linux.
Only supported on Linux with `auto_route` enabled.
Enable generic segmentation offload.
@@ -138,6 +204,57 @@ Set the default route to the Tun.
By default, VPN takes precedence over tun. To make tun go through VPN, enable `route.override_android_vpn`.
#### iproute2_table_index
!!! question "Since sing-box 1.10.0"
Linux iproute2 table index generated by `auto_route`.
`2022` is used by default.
#### iproute2_rule_index
!!! question "Since sing-box 1.10.0"
Linux iproute2 rule start index generated by `auto_route`.
`9000` is used by default.
#### auto_redirect
!!! question "Since sing-box 1.10.0"
!!! quote ""
Only supported on Linux with `auto_route` enabled.
Automatically configure iptables/nftables to redirect connections.
*In Android*:
Only local connections are forwarded. To share your VPN connection over hotspot or repeater,
use [VPNHotspot](https://github.com/Mygod/VPNHotspot).
*In Linux*:
`auto_route` with `auto_redirect` now works as expected on routers **without intervention**.
#### auto_redirect_input_mark
!!! question "Since sing-box 1.10.0"
Connection input mark used by `route_address_set` and `route_exclude_address_set`.
`0x2023` is used by default.
#### auto_redirect_output_mark
!!! question "Since sing-box 1.10.0"
Connection output mark used by `route_address_set` and `route_exclude_address_set`.
`0x2024` is used by default.
#### strict_route
Enforce strict routing rules when `auto_route` is enabled:
@@ -145,9 +262,10 @@ Enforce strict routing rules when `auto_route` is enabled:
*In Linux*:
* Let unsupported network unreachable
* Make ICMP traffic route to tun instead of upstream interfaces
* Route all connections to tun
It prevents address leaks and makes DNS hijacking work on Android.
It prevents IP address leaks and makes DNS hijacking work on Android.
*In Windows*:
@@ -156,22 +274,80 @@ It prevents address leaks and makes DNS hijacking work on Android.
It may prevent some applications (such as VirtualBox) from working properly in certain situations.
#### route_address
!!! question "Since sing-box 1.10.0"
Use custom routes instead of default when `auto_route` is enabled.
#### inet4_route_address
!!! failure "Deprecated in sing-box 1.10.0"
`inet4_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address)
instead.
Use custom routes instead of default when `auto_route` is enabled.
#### inet6_route_address
!!! failure "Deprecated in sing-box 1.10.0"
`inet6_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address)
instead.
Use custom routes instead of default when `auto_route` is enabled.
#### route_exclude_address
!!! question "Since sing-box 1.10.0"
Exclude custom routes when `auto_route` is enabled.
#### inet4_route_exclude_address
!!! failure "Deprecated in sing-box 1.10.0"
`inet4_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please
use [route_exclude_address](#route_exclude_address) instead.
Exclude custom routes when `auto_route` is enabled.
#### inet6_route_exclude_address
!!! failure "Deprecated in sing-box 1.10.0"
`inet6_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please
use [route_exclude_address](#route_exclude_address) instead.
Exclude custom routes when `auto_route` is enabled.
#### route_address_set
!!! question "Since sing-box 1.10.0"
!!! quote ""
Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled.
Add the destination IP CIDR rules in the specified rule-sets to the firewall.
Unmatched traffic will bypass the sing-box routes.
Conflict with `route.default_mark` and `[dialOptions].routing_mark`.
#### route_exclude_address_set
!!! question "Since sing-box 1.10.0"
!!! quote ""
Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled.
Add the destination IP CIDR rules in the specified rule-sets to the firewall.
Matched traffic will bypass the sing-box routes.
Conflict with `route.default_mark` and `[dialOptions].routing_mark`.
#### endpoint_independent_nat
!!! info ""
@@ -214,6 +390,10 @@ Conflict with `exclude_interface`.
#### exclude_interface
!!! warning ""
When `strict_route` enabled, return traffic to excluded interfaces will not be automatically excluded, so add them as well (example: `br-lan` and `pppoe-wan`).
Exclude interfaces in route.
Conflict with `include_interface`.
+183 -4
View File
@@ -2,6 +2,25 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.10.0"
:material-plus: [address](#address)
:material-delete-clock: [inet4_address](#inet4_address)
:material-delete-clock: [inet6_address](#inet6_address)
:material-plus: [route_address](#route_address)
:material-delete-clock: [inet4_route_address](#inet4_route_address)
:material-delete-clock: [inet6_route_address](#inet6_route_address)
:material-plus: [route_exclude_address](#route_address)
:material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address)
:material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address)
:material-plus: [iproute2_table_index](#iproute2_table_index)
:material-plus: [iproute2_rule_index](#iproute2_table_index)
:material-plus: [auto_redirect](#auto_redirect)
:material-plus: [auto_redirect_input_mark](#auto_redirect_input_mark)
:material-plus: [auto_redirect_output_mark](#auto_redirect_output_mark)
:material-plus: [route_address_set](#route_address_set)
:material-plus: [route_exclude_address_set](#route_address_set)
!!! quote "sing-box 1.9.0 中的更改"
:material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain)
@@ -23,26 +42,61 @@ icon: material/new-box
"type": "tun",
"tag": "tun-in",
"interface_name": "tun0",
"inet4_address": "172.19.0.1/30",
"inet6_address": "fdfe:dcba:9876::1/126",
"address": [
"172.18.0.1/30",
"fdfe:dcba:9876::1/126"
],
// 已弃用
"inet4_address": [
"172.19.0.1/30"
],
// 已弃用
"inet6_address": [
"fdfe:dcba:9876::1/126"
],
"mtu": 9000,
"gso": false,
"auto_route": true,
"iproute2_table_index": 2022,
"iproute2_rule_index": 9000,
"auto_redirect": false,
"auto_redirect_input_mark": "0x2023",
"auto_redirect_output_mark": "0x2024",
"strict_route": true,
"route_address": [
"0.0.0.0/1",
"128.0.0.0/1",
"::/1",
"8000::/1"
],
// 已弃用
"inet4_route_address": [
"0.0.0.0/1",
"128.0.0.0/1"
],
// 已弃用
"inet6_route_address": [
"::/1",
"8000::/1"
],
"route_exclude_address": [
"192.168.0.0/16",
"fc00::/7"
],
// 已弃用
"inet4_route_exclude_address": [
"192.168.0.0/16"
],
// 已弃用
"inet6_route_exclude_address": [
"fc00::/7"
],
"route_address_set": [
"geoip-cloudflare"
],
"route_exclude_address_set": [
"geoip-cn"
],
"endpoint_independent_nat": false,
"udp_timeout": "5m",
"stack": "system",
@@ -102,14 +156,30 @@ icon: material/new-box
虚拟设备名称,默认自动选择。
#### address
!!! question "自 sing-box 1.10.0 起"
==必填==
tun 接口的 IPv4 和 IPv6 前缀。
#### inet4_address
!!! failure "已在 sing-box 1.10.0 废弃"
`inet4_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除。
==必填==
tun 接口的 IPv4 前缀。
#### inet6_address
!!! failure "已在 sing-box 1.10.0 废弃"
`inet6_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除。
tun 接口的 IPv6 前缀。
#### mtu
@@ -138,6 +208,56 @@ tun 接口的 IPv6 前缀。
VPN 默认优先于 tun。要使 tun 经过 VPN,启用 `route.override_android_vpn`。
#### iproute2_table_index
!!! question "自 sing-box 1.10.0 起"
`auto_route` 生成的 iproute2 路由表索引。
默认使用 `2022`。
#### iproute2_rule_index
!!! question "自 sing-box 1.10.0 起"
`auto_route` 生成的 iproute2 规则起始索引。
默认使用 `9000`。
#### auto_redirect
!!! question "自 sing-box 1.10.0 起"
!!! quote ""
仅支持 Linux,且需要 `auto_route` 已启用。
自动配置 iptables 以重定向 TCP 连接。
*在 Android 中*:
仅转发本地 IPv4 连接。 要通过热点或中继共享您的 VPN 连接,请使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot)。
*在 Linux 中*:
带有 `auto_redirect `的 `auto_route` 现在可以在路由器上按预期工作,**无需干预**。
#### auto_redirect_input_mark
!!! question "自 sing-box 1.10.0 起"
`route_address_set` 和 `route_exclude_address_set` 使用的连接输入标记。
默认使用 `0x2023`。
#### auto_redirect_output_mark
!!! question "自 sing-box 1.10.0 起"
`route_address_set` 和 `route_exclude_address_set` 使用的连接输出标记。
默认使用 `0x2024`。
#### strict_route
启用 `auto_route` 时执行严格的路由规则。
@@ -145,9 +265,10 @@ tun 接口的 IPv6 前缀。
*在 Linux 中*:
* 让不支持的网络无法到达
* 使 ICMP 流量路由到 tun 而不是上游接口
* 将所有连接路由到 tun
它可以防止地址泄漏,并使 DNS 劫持在 Android 上工作。
它可以防止 IP 地址泄漏,并使 DNS 劫持在 Android 上工作。
*在 Windows 中*:
@@ -157,22 +278,76 @@ tun 接口的 IPv6 前缀。
它可能会使某些应用程序(如 VirtualBox)在某些情况下无法正常工作。
#### route_address
!!! question "自 sing-box 1.10.0 起"
设置到 Tun 的自定义路由。
#### inet4_route_address
!!! failure "已在 sing-box 1.10.0 废弃"
`inet4_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除。
启用 `auto_route` 时使用自定义路由而不是默认路由。
#### inet6_route_address
!!! failure "已在 sing-box 1.10.0 废弃"
`inet6_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除。
启用 `auto_route` 时使用自定义路由而不是默认路由。
#### route_exclude_address
!!! question "自 sing-box 1.10.0 起"
设置到 Tun 的排除自定义路由。
#### inet4_route_exclude_address
!!! failure "已在 sing-box 1.10.0 废弃"
`inet4_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除。
启用 `auto_route` 时排除自定义路由。
#### inet6_route_exclude_address
!!! failure "已在 sing-box 1.10.0 废弃"
`inet6_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除。
启用 `auto_route` 时排除自定义路由。
#### route_address_set
!!! question "自 sing-box 1.10.0 起"
!!! quote ""
仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。
将指定规则集中的目标 IP CIDR 规则添加到防火墙。
不匹配的流量将绕过 sing-box 路由。
与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。
#### route_exclude_address_set
!!! question "自 sing-box 1.10.0 起"
!!! quote ""
仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。
将指定规则集中的目标 IP CIDR 规则添加到防火墙。
匹配的流量将绕过 sing-box 路由。
与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。
#### endpoint_independent_nat
启用独立于端点的 NAT。
@@ -211,6 +386,10 @@ TCP/IP 栈。
#### exclude_interface
!!! warning ""
当 `strict_route` 启用,到被排除接口的回程流量将不会被自动排除,因此也要添加它们(例:`br-lan` 与 `pppoe-wan`)。
排除路由的接口。
与 `include_interface` 冲突。
@@ -284,7 +463,7 @@ TCP/IP 栈。
!!! note ""
在 Apple 平台,`bypass_domain` 项匹配主机名 **后缀**.
在 Apple 平台,`bypass_domain` 项匹配主机名 **后缀**.
绕过代理的主机名列表。
+1 -1
View File
@@ -39,7 +39,7 @@ List of [Route Rule](./rule/)
!!! question "Since sing-box 1.8.0"
List of [Rule Set](/configuration/rule-set/)
List of [rule-set](/configuration/rule-set/)
#### final
+24 -3
View File
@@ -1,3 +1,12 @@
---
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.10.0"
: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)
!!! quote "Changes in sing-box 1.8.0"
:material-plus: [rule_set](#rule_set)
@@ -105,7 +114,9 @@
"geoip-cn",
"geosite-cn"
],
// deprecated
"rule_set_ipcidr_match_source": false,
"rule_set_ip_cidr_match_source": false,
"invert": false,
"outbound": "direct"
},
@@ -137,7 +148,7 @@
(`source_port` || `source_port_range`) &&
`other fields`
Additionally, included rule sets can be considered merged rather than as a single rule sub-item.
Additionally, included rule-sets can be considered merged rather than as a single rule sub-item.
#### inbound
@@ -297,13 +308,23 @@ Match WiFi BSSID.
!!! question "Since sing-box 1.8.0"
Match [Rule Set](/configuration/route/#rule_set).
Match [rule-set](/configuration/route/#rule_set).
#### rule_set_ipcidr_match_source
!!! question "Since sing-box 1.8.0"
Make `ipcidr` in rule sets match the source IP.
!!! failure "Deprecated in sing-box 1.10.0"
`rule_set_ipcidr_match_source` is renamed to `rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0.
Make `ip_cidr` in rule-sets match the source IP.
#### rule_set_ip_cidr_match_source
!!! question "Since sing-box 1.10.0"
Make `ip_cidr` in rule-sets match the source IP.
#### invert
+22 -1
View File
@@ -1,3 +1,12 @@
---
icon: material/alert-decagram
---
!!! quote "sing-box 1.10.0 中的更改"
: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)
!!! quote "sing-box 1.8.0 中的更改"
:material-plus: [rule_set](#rule_set)
@@ -103,7 +112,9 @@
"geoip-cn",
"geosite-cn"
],
// 已弃用
"rule_set_ipcidr_match_source": false,
"rule_set_ip_cidr_match_source": false,
"invert": false,
"outbound": "direct"
},
@@ -301,7 +312,17 @@
!!! question "自 sing-box 1.8.0 起"
使规则集中的 `ipcidr` 规则匹配源 IP。
!!! failure "已在 sing-box 1.10.0 废弃"
`rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 移除。
使规则集中的 `ip_cidr` 规则匹配源 IP。
#### rule_set_ip_cidr_match_source
!!! question "自 sing-box 1.10.0 起"
使规则集中的 `ip_cidr` 规则匹配源 IP。
#### invert
+18 -7
View File
@@ -1,11 +1,22 @@
---
icon: material/new-box
---
!!! quote "Changes in sing-box 1.10.0"
:material-plus: BitTorrent support
:material-plus: DTLS support
If enabled in the inbound, the protocol and domain name (if present) of by the connection can be sniffed.
#### Supported Protocols
| Network | Protocol | Domain Name |
|:-------:|:--------:|:-----------:|
| TCP | HTTP | Host |
| TCP | TLS | Server Name |
| UDP | QUIC | Server Name |
| UDP | STUN | / |
| TCP/UDP | DNS | / |
| Network | Protocol | Domain Name |
|:-------:|:------------:|:-----------:|
| TCP | `http` | Host |
| TCP | `tls` | Server Name |
| UDP | `quic` | Server Name |
| UDP | `stun` | / |
| TCP/UDP | `dns` | / |
| TCP/UDP | `bittorrent` | / |
| UDP | `dtls` | / |
+18 -7
View File
@@ -1,11 +1,22 @@
---
icon: material/new-box
---
!!! quote "sing-box 1.10.0 中的更改"
:material-plus: BitTorrent 支持
:material-plus: DTLS 支持
如果在入站中启用,则可以嗅探连接的协议和域名(如果存在)。
#### 支持的协议
| 网络 | 协议 | 域名 |
|:-------:|:----:|:-----------:|
| TCP | HTTP | Host |
| TCP | TLS | Server Name |
| UDP | QUIC | Server Name |
| UDP | STUN | / |
| TCP/UDP | DNS | / |
| 网络 | 协议 | 域名 |
|:-------:|:------------:|:-----------:|
| TCP | `http` | Host |
| TCP | `tls` | Server Name |
| UDP | `quic` | Server Name |
| UDP | `stun` | / |
| TCP/UDP | `dns` | / |
| TCP/UDP | `bittorrent` | / |
| UDP | `dtls` | / |
+64 -40
View File
@@ -1,48 +1,56 @@
# Rule Set
---
icon: material/new-box
---
!!! quote "Changes in sing-box 1.10.0"
:material-plus: `type: inline`
# rule-set
!!! question "Since sing-box 1.8.0"
### Structure
```json
{
"type": "",
"tag": "",
"format": "",
... // Typed Fields
}
```
=== "Inline"
#### Local Structure
!!! question "Since sing-box 1.10.0"
```json
{
"type": "local",
...
"path": ""
}
```
```json
{
"type": "inline", // optional
"tag": "",
"rules": []
}
```
#### Remote Structure
=== "Local File"
!!! info ""
```json
{
"type": "local",
"tag": "",
"format": "source", // or binary
"path": ""
}
```
Remote rule-set will be cached if `experimental.cache_file.enabled`.
=== "Remote File"
```json
{
"type": "remote",
...,
"url": "",
"download_detour": "",
"update_interval": ""
}
```
!!! info ""
Remote rule-set will be cached if `experimental.cache_file.enabled`.
```json
{
"type": "remote",
"tag": "",
"format": "source", // or binary
"url": "",
"download_detour": "", // optional
"update_interval": "" // optional
}
```
### Fields
@@ -50,19 +58,31 @@
==Required==
Type of Rule Set, `local` or `remote`.
Type of rule-set, `local` or `remote`.
#### tag
==Required==
Tag of Rule Set.
Tag of rule-set.
### Inline Fields
!!! question "Since sing-box 1.10.0"
#### rules
==Required==
List of [Headless Rule](./headless-rule.md/).
### Local or Remote Fields
#### format
==Required==
Format of Rule Set, `source` or `binary`.
Format of rule-set file, `source` or `binary`.
### Local Fields
@@ -70,7 +90,11 @@ Format of Rule Set, `source` or `binary`.
==Required==
File path of Rule Set.
!!! note ""
Will be automatically reloaded if file modified since sing-box 1.10.0.
File path of rule-set.
### Remote Fields
@@ -78,7 +102,7 @@ File path of Rule Set.
==Required==
Download URL of Rule Set.
Download URL of rule-set.
#### download_detour
@@ -88,6 +112,6 @@ Default outbound will be used if empty.
#### update_interval
Update interval of Rule Set.
Update interval of rule-set.
`1d` will be used if empty.
+1 -1
View File
@@ -21,7 +21,7 @@ Use `sing-box rule-set compile [--output <file-name>.srs] <file-name>.json` to c
==Required==
Version of Rule Set, must be `1`.
Version of rule-set, must be `1`.
#### rules
+1 -4
View File
@@ -83,10 +83,7 @@
如果设置,域名将在请求发出之前解析为 IP。
| 出站 | 受影响的域名 | 默认回退值 |
|----------|--------------------------|-------------------------------------------|
| `direct` | 请求中的域名 | `inbound.domain_strategy` |
| others | 服务器地址中的域名 | / |
默认使用 `dns.strategy`。
#### fallback_delay
+13 -5
View File
@@ -178,6 +178,10 @@ The server certificate line array, in PEM format.
#### certificate_path
!!! note ""
Will be automatically reloaded if file modified.
The path to the server certificate, in PEM format.
#### key
@@ -190,6 +194,10 @@ The server private key line array, in PEM format.
==Server only==
!!! note ""
Will be automatically reloaded if file modified.
The path to the server private key, in PEM format.
## Custom TLS support
@@ -266,6 +274,10 @@ ECH key line array, in PEM format.
==Server only==
!!! note ""
Will be automatically reloaded if file modified.
The path to ECH key, in PEM format.
#### config
@@ -397,8 +409,4 @@ A hexadecimal string with zero to eight digits.
The maximum time difference between the server and the client.
Check disabled if empty.
### Reload
For server configuration, certificate, key and ECH key will be automatically reloaded if modified.
Check disabled if empty.
+12 -4
View File
@@ -176,12 +176,20 @@ TLS 版本值:
#### certificate_path
!!! note ""
文件更改时将自动重新加载。
服务器 PEM 证书路径。
#### key
==仅服务器==
!!! note ""
文件更改时将自动重新加载。
服务器 PEM 私钥行数组。
#### key_path
@@ -258,6 +266,10 @@ ECH PEM 密钥行数组
==仅服务器==
!!! note ""
文件更改时将自动重新加载。
ECH PEM 密钥路径
#### config
@@ -384,7 +396,3 @@ ACME DNS01 验证字段。如果配置,将禁用其他验证方法。
服务器与和客户端之间允许的最大时间差。
默认禁用检查。
### 重载
对于服务器配置,如果修改,证书和密钥将自动重新加载。
+16 -2
View File
@@ -4,6 +4,20 @@ icon: material/delete-alert
# Deprecated Feature List
## 1.10.0
#### TUN address fields are merged
`inet4_address` and `inet6_address` are merged into `address`,
`inet4_route_address` and `inet6_route_address` are merged into `route_address`,
`inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`.
Old fields are deprecated and will be removed in sing-box 1.11.0.
#### Drop support for go1.18 and go1.19
Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile.
## 1.8.0
#### Cache file and related features in Clash API
@@ -19,7 +33,7 @@ The maxmind GeoIP National Database, as an IP classification database,
is not entirely suitable for traffic bypassing,
and all existing implementations suffer from high memory usage and difficult management.
sing-box 1.8.0 introduces [Rule Set](/configuration/rule-set/), which can completely replace GeoIP,
sing-box 1.8.0 introduces [rule-set](/configuration/rule-set/), which can completely replace GeoIP,
check [Migration](/migration/#migrate-geoip-to-rule-sets).
#### Geosite
@@ -29,7 +43,7 @@ Geosite is deprecated and may be removed in the future.
Geosite, the `domain-list-community` project maintained by V2Ray as an early traffic bypassing solution,
suffers from a number of problems, including lack of maintenance, inaccurate rules, and difficult management.
sing-box 1.8.0 introduces [Rule Set](/configuration/rule-set/), which can completely replace Geosite,
sing-box 1.8.0 introduces [rule-set](/configuration/rule-set/), which can completely replace Geosite,
check [Migration](/migration/#migrate-geosite-to-rule-sets).
## 1.6.0
+14
View File
@@ -4,6 +4,20 @@ icon: material/delete-alert
# 废弃功能列表
## 1.10.0
#### TUN 地址字段已合并
`inet4_address` 和 `inet6_address` 已合并为 `address`,
`inet4_route_address` 和 `inet6_route_address` 已合并为 `route_address`,
`inet4_route_exclude_address` 和 `inet6_route_exclude_address` 已合并为 `route_exclude_address`。
旧字段已废弃,且将在 sing-box 1.11.0 中移除。
#### 移除对 go1.18 和 go1.19 的支持
由于维护困难,sing-box 1.10.0 要求至少 Go 1.20 才能编译。
## 1.8.0
#### Clash API 中的 Cache file 及相关功能
+16 -8
View File
@@ -6,18 +6,26 @@ icon: material/file-code
## :material-graph: Requirements
### sing-box 1.10
Before sing-box 1.4.0:
* Go 1.20.0 - ~
* Go 1.18.5 - 1.20.x
Since sing-box 1.4.0:
* Go 1.18.5 - ~
* Go 1.20.0 - ~ with tag `with_quic` enabled
Since sing-box 1.5.0:
* Go 1.18.5 - ~
* Go 1.20.0 - ~ with tag `with_quic` or `with_ech` enabled
Since sing-box 1.8.0:
* Go 1.18.5 - ~
* Go 1.20.0 - ~ with tag `with_quic`, or `with_utls` enabled
* Go 1.21.0 - ~ with tag `with_ech` enabled
### sing-box 1.9
* Go 1.18.5 - 1.22.x
* Go 1.20.0 - 1.22.x with tag `with_quic`, or `with_utls` enabled
* Go 1.21.0 - 1.22.x with tag `with_ech` enabled
You can download and install Go from: https://go.dev/doc/install, latest version is recommended.
## :material-fast-forward: Simple Build
+16 -8
View File
@@ -6,17 +6,25 @@ icon: material/file-code
## :material-graph: 要求
### sing-box 1.10
sing-box 1.4.0 前:
* Go 1.20.0 - ~
* Go 1.20.0 - ~ with tag `with_quic`, or `with_utls` enabled
* Go 1.21.0 - ~ with tag `with_ech` enabled
* Go 1.18.5 - 1.20.x
### sing-box 1.9
从 sing-box 1.4.0:
* Go 1.18.5 - 1.22.x
* Go 1.20.0 - 1.22.x with tag `with_quic`, or `with_utls` enabled
* Go 1.21.0 - 1.22.x with tag `with_ech` enabled
* Go 1.18.5 - ~
* Go 1.20.0 - ~ 如果启用构建标记 `with_quic`
从 sing-box 1.5.0:
* Go 1.18.5 - ~
* Go 1.20.0 - ~ 如果启用构建标记 `with_quic` 或 `with_ech`
从 sing-box 1.8.0:
* Go 1.18.5 - ~
* Go 1.20.0 - ~ 如果启用构建标记 `with_quic` 或 `with_utls`
* Go 1.20.1 - ~ 如果启用构建标记 `with_ech`
您可以从 https://go.dev/doc/install 下载并安装 Go,推荐使用最新版本。
+8 -21
View File
@@ -24,7 +24,14 @@ icon: material/package
sudo dnf config-manager --add-repo https://sing-box.app/sing-box.repo
sudo dnf install sing-box # or sing-box-beta
```
(This applies to any distribution that uses `dnf` as the package manager: Fedora, CentOS, even OpenSUSE with DNF installed.)
=== ":material-redhat: CentOS / YUM"
```bash
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://sing-box.app/sing-box.repo
sudo yum install sing-box # or sing-box-beta
```
## :material-download-box: Manual Installation
@@ -39,7 +46,6 @@ icon: material/package
```bash
bash <(curl -fsSL https://sing-box.app/rpm-install.sh)
```
(This applies to any distribution that uses `rpm` and `systemd`. Because of how `rpm` defines dependencies, if it installs, it probably works.)
=== ":simple-archlinux: Archlinux / PKG"
@@ -57,7 +63,6 @@ icon: material/package
| nixpkgs | NixOS | `nix-env -iA nixos.sing-box` | [![nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/sing-box.svg)][nixpkgs] |
| Homebrew | macOS / Linux | `brew install sing-box` | [![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/sing-box.svg)][brew] |
| APK | Alpine | `apk add sing-box` | [![Alpine Linux Edge package](https://repology.org/badge/version-for-repo/alpine_edge/sing-box.svg)][alpine] |
| DEB | AOSC | `apt install sing-box` | [![AOSC package](https://repology.org/badge/version-for-repo/aosc/sing-box.svg)][aosc] |
=== ":material-apple: macOS"
@@ -85,22 +90,6 @@ icon: material/package
|------------|----------|------------------------|--------------------------------------------------------------------------------------------|
| FreshPorts | FreeBSD | `pkg install sing-box` | [![FreeBSD port](https://repology.org/badge/version-for-repo/freebsd/sing-box.svg)][ports] |
## :material-alert: Problematic Sources
| Type | Platform | Link | Promblem(s) |
|------------|----------|-------------------------------------------------------------------------------------------|------------------------------------------------------------------|
| DEB | AOSC | [aosc-os-abbs](https://github.com/AOSC-Dev/aosc-os-abbs/tree/stable/app-network/sing-box) | Problematic build tag list modification; Not actively maintained |
| Homebrew | / | [homebrew-core][brew] | Problematic build tag list modification |
| Termux | Android | [termux-packages][termux] | Problematic build tag list modification |
| FreshPorts | FreeBSD | [FreeBSD ports][ports] | Old Go (go1.20) |
If you are a user of them, please report issues to them:
1. Please do not modify release build tags without full understanding of the related functionality: enabling non-default
labels may result in decreased performance; the lack of default labels may cause user confusion.
2. sing-box supports compiling with some older Go versions, but it is not recommended (especially versions that are no
longer supported by Go).
## :material-book-multiple: Service Management
For Linux systems with [systemd][systemd], usually the installation already includes a sing-box service,
@@ -139,6 +128,4 @@ you can manage the service using the following command:
[ports]: https://www.freshports.org/net/sing-box
[aosc]: https://packages.aosc.io/packages/sing-box
[systemd]: https://systemd.io/
+8 -20
View File
@@ -24,7 +24,14 @@ icon: material/package
sudo dnf config-manager --add-repo https://sing-box.app/sing-box.repo
sudo dnf install sing-box # or sing-box-beta
```
(这适用于任何使用 `dnf` 作为包管理器的发行版:Fedora、CentOS,甚至安装了 DNF 的 OpenSUSE。)
=== ":material-redhat: CentOS / YUM"
```bash
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://sing-box.app/sing-box.repo
sudo yum install sing-box # or sing-box-beta
```
## :material-download-box: 手动安装
@@ -39,7 +46,6 @@ icon: material/package
```bash
bash <(curl -fsSL https://sing-box.app/rpm-install.sh)
```
(这适用于任何使用 `rpm` 和 `systemd` 的发行版。由于 `rpm` 定义依赖关系的方式,如果安装成功,就多半能用。)
=== ":simple-archlinux: Archlinux / PKG"
@@ -57,7 +63,6 @@ icon: material/package
| nixpkgs | NixOS | `nix-env -iA nixos.sing-box` | [![nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/sing-box.svg)][nixpkgs] |
| Homebrew | macOS / Linux | `brew install sing-box` | [![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/sing-box.svg)][brew] |
| APK | Alpine | `apk add sing-box` | [![Alpine Linux Edge package](https://repology.org/badge/version-for-repo/alpine_edge/sing-box.svg)][alpine] |
| DEB | AOSC | `apt install sing-box` | [![AOSC package](https://repology.org/badge/version-for-repo/aosc/sing-box.svg)][aosc] |
=== ":material-apple: macOS"
@@ -85,21 +90,6 @@ icon: material/package
|------------|---------|------------------------|--------------------------------------------------------------------------------------------|
| FreshPorts | FreeBSD | `pkg install sing-box` | [![FreeBSD port](https://repology.org/badge/version-for-repo/freebsd/sing-box.svg)][ports] |
## :material-alert: 存在问题的源
| 类型 | 平台 | 链接 | 原因 |
|------------|---------|-------------------------------------------------------------------------------------------|-----------------------|
| DEB | AOSC | [aosc-os-abbs](https://github.com/AOSC-Dev/aosc-os-abbs/tree/stable/app-network/sing-box) | 存在问题的构建标志列表修改; 没有活跃维护 |
| Homebrew | / | [homebrew-core][brew] | 存在问题的构建标志列表修改 |
| Termux | Android | [termux-packages][termux] | 存在问题的构建标志列表修改 |
| FreshPorts | FreeBSD | [FreeBSD ports][ports] | 太旧的 Go (go1.20) |
如果您是其用户,请向他们报告问题:
1. 在未完全了解相关功能的情况下,请勿修改发布版本标签:启用非默认标签可能会导致性能下降;缺少默认标签可能会引起用户混淆。
2. sing-box 支持使用一些较旧的 Go 版本进行编译,但不推荐使用(特别是已不再受 Go 支持的版本)。
## :material-book-multiple: 服务管理
对于带有 [systemd][systemd] 的 Linux 系统,通常安装已经包含 sing-box 服务,
@@ -134,6 +124,4 @@ icon: material/package
[ports]: https://www.freshports.org/net/sing-box
[aosc]: https://packages.aosc.io/packages/sing-box
[systemd]: https://systemd.io/
+7 -8
View File
@@ -4,17 +4,16 @@ icon: material/lightning-bolt
# Hysteria 2
Hysteria 2 is a simple, Chinese-made protocol based on QUIC.
The selling point is Brutal, a congestion control algorithm that
tries to achieve a user-defined bandwidth despite packet loss.
The most popular Chinese-made simple protocol based on QUIC, the selling point is Brutal,
a congestion control algorithm that can resist packet loss by manually specifying the required rate by the user.
!!! warning
Even though GFW rarely blocks UDP-based proxies, such protocols actually have far more obvious characteristics than TCP based proxies.
Even though GFW rarely blocks UDP-based proxies, such protocols actually have far more characteristics than TCP based proxies.
| Specification | Resists passive detection | Resists active probes |
|---------------------------------------------------------------------------|---------------------------|-----------------------|
| [hysteria.network](https://v2.hysteria.network/docs/developers/Protocol/) | :material-alert: | :material-check: |
| Specification | Binary Characteristics | Active Detect Hiddenness |
|---------------------------------------------------------------------------|------------------------|--------------------------|
| [hysteria.network](https://v2.hysteria.network/docs/developers/Protocol/) | :material-alert: | :material-check: |
## :material-text-box-check: Password Generator
@@ -45,7 +44,7 @@ To use sing-box with the official program, you need to fill in that combination
Replace `up_mbps` and `down_mbps` values with the actual bandwidth of your server.
=== ":material-harddisk: With local certificate"
```json
{
"inbounds": [
+8 -11
View File
@@ -4,18 +4,15 @@ icon: material/send
# Shadowsocks
Shadowsocks is the most well-known Chinese-made proxy protocol.
It exists in multiple versions, but only AEAD 2022 ciphers
over TCP with multiplexing is recommended.
As the most well-known Chinese-made proxy protocol,
Shadowsocks exists in multiple versions,
but only AEAD 2022 ciphers TCP with multiplexing is recommended.
| Ciphers | Specification | Cryptographically sound | Resists passive detection | Resists active probes |
|----------------|------------------------------------------------------------|-------------------------|---------------------------|-----------------------|
| Stream Ciphers | [shadowsocks.org](https://shadowsocks.org/doc/stream.html) | :material-alert: | :material-alert: | :material-alert: |
| AEAD | [shadowsocks.org](https://shadowsocks.org/doc/aead.html) | :material-check: | :material-alert: | :material-alert: |
| AEAD 2022 | [shadowsocks.org](https://shadowsocks.org/doc/sip022.html) | :material-check: | :material-check: | :material-help: |
(We strongly recommend using multiplexing to send UDP traffic over TCP, because
doing otherwise is vulnerable to passive detection.)
| Ciphers | Specification | Cryptographic Security | Binary Characteristics | Active Detect Hiddenness |
|----------------|------------------------------------------------------------|------------------------|------------------------|--------------------------|
| Stream Ciphers | [shadowsocks.org](https://shadowsocks.org/doc/stream.html) | :material-alert: | :material-alert: | :material-alert: |
| AEAD | [shadowsocks.org](https://shadowsocks.org/doc/aead.html) | :material-check: | :material-alert: | :material-alert: |
| AEAD 2022 | [shadowsocks.org](https://shadowsocks.org/doc/sip022.html) | :material-check: | :material-check: | :material-help: |
## :material-text-box-check: Password Generator
+8 -7
View File
@@ -4,15 +4,15 @@ icon: material/horse
# Trojan
Torjan is the most commonly used TLS proxy made in China. It can be used in various combinations,
As the most commonly used TLS proxy made in China, Trojan can be used in various combinations,
but only the combination of uTLS and multiplexing is recommended.
| Protocol and implementation combination | Specification | Resists passive detection | Resists active probes |
|-----------------------------------------|----------------------------------------------------------------------|---------------------------|-----------------------|
| Origin / trojan-gfw | [trojan-gfw.github.io](https://trojan-gfw.github.io/trojan/protocol) | :material-check: | :material-check: |
| Basic Go implementation | / | :material-alert: | :material-check: |
| with privates transport by V2Ray | No formal definition | :material-alert: | :material-alert: |
| with uTLS enabled | No formal definition | :material-help: | :material-check: |
| Protocol and implementation combination | Specification | Binary Characteristics | Active Detect Hiddenness |
|-----------------------------------------|----------------------------------------------------------------------|------------------------|--------------------------|
| Origin / trojan-gfw | [trojan-gfw.github.io](https://trojan-gfw.github.io/trojan/protocol) | :material-check: | :material-check: |
| Basic Go implementation | / | :material-alert: | :material-check: |
| with privates transport by V2Ray | No formal definition | :material-alert: | :material-alert: |
| with uTLS enabled | No formal definition | :material-help: | :material-check: |
## :material-text-box-check: Password Generator
@@ -211,3 +211,4 @@ but only the combination of uTLS and multiplexing is recommended.
]
}
```
+71 -20
View File
@@ -2,22 +2,73 @@
icon: material/arrange-bring-forward
---
## 1.9.5
## 1.10.0
### Bundle Identifier updates in Apple platform clients
### TUN address fields are merged
Due to problems with our old Apple developer account,
we can only change Bundle Identifiers to re-list sing-box apps,
which means the data will not be automatically inherited.
`inet4_address` and `inet6_address` are merged into `address`,
`inet4_route_address` and `inet6_route_address` are merged into `route_address`,
`inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`.
For iOS, you need to back up your old data yourself (if you still have access to it);
for tvOS, you need to re-import profiles from your iPhone or iPad or create it manually;
for macOS, you can migrate the data folder using the following command:
Old fields are deprecated and will be removed in sing-box 1.11.0.
```bash
cd ~/Library/Group\ Containers && \
mv group.io.nekohasekai.sfa group.io.nekohasekai.sfavt
```
!!! info "References"
[TUN](/configuration/inbound/tun/)
=== ":material-card-remove: Deprecated"
```json
{
"inbounds": [
{
"type": "tun",
"inet4_address": "172.19.0.1/30",
"inet6_address": "fdfe:dcba:9876::1/126",
"inet4_route_address": [
"0.0.0.0/1",
"128.0.0.0/1"
],
"inet6_route_address": [
"::/1",
"8000::/1"
],
"inet4_route_exclude_address": [
"192.168.0.0/16"
],
"inet6_route_exclude_address": [
"fc00::/7"
]
}
]
}
```
=== ":material-card-multiple: New"
```json
{
"inbounds": [
{
"type": "tun",
"address": [
"172.19.0.1/30",
"fdfe:dcba:9876::1/126"
],
"route_address": [
"0.0.0.0/1",
"128.0.0.0/1",
"::/1",
"8000::/1"
],
"route_exclude_address": [
"192.168.0.0/16",
"fc00::/7"
]
}
]
}
```
## 1.9.0
@@ -77,7 +128,7 @@ which will disrupt the existing `process_path` use cases in Windows.
}
```
### :material-checkbox-intermediate: Migrate GeoIP to rule sets
### :material-checkbox-intermediate: Migrate GeoIP to rule-sets
!!! info "References"
@@ -85,11 +136,11 @@ which will disrupt the existing `process_path` use cases in Windows.
[Route](/configuration/route/) /
[Route Rule](/configuration/route/rule/) /
[DNS Rule](/configuration/dns/rule/) /
[Rule Set](/configuration/rule-set/)
[rule-set](/configuration/rule-set/)
!!! tip
`sing-box geoip` commands can help you convert custom GeoIP into rule sets.
`sing-box geoip` commands can help you convert custom GeoIP into rule-sets.
=== ":material-card-remove: Deprecated"
@@ -156,13 +207,13 @@ which will disrupt the existing `process_path` use cases in Windows.
},
"experimental": {
"cache_file": {
"enabled": true // required to save Rule Set cache
"enabled": true // required to save rule-set cache
}
}
}
```
### :material-checkbox-intermediate: Migrate Geosite to rule sets
### :material-checkbox-intermediate: Migrate Geosite to rule-sets
!!! info "References"
@@ -170,11 +221,11 @@ which will disrupt the existing `process_path` use cases in Windows.
[Route](/configuration/route/) /
[Route Rule](/configuration/route/rule/) /
[DNS Rule](/configuration/dns/rule/) /
[Rule Set](/configuration/rule-set/)
[rule-set](/configuration/rule-set/)
!!! tip
`sing-box geosite` commands can help you convert custom Geosite into rule sets.
`sing-box geosite` commands can help you convert custom Geosite into rule-sets.
=== ":material-card-remove: Deprecated"
@@ -217,7 +268,7 @@ which will disrupt the existing `process_path` use cases in Windows.
},
"experimental": {
"cache_file": {
"enabled": true // required to save Rule Set cache
"enabled": true // required to save rule-set cache
}
}
}
+65 -13
View File
@@ -2,21 +2,73 @@
icon: material/arrange-bring-forward
---
## 1.9.5
## 1.10.0
### Apple 平台客户端的 Bundle Identifier 更新
### TUN 地址字段已合并
由于我们旧的苹果开发者账户存在问题,我们只能通过更新 Bundle Identifiers
来重新上架 sing-box 应用, 这意味着数据不会自动继承。
`inet4_address` 和 `inet6_address` 已合并为 `address`,
`inet4_route_address` 和 `inet6_route_address` 已合并为 `route_address`,
`inet4_route_exclude_address` 和 `inet6_route_exclude_address` 已合并为 `route_exclude_address`。
对于 iOS,您需要自行备份旧的数据(如果您仍然可以访问);
对于 Apple tvOS,您需要从 iPhone 或 iPad 重新导入配置或者手动创建;
对于 macOS,您可以使用以下命令迁移数据文件夹:
旧字段已废弃,且将在 sing-box 1.11.0 中移除。
```bash
cd ~/Library/Group\ Containers && \
mv group.io.nekohasekai.sfa group.io.nekohasekai.sfavt
```
!!! info "参考"
[TUN](/zh/configuration/inbound/tun/)
=== ":material-card-remove: 弃用的"
```json
{
"inbounds": [
{
"type": "tun",
"inet4_address": "172.19.0.1/30",
"inet6_address": "fdfe:dcba:9876::1/126",
"inet4_route_address": [
"0.0.0.0/1",
"128.0.0.0/1"
],
"inet6_route_address": [
"::/1",
"8000::/1"
],
"inet4_route_exclude_address": [
"192.168.0.0/16"
],
"inet6_route_exclude_address": [
"fc00::/7"
]
}
]
}
```
=== ":material-card-multiple: 新的"
```json
{
"inbounds": [
{
"type": "tun",
"address": [
"172.19.0.1/30",
"fdfe:dcba:9876::1/126"
],
"route_address": [
"0.0.0.0/1",
"128.0.0.0/1",
"::/1",
"8000::/1"
],
"route_exclude_address": [
"192.168.0.0/16",
"fc00::/7"
]
}
]
}
```
## 1.9.0
@@ -154,7 +206,7 @@ sing-box 1.9.0 使 QueryFullProcessImageNameW 输出 Win32 路径(如 `C:\fold
},
"experimental": {
"cache_file": {
"enabled": true // required to save Rule Set cache
"enabled": true // required to save rule-set cache
}
}
}
@@ -215,7 +267,7 @@ sing-box 1.9.0 使 QueryFullProcessImageNameW 输出 Win32 路径(如 `C:\fold
},
"experimental": {
"cache_file": {
"enabled": true // required to save Rule Set cache
"enabled": true // required to save rule-set cache
}
}
}
-26
View File
@@ -1,26 +0,0 @@
---
icon: material/hand-coin
---
# Sponsors
Do you or your friends use sing-box?
You can help keep the project bug-free and feature rich by sponsoring
the project maintainer via [GitHub Sponsors](https://github.com/sponsors/nekohasekai).
![](https://nekohasekai.github.io/sponsor-images/sponsors.svg)
### Special Sponsors
**Viral Tech, Inc.**
Helping us re-list sing-box apps on the Apple Store.
---
[![JetBrains logo](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://www.jetbrains.com)
Free license for the amazing IDEs.
---
+2 -1
View File
@@ -5,7 +5,8 @@ icon: material/forum
# Support
| Channel | Link |
| :---------------------------- | :------------------------------------------ |
|:------------------------------|:--------------------------------------------|
| Community | https://community.sagernet.org |
| GitHub Issues | https://github.com/SagerNet/sing-box/issues |
| Telegram notification channel | https://t.me/yapnc |
| Telegram user group | https://t.me/yapug |
+6 -5
View File
@@ -4,10 +4,11 @@ icon: material/forum
# 支持
| 通道 | 链接 |
| :---------------- | :------------------------------------------ |
| GitHub Issues | https://github.com/SagerNet/sing-box/issues |
| 通道 | 链接 |
|:--------------|:--------------------------------------------|
| 社区 | https://community.sagernet.org |
| GitHub Issues | https://github.com/SagerNet/sing-box/issues |
| Telegram 通知频道 | https://t.me/yapnc |
| Telegram 用户组 | https://t.me/yapug |
| 邮件 | contact@sagernet.org |
| Telegram 用户组 | https://t.me/yapug |
| 邮件 | contact@sagernet.org |
+3 -2
View File
@@ -14,6 +14,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/gofrs/uuid/v5"
)
func connectionRouter(router adapter.Router, trafficManager *trafficontrol.Manager) http.Handler {
@@ -76,10 +77,10 @@ func getConnections(trafficManager *trafficontrol.Manager) func(w http.ResponseW
func closeConnection(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
id := uuid.FromStringOrNil(chi.URLParam(r, "id"))
snapshot := trafficManager.Snapshot()
for _, c := range snapshot.Connections {
if id == c.ID() {
if id == c.Metadata().ID {
c.Close()
break
}
+19 -51
View File
@@ -7,7 +7,9 @@ import (
"net"
"net/http"
"os"
"runtime"
"strings"
"syscall"
"time"
"github.com/sagernet/sing-box/adapter"
@@ -19,7 +21,6 @@ import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
@@ -144,7 +145,18 @@ func (s *Server) PreStart() error {
func (s *Server) Start() error {
if s.externalController {
s.checkAndDownloadExternalUI()
listener, err := net.Listen("tcp", s.httpServer.Addr)
var (
listener net.Listener
err error
)
for i := 0; i < 3; i++ {
listener, err = net.Listen("tcp", s.httpServer.Addr)
if runtime.GOOS == "android" && errors.Is(err, syscall.EADDRINUSE) {
time.Sleep(100 * time.Millisecond)
continue
}
break
}
if err != nil {
return E.Cause(err, "external controller listen error")
}
@@ -218,58 +230,15 @@ func (s *Server) TrafficManager() *trafficontrol.Manager {
}
func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule) (net.Conn, adapter.Tracker) {
tracker := trafficontrol.NewTCPTracker(conn, s.trafficManager, castMetadata(metadata), s.router, matchedRule)
tracker := trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.router, matchedRule)
return tracker, tracker
}
func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule) (N.PacketConn, adapter.Tracker) {
tracker := trafficontrol.NewUDPTracker(conn, s.trafficManager, castMetadata(metadata), s.router, matchedRule)
tracker := trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.router, matchedRule)
return tracker, tracker
}
func castMetadata(metadata adapter.InboundContext) trafficontrol.Metadata {
var inbound string
if metadata.Inbound != "" {
inbound = metadata.InboundType + "/" + metadata.Inbound
} else {
inbound = metadata.InboundType
}
var domain string
if metadata.Domain != "" {
domain = metadata.Domain
} else {
domain = metadata.Destination.Fqdn
}
var processPath string
if metadata.ProcessInfo != nil {
if metadata.ProcessInfo.ProcessPath != "" {
processPath = metadata.ProcessInfo.ProcessPath
} else if metadata.ProcessInfo.PackageName != "" {
processPath = metadata.ProcessInfo.PackageName
}
if processPath == "" {
if metadata.ProcessInfo.UserId != -1 {
processPath = F.ToString(metadata.ProcessInfo.UserId)
}
} else if metadata.ProcessInfo.User != "" {
processPath = F.ToString(processPath, " (", metadata.ProcessInfo.User, ")")
} else if metadata.ProcessInfo.UserId != -1 {
processPath = F.ToString(processPath, " (", metadata.ProcessInfo.UserId, ")")
}
}
return trafficontrol.Metadata{
NetWork: metadata.Network,
Type: inbound,
SrcIP: metadata.Source.Addr,
DstIP: metadata.Destination.Addr,
SrcPort: F.ToString(metadata.Source.Port),
DstPort: F.ToString(metadata.Destination.Port),
Host: domain,
DNSMode: "normal",
ProcessPath: processPath,
}
}
func authentication(serverSecret string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
@@ -308,11 +277,10 @@ func authentication(serverSecret string) func(next http.Handler) http.Handler {
func hello(redirect bool) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
contentType := r.Header.Get("Content-Type")
if !redirect || contentType == "application/json" {
render.JSON(w, r, render.M{"hello": "clash"})
} else {
if redirect {
http.Redirect(w, r, "/ui/", http.StatusTemporaryRedirect)
} else {
render.JSON(w, r, render.M{"hello": "clash"})
}
}
}
+72 -19
View File
@@ -2,10 +2,17 @@ package trafficontrol
import (
"runtime"
"sync"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/clashapi/compatible"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/x/list"
"github.com/gofrs/uuid/v5"
)
type Manager struct {
@@ -16,9 +23,11 @@ type Manager struct {
uploadTotal atomic.Int64
downloadTotal atomic.Int64
connections compatible.Map[string, tracker]
ticker *time.Ticker
done chan struct{}
connections compatible.Map[uuid.UUID, Tracker]
closedConnectionsAccess sync.Mutex
closedConnections list.List[TrackerMetadata]
ticker *time.Ticker
done chan struct{}
// process *process.Process
memory uint64
}
@@ -33,12 +42,22 @@ func NewManager() *Manager {
return manager
}
func (m *Manager) Join(c tracker) {
m.connections.Store(c.ID(), c)
func (m *Manager) Join(c Tracker) {
m.connections.Store(c.Metadata().ID, c)
}
func (m *Manager) Leave(c tracker) {
m.connections.Delete(c.ID())
func (m *Manager) Leave(c Tracker) {
metadata := c.Metadata()
_, loaded := m.connections.LoadAndDelete(metadata.ID)
if loaded {
metadata.ClosedAt = time.Now()
m.closedConnectionsAccess.Lock()
defer m.closedConnectionsAccess.Unlock()
if m.closedConnections.Len() >= 1000 {
m.closedConnections.PopFront()
}
m.closedConnections.PushBack(metadata)
}
}
func (m *Manager) PushUploaded(size int64) {
@@ -59,14 +78,39 @@ func (m *Manager) Total() (up int64, down int64) {
return m.uploadTotal.Load(), m.downloadTotal.Load()
}
func (m *Manager) Connections() int {
func (m *Manager) ConnectionsLen() int {
return m.connections.Len()
}
func (m *Manager) Connections() []TrackerMetadata {
var connections []TrackerMetadata
m.connections.Range(func(_ uuid.UUID, value Tracker) bool {
connections = append(connections, value.Metadata())
return true
})
return connections
}
func (m *Manager) ClosedConnections() []TrackerMetadata {
m.closedConnectionsAccess.Lock()
defer m.closedConnectionsAccess.Unlock()
return m.closedConnections.Array()
}
func (m *Manager) Connection(id uuid.UUID) Tracker {
connection, loaded := m.connections.Load(id)
if !loaded {
return nil
}
return connection
}
func (m *Manager) Snapshot() *Snapshot {
var connections []tracker
m.connections.Range(func(_ string, value tracker) bool {
connections = append(connections, value)
var connections []Tracker
m.connections.Range(func(_ uuid.UUID, value Tracker) bool {
if value.Metadata().OutboundType != C.TypeDNS {
connections = append(connections, value)
}
return true
})
@@ -75,10 +119,10 @@ func (m *Manager) Snapshot() *Snapshot {
m.memory = memStats.StackInuse + memStats.HeapInuse + memStats.HeapIdle - memStats.HeapReleased
return &Snapshot{
UploadTotal: m.uploadTotal.Load(),
DownloadTotal: m.downloadTotal.Load(),
Connections: connections,
Memory: m.memory,
Upload: m.uploadTotal.Load(),
Download: m.downloadTotal.Load(),
Connections: connections,
Memory: m.memory,
}
}
@@ -114,8 +158,17 @@ func (m *Manager) Close() error {
}
type Snapshot struct {
DownloadTotal int64 `json:"downloadTotal"`
UploadTotal int64 `json:"uploadTotal"`
Connections []tracker `json:"connections"`
Memory uint64 `json:"memory"`
Download int64
Upload int64
Connections []Tracker
Memory uint64
}
func (s *Snapshot) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"downloadTotal": s.Download,
"uploadTotal": s.Upload,
"connections": common.Map(s.Connections, func(t Tracker) TrackerMetadata { return t.Metadata() }),
"memory": s.Memory,
})
}
+140 -109
View File
@@ -2,97 +2,135 @@ package trafficontrol
import (
"net"
"net/netip"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/bufio"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json"
N "github.com/sagernet/sing/common/network"
"github.com/gofrs/uuid/v5"
)
type Metadata struct {
NetWork string `json:"network"`
Type string `json:"type"`
SrcIP netip.Addr `json:"sourceIP"`
DstIP netip.Addr `json:"destinationIP"`
SrcPort string `json:"sourcePort"`
DstPort string `json:"destinationPort"`
Host string `json:"host"`
DNSMode string `json:"dnsMode"`
ProcessPath string `json:"processPath"`
type TrackerMetadata struct {
ID uuid.UUID
Metadata adapter.InboundContext
CreatedAt time.Time
ClosedAt time.Time
Upload *atomic.Int64
Download *atomic.Int64
Chain []string
Rule adapter.Rule
Outbound string
OutboundType string
}
type tracker interface {
ID() string
Close() error
Leave()
}
type trackerInfo struct {
UUID uuid.UUID `json:"id"`
Metadata Metadata `json:"metadata"`
UploadTotal *atomic.Int64 `json:"upload"`
DownloadTotal *atomic.Int64 `json:"download"`
Start time.Time `json:"start"`
Chain []string `json:"chains"`
Rule string `json:"rule"`
RulePayload string `json:"rulePayload"`
}
func (t trackerInfo) MarshalJSON() ([]byte, error) {
func (t TrackerMetadata) MarshalJSON() ([]byte, error) {
var inbound string
if t.Metadata.Inbound != "" {
inbound = t.Metadata.InboundType + "/" + t.Metadata.Inbound
} else {
inbound = t.Metadata.InboundType
}
var domain string
if t.Metadata.Domain != "" {
domain = t.Metadata.Domain
} else {
domain = t.Metadata.Destination.Fqdn
}
var processPath string
if t.Metadata.ProcessInfo != nil {
if t.Metadata.ProcessInfo.ProcessPath != "" {
processPath = t.Metadata.ProcessInfo.ProcessPath
} else if t.Metadata.ProcessInfo.PackageName != "" {
processPath = t.Metadata.ProcessInfo.PackageName
}
if processPath == "" {
if t.Metadata.ProcessInfo.UserId != -1 {
processPath = F.ToString(t.Metadata.ProcessInfo.UserId)
}
} else if t.Metadata.ProcessInfo.User != "" {
processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.User, ")")
} else if t.Metadata.ProcessInfo.UserId != -1 {
processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.UserId, ")")
}
}
var rule string
if t.Rule != nil {
rule = F.ToString(t.Rule, " => ", t.Rule.Outbound())
} else {
rule = "final"
}
return json.Marshal(map[string]any{
"id": t.UUID.String(),
"metadata": t.Metadata,
"upload": t.UploadTotal.Load(),
"download": t.DownloadTotal.Load(),
"start": t.Start,
"id": t.ID,
"metadata": map[string]any{
"network": t.Metadata.Network,
"type": inbound,
"sourceIP": t.Metadata.Source.Addr,
"destinationIP": t.Metadata.Destination.Addr,
"sourcePort": F.ToString(t.Metadata.Source.Port),
"destinationPort": F.ToString(t.Metadata.Destination.Port),
"host": domain,
"dnsMode": "normal",
"processPath": processPath,
},
"upload": t.Upload.Load(),
"download": t.Download.Load(),
"start": t.CreatedAt,
"chains": t.Chain,
"rule": t.Rule,
"rulePayload": t.RulePayload,
"rule": rule,
"rulePayload": "",
})
}
type tcpTracker struct {
N.ExtendedConn `json:"-"`
*trackerInfo
manager *Manager
type Tracker interface {
adapter.Tracker
Metadata() TrackerMetadata
Close() error
}
func (tt *tcpTracker) ID() string {
return tt.UUID.String()
type TCPConn struct {
N.ExtendedConn
metadata TrackerMetadata
manager *Manager
}
func (tt *tcpTracker) Close() error {
func (tt *TCPConn) Metadata() TrackerMetadata {
return tt.metadata
}
func (tt *TCPConn) Close() error {
tt.manager.Leave(tt)
return tt.ExtendedConn.Close()
}
func (tt *tcpTracker) Leave() {
func (tt *TCPConn) Leave() {
tt.manager.Leave(tt)
}
func (tt *tcpTracker) Upstream() any {
func (tt *TCPConn) Upstream() any {
return tt.ExtendedConn
}
func (tt *tcpTracker) ReaderReplaceable() bool {
func (tt *TCPConn) ReaderReplaceable() bool {
return true
}
func (tt *tcpTracker) WriterReplaceable() bool {
func (tt *TCPConn) WriterReplaceable() bool {
return true
}
func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router adapter.Router, rule adapter.Rule) *tcpTracker {
uuid, _ := uuid.NewV4()
var chain []string
var next string
func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundContext, router adapter.Router, rule adapter.Rule) *TCPConn {
id, _ := uuid.NewV4()
var (
chain []string
next string
outbound string
outboundType string
)
if rule == nil {
if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil {
next = defaultOutbound.Tag()
@@ -106,17 +144,17 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad
if !loaded {
break
}
outbound = detour.Tag()
outboundType = detour.Type()
group, isGroup := detour.(adapter.OutboundGroup)
if !isGroup {
break
}
next = group.Now()
}
upload := new(atomic.Int64)
download := new(atomic.Int64)
t := &tcpTracker{
tracker := &TCPConn{
ExtendedConn: bufio.NewCounterConn(conn, []N.CountFunc{func(n int64) {
upload.Add(n)
manager.PushUploaded(n)
@@ -124,64 +162,62 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad
download.Add(n)
manager.PushDownloaded(n)
}}),
manager: manager,
trackerInfo: &trackerInfo{
UUID: uuid,
Start: time.Now(),
Metadata: metadata,
Chain: common.Reverse(chain),
Rule: "",
UploadTotal: upload,
DownloadTotal: download,
metadata: TrackerMetadata{
ID: id,
Metadata: metadata,
CreatedAt: time.Now(),
Upload: upload,
Download: download,
Chain: common.Reverse(chain),
Rule: rule,
Outbound: outbound,
OutboundType: outboundType,
},
manager: manager,
}
if rule != nil {
t.trackerInfo.Rule = rule.String() + " => " + rule.Outbound()
} else {
t.trackerInfo.Rule = "final"
}
manager.Join(t)
return t
manager.Join(tracker)
return tracker
}
type udpTracker struct {
type UDPConn struct {
N.PacketConn `json:"-"`
*trackerInfo
manager *Manager
metadata TrackerMetadata
manager *Manager
}
func (ut *udpTracker) ID() string {
return ut.UUID.String()
func (ut *UDPConn) Metadata() TrackerMetadata {
return ut.metadata
}
func (ut *udpTracker) Close() error {
func (ut *UDPConn) Close() error {
ut.manager.Leave(ut)
return ut.PacketConn.Close()
}
func (ut *udpTracker) Leave() {
func (ut *UDPConn) Leave() {
ut.manager.Leave(ut)
}
func (ut *udpTracker) Upstream() any {
func (ut *UDPConn) Upstream() any {
return ut.PacketConn
}
func (ut *udpTracker) ReaderReplaceable() bool {
func (ut *UDPConn) ReaderReplaceable() bool {
return true
}
func (ut *udpTracker) WriterReplaceable() bool {
func (ut *UDPConn) WriterReplaceable() bool {
return true
}
func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, router adapter.Router, rule adapter.Rule) *udpTracker {
uuid, _ := uuid.NewV4()
var chain []string
var next string
func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.InboundContext, router adapter.Router, rule adapter.Rule) *UDPConn {
id, _ := uuid.NewV4()
var (
chain []string
next string
outbound string
outboundType string
)
if rule == nil {
if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil {
next = defaultOutbound.Tag()
@@ -195,17 +231,17 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route
if !loaded {
break
}
outbound = detour.Tag()
outboundType = detour.Type()
group, isGroup := detour.(adapter.OutboundGroup)
if !isGroup {
break
}
next = group.Now()
}
upload := new(atomic.Int64)
download := new(atomic.Int64)
ut := &udpTracker{
trackerConn := &UDPConn{
PacketConn: bufio.NewCounterPacketConn(conn, []N.CountFunc{func(n int64) {
upload.Add(n)
manager.PushUploaded(n)
@@ -213,24 +249,19 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route
download.Add(n)
manager.PushDownloaded(n)
}}),
manager: manager,
trackerInfo: &trackerInfo{
UUID: uuid,
Start: time.Now(),
Metadata: metadata,
Chain: common.Reverse(chain),
Rule: "",
UploadTotal: upload,
DownloadTotal: download,
metadata: TrackerMetadata{
ID: id,
Metadata: metadata,
CreatedAt: time.Now(),
Upload: upload,
Download: download,
Chain: common.Reverse(chain),
Rule: rule,
Outbound: outbound,
OutboundType: outboundType,
},
manager: manager,
}
if rule != nil {
ut.trackerInfo.Rule = rule.String() + " => " + rule.Outbound()
} else {
ut.trackerInfo.Rule = "final"
}
manager.Join(ut)
return ut
manager.Join(trackerConn)
return trackerConn
}
+2
View File
@@ -14,4 +14,6 @@ const (
CommandSetClashMode
CommandGetSystemProxyStatus
CommandSetSystemProxyEnabled
CommandConnections
CommandCloseConnection
)
+9 -9
View File
@@ -9,7 +9,7 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/experimental/clashapi"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/varbin"
)
func (c *CommandClient) SetClashMode(newMode string) error {
@@ -22,7 +22,7 @@ func (c *CommandClient) SetClashMode(newMode string) error {
if err != nil {
return err
}
err = rw.WriteVString(conn, newMode)
err = varbin.Write(conn, binary.BigEndian, newMode)
if err != nil {
return err
}
@@ -30,7 +30,7 @@ func (c *CommandClient) SetClashMode(newMode string) error {
}
func (s *CommandServer) handleSetClashMode(conn net.Conn) error {
newMode, err := rw.ReadVString(conn)
newMode, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil {
return err
}
@@ -50,7 +50,7 @@ func (c *CommandClient) handleModeConn(conn net.Conn) {
defer conn.Close()
for {
newMode, err := rw.ReadVString(conn)
newMode, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil {
c.handler.Disconnected(err.Error())
return
@@ -80,7 +80,7 @@ func (s *CommandServer) handleModeConn(conn net.Conn) error {
for {
select {
case <-s.modeUpdate:
err = rw.WriteVString(conn, clashServer.Mode())
err = varbin.Write(conn, binary.BigEndian, clashServer.Mode())
if err != nil {
return err
}
@@ -101,12 +101,12 @@ func readClashModeList(reader io.Reader) (modeList []string, currentMode string,
}
modeList = make([]string, modeListLength)
for i := 0; i < int(modeListLength); i++ {
modeList[i], err = rw.ReadVString(reader)
modeList[i], err = varbin.ReadValue[string](reader, binary.BigEndian)
if err != nil {
return
}
}
currentMode, err = rw.ReadVString(reader)
currentMode, err = varbin.ReadValue[string](reader, binary.BigEndian)
return
}
@@ -118,12 +118,12 @@ func writeClashModeList(writer io.Writer, clashServer adapter.ClashServer) error
}
if len(modeList) > 0 {
for _, mode := range modeList {
err = rw.WriteVString(writer, mode)
err = varbin.Write(writer, binary.BigEndian, mode)
if err != nil {
return err
}
}
err = rw.WriteVString(writer, clashServer.Mode())
err = varbin.Write(writer, binary.BigEndian, clashServer.Mode())
if err != nil {
return err
}
+14 -2
View File
@@ -25,12 +25,13 @@ type CommandClientOptions struct {
type CommandClientHandler interface {
Connected()
Disconnected(message string)
ClearLog()
WriteLog(message string)
ClearLogs()
WriteLogs(messageList StringIterator)
WriteStatus(message *StatusMessage)
WriteGroups(message OutboundGroupIterator)
InitializeClashMode(modeList StringIterator, currentMode string)
UpdateClashMode(newMode string)
WriteConnections(message *Connections)
}
func NewStandaloneCommandClient() *CommandClient {
@@ -83,6 +84,10 @@ func (c *CommandClient) Connect() error {
}
switch c.options.Command {
case CommandLog:
err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval)
if err != nil {
return E.Cause(err, "write interval")
}
c.handler.Connected()
go c.handleLogConn(conn)
case CommandStatus:
@@ -116,6 +121,13 @@ func (c *CommandClient) Connect() error {
return nil
}
go c.handleModeConn(conn)
case CommandConnections:
err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval)
if err != nil {
return E.Cause(err, "write interval")
}
c.handler.Connected()
go c.handleConnectionsConn(conn)
}
return nil
}
@@ -0,0 +1,54 @@
package libbox
import (
"bufio"
"net"
"github.com/sagernet/sing-box/experimental/clashapi"
"github.com/sagernet/sing/common/binary"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin"
"github.com/gofrs/uuid/v5"
)
func (c *CommandClient) CloseConnection(connId string) error {
conn, err := c.directConnect()
if err != nil {
return err
}
defer conn.Close()
writer := bufio.NewWriter(conn)
err = varbin.Write(writer, binary.BigEndian, connId)
if err != nil {
return err
}
err = writer.Flush()
if err != nil {
return err
}
return readError(conn)
}
func (s *CommandServer) handleCloseConnection(conn net.Conn) error {
reader := bufio.NewReader(conn)
var connId string
err := varbin.Read(reader, binary.BigEndian, &connId)
if err != nil {
return E.Cause(err, "read connection id")
}
service := s.service
if service == nil {
return writeError(conn, E.New("service not ready"))
}
clashServer := service.instance.Router().ClashServer()
if clashServer == nil {
return writeError(conn, E.New("Clash API disabled"))
}
targetConn := clashServer.(*clashapi.Server).TrafficManager().Connection(uuid.FromStringOrNil(connId))
if targetConn == nil {
return writeError(conn, E.New("connection already closed"))
}
targetConn.Close()
return writeError(conn, nil)
}
+272
View File
@@ -0,0 +1,272 @@
package libbox
import (
"bufio"
"net"
"slices"
"strings"
"time"
"github.com/sagernet/sing-box/experimental/clashapi"
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
"github.com/sagernet/sing/common/binary"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/varbin"
"github.com/gofrs/uuid/v5"
)
func (c *CommandClient) handleConnectionsConn(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
var (
rawConnections []Connection
connections Connections
)
for {
err := varbin.Read(reader, binary.BigEndian, &rawConnections)
if err != nil {
c.handler.Disconnected(err.Error())
return
}
connections.input = rawConnections
c.handler.WriteConnections(&connections)
}
}
func (s *CommandServer) handleConnectionsConn(conn net.Conn) error {
var interval int64
err := binary.Read(conn, binary.BigEndian, &interval)
if err != nil {
return E.Cause(err, "read interval")
}
ticker := time.NewTicker(time.Duration(interval))
defer ticker.Stop()
ctx := connKeepAlive(conn)
var trafficManager *trafficontrol.Manager
for {
service := s.service
if service != nil {
clashServer := service.instance.Router().ClashServer()
if clashServer == nil {
return E.New("Clash API disabled")
}
trafficManager = clashServer.(*clashapi.Server).TrafficManager()
break
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
var (
connections = make(map[uuid.UUID]*Connection)
outConnections []Connection
)
writer := bufio.NewWriter(conn)
for {
outConnections = outConnections[:0]
for _, connection := range trafficManager.Connections() {
outConnections = append(outConnections, newConnection(connections, connection, false))
}
for _, connection := range trafficManager.ClosedConnections() {
outConnections = append(outConnections, newConnection(connections, connection, true))
}
err = varbin.Write(writer, binary.BigEndian, outConnections)
if err != nil {
return err
}
err = writer.Flush()
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
}
const (
ConnectionStateAll = iota
ConnectionStateActive
ConnectionStateClosed
)
type Connections struct {
input []Connection
filtered []Connection
}
func (c *Connections) FilterState(state int32) {
c.filtered = c.filtered[:0]
switch state {
case ConnectionStateAll:
c.filtered = append(c.filtered, c.input...)
case ConnectionStateActive:
for _, connection := range c.input {
if connection.ClosedAt == 0 {
c.filtered = append(c.filtered, connection)
}
}
case ConnectionStateClosed:
for _, connection := range c.input {
if connection.ClosedAt != 0 {
c.filtered = append(c.filtered, connection)
}
}
}
}
func (c *Connections) SortByDate() {
slices.SortStableFunc(c.filtered, func(x, y Connection) int {
if x.CreatedAt < y.CreatedAt {
return 1
} else if x.CreatedAt > y.CreatedAt {
return -1
} else {
return strings.Compare(y.ID, x.ID)
}
})
}
func (c *Connections) SortByTraffic() {
slices.SortStableFunc(c.filtered, func(x, y Connection) int {
xTraffic := x.Uplink + x.Downlink
yTraffic := y.Uplink + y.Downlink
if xTraffic < yTraffic {
return 1
} else if xTraffic > yTraffic {
return -1
} else {
return strings.Compare(y.ID, x.ID)
}
})
}
func (c *Connections) SortByTrafficTotal() {
slices.SortStableFunc(c.filtered, func(x, y Connection) int {
xTraffic := x.UplinkTotal + x.DownlinkTotal
yTraffic := y.UplinkTotal + y.DownlinkTotal
if xTraffic < yTraffic {
return 1
} else if xTraffic > yTraffic {
return -1
} else {
return strings.Compare(y.ID, x.ID)
}
})
}
func (c *Connections) Iterator() ConnectionIterator {
return newPtrIterator(c.filtered)
}
type Connection struct {
ID string
Inbound string
InboundType string
IPVersion int32
Network string
Source string
Destination string
Domain string
Protocol string
User string
FromOutbound string
CreatedAt int64
ClosedAt int64
Uplink int64
Downlink int64
UplinkTotal int64
DownlinkTotal int64
Rule string
Outbound string
OutboundType string
ChainList []string
}
func (c *Connection) Chain() StringIterator {
return newIterator(c.ChainList)
}
func (c *Connection) DisplayDestination() string {
destination := M.ParseSocksaddr(c.Destination)
if destination.IsIP() && c.Domain != "" {
destination = M.Socksaddr{
Fqdn: c.Domain,
Port: destination.Port,
}
return destination.String()
}
return c.Destination
}
type ConnectionIterator interface {
Next() *Connection
HasNext() bool
}
func newConnection(connections map[uuid.UUID]*Connection, metadata trafficontrol.TrackerMetadata, isClosed bool) Connection {
if oldConnection, loaded := connections[metadata.ID]; loaded {
if isClosed {
if oldConnection.ClosedAt == 0 {
oldConnection.Uplink = 0
oldConnection.Downlink = 0
oldConnection.ClosedAt = metadata.ClosedAt.UnixMilli()
}
return *oldConnection
}
lastUplink := oldConnection.UplinkTotal
lastDownlink := oldConnection.DownlinkTotal
uplinkTotal := metadata.Upload.Load()
downlinkTotal := metadata.Download.Load()
oldConnection.Uplink = uplinkTotal - lastUplink
oldConnection.Downlink = downlinkTotal - lastDownlink
oldConnection.UplinkTotal = uplinkTotal
oldConnection.DownlinkTotal = downlinkTotal
return *oldConnection
}
var rule string
if metadata.Rule != nil {
rule = metadata.Rule.String()
}
uplinkTotal := metadata.Upload.Load()
downlinkTotal := metadata.Download.Load()
uplink := uplinkTotal
downlink := downlinkTotal
var closedAt int64
if !metadata.ClosedAt.IsZero() {
closedAt = metadata.ClosedAt.UnixMilli()
uplink = 0
downlink = 0
}
connection := Connection{
ID: metadata.ID.String(),
Inbound: metadata.Metadata.Inbound,
InboundType: metadata.Metadata.InboundType,
IPVersion: int32(metadata.Metadata.IPVersion),
Network: metadata.Metadata.Network,
Source: metadata.Metadata.Source.String(),
Destination: metadata.Metadata.Destination.String(),
Domain: metadata.Metadata.Domain,
Protocol: metadata.Metadata.Protocol,
User: metadata.Metadata.User,
FromOutbound: metadata.Metadata.Outbound,
CreatedAt: metadata.CreatedAt.UnixMilli(),
ClosedAt: closedAt,
Uplink: uplink,
Downlink: downlink,
UplinkTotal: uplinkTotal,
DownlinkTotal: downlinkTotal,
Rule: rule,
Outbound: metadata.Outbound,
OutboundType: metadata.OutboundType,
ChainList: metadata.Chain,
}
connections[metadata.ID] = &connection
return connection
}
+45 -151
View File
@@ -1,6 +1,7 @@
package libbox
import (
"bufio"
"encoding/binary"
"io"
"net"
@@ -10,40 +11,10 @@ import (
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-box/outbound"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/varbin"
"github.com/sagernet/sing/service"
)
type OutboundGroup struct {
Tag string
Type string
Selectable bool
Selected string
IsExpand bool
items []*OutboundGroupItem
}
func (g *OutboundGroup) GetItems() OutboundGroupItemIterator {
return newIterator(g.items)
}
type OutboundGroupIterator interface {
Next() *OutboundGroup
HasNext() bool
}
type OutboundGroupItem struct {
Tag string
Type string
URLTestTime int64
URLTestDelay int32
}
type OutboundGroupItemIterator interface {
Next() *OutboundGroupItem
HasNext() bool
}
func (c *CommandClient) handleGroupConn(conn net.Conn) {
defer conn.Close()
@@ -66,19 +37,24 @@ func (s *CommandServer) handleGroupConn(conn net.Conn) error {
ticker := time.NewTicker(time.Duration(interval))
defer ticker.Stop()
ctx := connKeepAlive(conn)
writer := bufio.NewWriter(conn)
for {
service := s.service
if service != nil {
err := writeGroups(conn, service)
err = writeGroups(writer, service)
if err != nil {
return err
}
} else {
err := binary.Write(conn, binary.BigEndian, uint16(0))
err = binary.Write(writer, binary.BigEndian, uint16(0))
if err != nil {
return err
}
}
err = writer.Flush()
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
@@ -92,74 +68,41 @@ func (s *CommandServer) handleGroupConn(conn net.Conn) error {
}
}
type OutboundGroup struct {
Tag string
Type string
Selectable bool
Selected string
IsExpand bool
ItemList []*OutboundGroupItem
}
func (g *OutboundGroup) GetItems() OutboundGroupItemIterator {
return newIterator(g.ItemList)
}
type OutboundGroupIterator interface {
Next() *OutboundGroup
HasNext() bool
}
type OutboundGroupItem struct {
Tag string
Type string
URLTestTime int64
URLTestDelay int32
}
type OutboundGroupItemIterator interface {
Next() *OutboundGroupItem
HasNext() bool
}
func readGroups(reader io.Reader) (OutboundGroupIterator, error) {
var groupLength uint16
err := binary.Read(reader, binary.BigEndian, &groupLength)
groups, err := varbin.ReadValue[[]*OutboundGroup](reader, binary.BigEndian)
if err != nil {
return nil, err
}
groups := make([]*OutboundGroup, 0, groupLength)
for i := 0; i < int(groupLength); i++ {
var group OutboundGroup
group.Tag, err = rw.ReadVString(reader)
if err != nil {
return nil, err
}
group.Type, err = rw.ReadVString(reader)
if err != nil {
return nil, err
}
err = binary.Read(reader, binary.BigEndian, &group.Selectable)
if err != nil {
return nil, err
}
group.Selected, err = rw.ReadVString(reader)
if err != nil {
return nil, err
}
err = binary.Read(reader, binary.BigEndian, &group.IsExpand)
if err != nil {
return nil, err
}
var itemLength uint16
err = binary.Read(reader, binary.BigEndian, &itemLength)
if err != nil {
return nil, err
}
group.items = make([]*OutboundGroupItem, itemLength)
for j := 0; j < int(itemLength); j++ {
var item OutboundGroupItem
item.Tag, err = rw.ReadVString(reader)
if err != nil {
return nil, err
}
item.Type, err = rw.ReadVString(reader)
if err != nil {
return nil, err
}
err = binary.Read(reader, binary.BigEndian, &item.URLTestTime)
if err != nil {
return nil, err
}
err = binary.Read(reader, binary.BigEndian, &item.URLTestDelay)
if err != nil {
return nil, err
}
group.items[j] = &item
}
groups = append(groups, &group)
}
return newIterator(groups), nil
}
@@ -199,63 +142,14 @@ func writeGroups(writer io.Writer, boxService *BoxService) error {
item.URLTestTime = history.Time.Unix()
item.URLTestDelay = int32(history.Delay)
}
group.items = append(group.items, &item)
group.ItemList = append(group.ItemList, &item)
}
if len(group.items) < 2 {
if len(group.ItemList) < 2 {
continue
}
groups = append(groups, group)
}
err := binary.Write(writer, binary.BigEndian, uint16(len(groups)))
if err != nil {
return err
}
for _, group := range groups {
err = rw.WriteVString(writer, group.Tag)
if err != nil {
return err
}
err = rw.WriteVString(writer, group.Type)
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, group.Selectable)
if err != nil {
return err
}
err = rw.WriteVString(writer, group.Selected)
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, group.IsExpand)
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, uint16(len(group.items)))
if err != nil {
return err
}
for _, item := range group.items {
err = rw.WriteVString(writer, item.Tag)
if err != nil {
return err
}
err = rw.WriteVString(writer, item.Type)
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, item.URLTestTime)
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, item.URLTestDelay)
if err != nil {
return err
}
}
}
return nil
return varbin.Write(writer, binary.BigEndian, groups)
}
func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error {
@@ -268,7 +162,7 @@ func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error {
if err != nil {
return err
}
err = rw.WriteVString(conn, groupTag)
err = varbin.Write(conn, binary.BigEndian, groupTag)
if err != nil {
return err
}
@@ -280,7 +174,7 @@ func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error {
}
func (s *CommandServer) handleSetGroupExpand(conn net.Conn) error {
groupTag, err := rw.ReadVString(conn)
groupTag, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil {
return err
}
+81 -52
View File
@@ -1,12 +1,27 @@
package libbox
import (
"bufio"
"context"
"encoding/binary"
"io"
"net"
"time"
"github.com/sagernet/sing/common/binary"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin"
)
func (s *CommandServer) ResetLog() {
s.access.Lock()
defer s.access.Unlock()
s.savedLines.Init()
select {
case s.logReset <- struct{}{}:
default:
}
}
func (s *CommandServer) WriteMessage(message string) {
s.subscriber.Emit(message)
s.access.Lock()
@@ -17,43 +32,19 @@ func (s *CommandServer) WriteMessage(message string) {
s.access.Unlock()
}
func readLog(reader io.Reader) ([]byte, error) {
var messageLength uint16
err := binary.Read(reader, binary.BigEndian, &messageLength)
if err != nil {
return nil, err
}
if messageLength == 0 {
return nil, nil
}
data := make([]byte, messageLength)
_, err = io.ReadFull(reader, data)
if err != nil {
return nil, err
}
return data, nil
}
func writeLog(writer io.Writer, message []byte) error {
err := binary.Write(writer, binary.BigEndian, uint8(0))
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, uint16(len(message)))
if err != nil {
return err
}
if len(message) > 0 {
_, err = writer.Write(message)
}
return err
}
func writeClearLog(writer io.Writer) error {
return binary.Write(writer, binary.BigEndian, uint8(1))
}
func (s *CommandServer) handleLogConn(conn net.Conn) error {
var (
interval int64
timer *time.Timer
)
err := binary.Read(conn, binary.BigEndian, &interval)
if err != nil {
return E.Cause(err, "read interval")
}
timer = time.NewTimer(time.Duration(interval))
if !timer.Stop() {
<-timer.C
}
var savedLines []string
s.access.Lock()
savedLines = make([]string, 0, s.savedLines.Len())
@@ -66,52 +57,90 @@ func (s *CommandServer) handleLogConn(conn net.Conn) error {
return err
}
defer s.observer.UnSubscribe(subscription)
for _, line := range savedLines {
err = writeLog(conn, []byte(line))
writer := bufio.NewWriter(conn)
select {
case <-s.logReset:
err = writer.WriteByte(1)
if err != nil {
return err
}
err = writer.Flush()
if err != nil {
return err
}
default:
}
if len(savedLines) > 0 {
err = writer.WriteByte(0)
if err != nil {
return err
}
err = varbin.Write(writer, binary.BigEndian, savedLines)
if err != nil {
return err
}
}
ctx := connKeepAlive(conn)
var logLines []string
for {
err = writer.Flush()
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case message := <-subscription:
err = writeLog(conn, []byte(message))
if err != nil {
return err
}
case <-s.logReset:
err = writeClearLog(conn)
err = writer.WriteByte(1)
if err != nil {
return err
}
case <-done:
return nil
case logLine := <-subscription:
logLines = logLines[:0]
logLines = append(logLines, logLine)
timer.Reset(time.Duration(interval))
loopLogs:
for {
select {
case logLine = <-subscription:
logLines = append(logLines, logLine)
case <-timer.C:
break loopLogs
}
}
err = writer.WriteByte(0)
if err != nil {
return err
}
err = varbin.Write(writer, binary.BigEndian, logLines)
if err != nil {
return err
}
}
}
}
func (c *CommandClient) handleLogConn(conn net.Conn) {
reader := bufio.NewReader(conn)
for {
var messageType uint8
err := binary.Read(conn, binary.BigEndian, &messageType)
messageType, err := reader.ReadByte()
if err != nil {
c.handler.Disconnected(err.Error())
return
}
var message []byte
var messages []string
switch messageType {
case 0:
message, err = readLog(conn)
err = varbin.Read(reader, binary.BigEndian, &messages)
if err != nil {
c.handler.Disconnected(err.Error())
return
}
c.handler.WriteLog(string(message))
c.handler.WriteLogs(newIterator(messages))
case 1:
c.handler.ClearLog()
c.handler.ClearLogs()
}
}
}
@@ -120,7 +149,7 @@ func connKeepAlive(reader io.Reader) context.Context {
ctx, cancel := context.WithCancelCause(context.Background())
go func() {
for {
_, err := readLog(reader)
_, err := reader.Read(make([]byte, 1))
if err != nil {
cancel(err)
return
+5 -5
View File
@@ -5,7 +5,7 @@ import (
"net"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/varbin"
)
func (c *CommandClient) ServiceReload() error {
@@ -24,7 +24,7 @@ func (c *CommandClient) ServiceReload() error {
return err
}
if hasError {
errorMessage, err := rw.ReadVString(conn)
errorMessage, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil {
return err
}
@@ -40,7 +40,7 @@ func (s *CommandServer) handleServiceReload(conn net.Conn) error {
return err
}
if rErr != nil {
return rw.WriteVString(conn, rErr.Error())
return varbin.Write(conn, binary.BigEndian, rErr.Error())
}
return nil
}
@@ -61,7 +61,7 @@ func (c *CommandClient) ServiceClose() error {
return nil
}
if hasError {
errorMessage, err := rw.ReadVString(conn)
errorMessage, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil {
return nil
}
@@ -78,7 +78,7 @@ func (s *CommandServer) handleServiceClose(conn net.Conn) error {
return err
}
if rErr != nil {
return rw.WriteVString(conn, rErr.Error())
return varbin.Write(conn, binary.BigEndian, rErr.Error())
}
return nil
}
+5 -5
View File
@@ -6,7 +6,7 @@ import (
"github.com/sagernet/sing-box/outbound"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/varbin"
)
func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) error {
@@ -19,11 +19,11 @@ func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) erro
if err != nil {
return err
}
err = rw.WriteVString(conn, groupTag)
err = varbin.Write(conn, binary.BigEndian, groupTag)
if err != nil {
return err
}
err = rw.WriteVString(conn, outboundTag)
err = varbin.Write(conn, binary.BigEndian, outboundTag)
if err != nil {
return err
}
@@ -31,11 +31,11 @@ func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) erro
}
func (s *CommandServer) handleSelectOutbound(conn net.Conn) error {
groupTag, err := rw.ReadVString(conn)
groupTag, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil {
return err
}
outboundTag, err := rw.ReadVString(conn)
outboundTag, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil {
return err
}
+6 -8
View File
@@ -33,6 +33,8 @@ type CommandServer struct {
urlTestUpdate chan struct{}
modeUpdate chan struct{}
logReset chan struct{}
closedConnections []Connection
}
type CommandServerHandler interface {
@@ -64,14 +66,6 @@ func (s *CommandServer) SetService(newService *BoxService) {
s.notifyURLTestUpdate()
}
func (s *CommandServer) ResetLog() {
s.savedLines.Init()
select {
case s.logReset <- struct{}{}:
default:
}
}
func (s *CommandServer) notifyURLTestUpdate() {
select {
case s.urlTestUpdate <- struct{}{}:
@@ -176,6 +170,10 @@ func (s *CommandServer) handleConnection(conn net.Conn) error {
return s.handleGetSystemProxyStatus(conn)
case CommandSetSystemProxyEnabled:
return s.handleSetSystemProxyEnabled(conn)
case CommandConnections:
return s.handleConnectionsConn(conn)
case CommandCloseConnection:
return s.handleCloseConnection(conn)
default:
return E.New("unknown command: ", command)
}
+3 -3
View File
@@ -5,7 +5,7 @@ import (
"io"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/varbin"
)
func readError(reader io.Reader) error {
@@ -15,7 +15,7 @@ func readError(reader io.Reader) error {
return err
}
if hasError {
errorMessage, err := rw.ReadVString(reader)
errorMessage, err := varbin.ReadValue[string](reader, binary.BigEndian)
if err != nil {
return err
}
@@ -30,7 +30,7 @@ func writeError(writer io.Writer, wErr error) error {
return err
}
if wErr != nil {
err = rw.WriteVString(writer, wErr.Error())
err = varbin.Write(writer, binary.BigEndian, wErr.Error())
if err != nil {
return err
}
+1 -1
View File
@@ -36,7 +36,7 @@ func (s *CommandServer) readStatus() StatusMessage {
trafficManager := clashServer.(*clashapi.Server).TrafficManager()
message.Uplink, message.Downlink = trafficManager.Now()
message.UplinkTotal, message.DownlinkTotal = trafficManager.Total()
message.ConnectionsIn = int32(trafficManager.Connections())
message.ConnectionsIn = int32(trafficManager.ConnectionsLen())
}
}
+3 -3
View File
@@ -11,7 +11,7 @@ import (
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/batch"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/varbin"
"github.com/sagernet/sing/service"
)
@@ -25,7 +25,7 @@ func (c *CommandClient) URLTest(groupTag string) error {
if err != nil {
return err
}
err = rw.WriteVString(conn, groupTag)
err = varbin.Write(conn, binary.BigEndian, groupTag)
if err != nil {
return err
}
@@ -33,7 +33,7 @@ func (c *CommandClient) URLTest(groupTag string) error {
}
func (s *CommandServer) handleURLTest(conn net.Conn) error {
groupTag, err := rw.ReadVString(conn)
groupTag, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil {
return err
}
+2 -14
View File
@@ -69,8 +69,7 @@ func (p *platformLocalDNSTransport) Exchange(ctx context.Context, message *mDNS.
context: ctx,
}
var responseMessage *mDNS.Msg
var group task.Group
group.Append0(func(ctx context.Context) error {
return responseMessage, task.Run(ctx, func() error {
err = p.iif.Exchange(response, messageBytes)
if err != nil {
return err
@@ -81,11 +80,6 @@ func (p *platformLocalDNSTransport) Exchange(ctx context.Context, message *mDNS.
responseMessage = &response.message
return nil
})
err = group.Run(ctx)
if err != nil {
return nil, err
}
return responseMessage, nil
}
func (p *platformLocalDNSTransport) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) {
@@ -102,8 +96,7 @@ func (p *platformLocalDNSTransport) Lookup(ctx context.Context, domain string, s
context: ctx,
}
var responseAddr []netip.Addr
var group task.Group
group.Append0(func(ctx context.Context) error {
return responseAddr, task.Run(ctx, func() error {
err := p.iif.Lookup(response, network, domain)
if err != nil {
return err
@@ -128,11 +121,6 @@ func (p *platformLocalDNSTransport) Lookup(ctx context.Context, domain string, s
}*/
return nil
})
err := group.Run(ctx)
if err != nil {
return nil, err
}
return responseAddr, nil
}
type Func interface {
+14 -5
View File
@@ -3,8 +3,9 @@ package libbox
import "github.com/sagernet/sing/common"
type StringIterator interface {
Next() string
Len() int32
HasNext() bool
Next() string
}
var _ StringIterator = (*iterator[string])(nil)
@@ -17,6 +18,18 @@ func newIterator[T any](values []T) *iterator[T] {
return &iterator[T]{values}
}
func newPtrIterator[T any](values []T) *iterator[*T] {
return &iterator[*T]{common.Map(values, func(value T) *T { return &value })}
}
func (i *iterator[T]) Len() int32 {
return int32(len(i.values))
}
func (i *iterator[T]) HasNext() bool {
return len(i.values) > 0
}
func (i *iterator[T]) Next() T {
if len(i.values) == 0 {
return common.DefaultValue[T]()
@@ -26,10 +39,6 @@ func (i *iterator[T]) Next() T {
return nextValue
}
func (i *iterator[T]) HasNext() bool {
return len(i.values) > 0
}
type abstractIterator[T any] interface {
Next() T
HasNext() bool
+23 -20
View File
@@ -1,13 +1,13 @@
package libbox
import (
"bufio"
"bytes"
"compress/gzip"
"encoding/binary"
"io"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/varbin"
)
func EncodeChunkedMessage(data []byte) []byte {
@@ -35,13 +35,13 @@ type ErrorMessage struct {
func (e *ErrorMessage) Encode() []byte {
var buffer bytes.Buffer
buffer.WriteByte(MessageTypeError)
rw.WriteVString(&buffer, e.Message)
varbin.Write(&buffer, binary.BigEndian, e.Message)
return buffer.Bytes()
}
func DecodeErrorMessage(data []byte) (*ErrorMessage, error) {
reader := bytes.NewReader(data)
messageType, err := rw.ReadByte(reader)
messageType, err := reader.ReadByte()
if err != nil {
return nil, err
}
@@ -49,7 +49,7 @@ func DecodeErrorMessage(data []byte) (*ErrorMessage, error) {
return nil, E.New("invalid message")
}
var message ErrorMessage
message.Message, err = rw.ReadVString(reader)
message.Message, err = varbin.ReadValue[string](reader, binary.BigEndian)
if err != nil {
return nil, err
}
@@ -87,7 +87,7 @@ func (e *ProfileEncoder) Encode() []byte {
binary.Write(&buffer, binary.BigEndian, uint16(len(e.profiles)))
for _, preview := range e.profiles {
binary.Write(&buffer, binary.BigEndian, preview.ProfileID)
rw.WriteVString(&buffer, preview.Name)
varbin.Write(&buffer, binary.BigEndian, preview.Name)
binary.Write(&buffer, binary.BigEndian, preview.Type)
}
return buffer.Bytes()
@@ -117,7 +117,7 @@ func (d *ProfileDecoder) Decode(data []byte) error {
if err != nil {
return err
}
profile.Name, err = rw.ReadVString(reader)
profile.Name, err = varbin.ReadValue[string](reader, binary.BigEndian)
if err != nil {
return err
}
@@ -147,7 +147,7 @@ func (r *ProfileContentRequest) Encode() []byte {
func DecodeProfileContentRequest(data []byte) (*ProfileContentRequest, error) {
reader := bytes.NewReader(data)
messageType, err := rw.ReadByte(reader)
messageType, err := reader.ReadByte()
if err != nil {
return nil, err
}
@@ -176,12 +176,13 @@ func (c *ProfileContent) Encode() []byte {
buffer := new(bytes.Buffer)
buffer.WriteByte(MessageTypeProfileContent)
buffer.WriteByte(1)
writer := gzip.NewWriter(buffer)
rw.WriteVString(writer, c.Name)
gWriter := gzip.NewWriter(buffer)
writer := bufio.NewWriter(gWriter)
varbin.Write(writer, binary.BigEndian, c.Name)
binary.Write(writer, binary.BigEndian, c.Type)
rw.WriteVString(writer, c.Config)
varbin.Write(writer, binary.BigEndian, c.Config)
if c.Type != ProfileTypeLocal {
rw.WriteVString(writer, c.RemotePath)
varbin.Write(writer, binary.BigEndian, c.RemotePath)
}
if c.Type == ProfileTypeRemote {
binary.Write(writer, binary.BigEndian, c.AutoUpdate)
@@ -189,29 +190,31 @@ func (c *ProfileContent) Encode() []byte {
binary.Write(writer, binary.BigEndian, c.LastUpdated)
}
writer.Flush()
writer.Close()
gWriter.Flush()
gWriter.Close()
return buffer.Bytes()
}
func DecodeProfileContent(data []byte) (*ProfileContent, error) {
var reader io.Reader = bytes.NewReader(data)
messageType, err := rw.ReadByte(reader)
reader := bytes.NewReader(data)
messageType, err := reader.ReadByte()
if err != nil {
return nil, err
}
if messageType != MessageTypeProfileContent {
return nil, E.New("invalid message")
}
version, err := rw.ReadByte(reader)
version, err := reader.ReadByte()
if err != nil {
return nil, err
}
reader, err = gzip.NewReader(reader)
gReader, err := gzip.NewReader(reader)
if err != nil {
return nil, E.Cause(err, "unsupported profile")
}
bReader := varbin.StubReader(gReader)
var content ProfileContent
content.Name, err = rw.ReadVString(reader)
content.Name, err = varbin.ReadValue[string](bReader, binary.BigEndian)
if err != nil {
return nil, err
}
@@ -219,12 +222,12 @@ func DecodeProfileContent(data []byte) (*ProfileContent, error) {
if err != nil {
return nil, err
}
content.Config, err = rw.ReadVString(reader)
content.Config, err = varbin.ReadValue[string](bReader, binary.BigEndian)
if err != nil {
return nil, err
}
if content.Type != ProfileTypeLocal {
content.RemotePath, err = rw.ReadVString(reader)
content.RemotePath, err = varbin.ReadValue[string](bReader, binary.BigEndian)
if err != nil {
return nil, err
}

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