Compare commits

..

4 Commits

Author SHA1 Message Date
世界
0bccaedc3c Bump version 2024-06-08 21:43:04 +08:00
世界
be15ce5054 Fix parse UDP DNS server with addr:port address 2024-06-08 21:39:47 +08:00
世界
2d0326982b Stop passing device sleep events on Android and Apple platforms 2024-06-08 21:39:47 +08:00
世界
98c1dd17a0 Fix rule-set start order 2024-06-08 21:39:47 +08:00
402 changed files with 9010 additions and 15879 deletions

View File

@@ -22,22 +22,43 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.23 go-version: ^1.22
continue-on-error: true
- name: Run Test - name: Run Test
run: | run: |
go test -v ./... 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: build_go120:
name: Debug build (Go 1.20) name: Debug build (Go 1.20)
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
@@ -57,7 +78,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
@@ -72,26 +93,6 @@ jobs:
key: go121-${{ hashFiles('**/go.sum') }} key: go121-${{ hashFiles('**/go.sum') }}
- name: Run Test - name: Run Test
run: make ci_build run: make ci_build
build_go122:
name: Debug build (Go 1.22)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.22
- name: Cache go module
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
key: go122-${{ hashFiles('**/go.sum') }}
- name: Run Test
run: make ci_build
cross: cross:
strategy: strategy:
matrix: matrix:
@@ -207,7 +208,7 @@ jobs:
TAGS: with_clash_api,with_quic TAGS: with_clash_api,with_quic
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go

View File

@@ -1,93 +1,16 @@
name: Publish Docker Images name: Build Docker Images
on: on:
release: release:
types: types:
- published - released
workflow_dispatch: workflow_dispatch:
inputs: inputs:
tag: tag:
description: "The tag version you want to build" description: "The tag version you want to build"
env:
REGISTRY_IMAGE: ghcr.io/sagernet/sing-box
jobs: jobs:
build: build:
runs-on: ubuntu-latest 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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # 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: steps:
- name: Get commit to build - name: Get commit to build
id: ref id: ref
@@ -106,28 +29,34 @@ jobs:
fi fi
echo "latest=$latest" echo "latest=$latest"
echo "latest=$latest" >> $GITHUB_OUTPUT echo "latest=$latest" >> $GITHUB_OUTPUT
- name: Download digests - name: Checkout
uses: actions/download-artifact@v4 uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4
with: with:
path: /tmp/digests ref: ${{ steps.ref.outputs.ref }}
pattern: digests-* - name: Setup Docker Buildx
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Setup QEMU for Docker Buildx
uses: docker/setup-qemu-action@v3
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Create manifest list and push - name: Docker metadata
working-directory: /tmp/digests id: metadata
run: | uses: docker/metadata-action@v5
docker buildx imagetools create \ with:
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}" \ images: ghcr.io/sagernet/sing-box
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \ - name: Build and release Docker images
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) uses: docker/build-push-action@v5
- name: Inspect image with:
run: | platforms: linux/386,linux/amd64,linux/arm64,linux/s390x
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }} context: .
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }} 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

View File

@@ -22,13 +22,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.23 go-version: ^1.22
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v6 uses: golangci/golangci-lint-action@v6
with: with:

View File

@@ -10,13 +10,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.23 go-version: ^1.22
- name: Extract signing key - name: Extract signing key
run: |- run: |-
mkdir -p $HOME/.gnupg mkdir -p $HOME/.gnupg

View File

@@ -12,5 +12,4 @@ jobs:
with: 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' 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-stale: 60
days-before-close: 5 days-before-close: 5
exempt-issue-labels: 'bug,enhancement'

2
.gitignore vendored
View File

@@ -14,5 +14,3 @@
/*.xcframework/ /*.xcframework/
.DS_Store .DS_Store
/config.d/ /config.d/
/venv/

View File

@@ -6,7 +6,14 @@ linters:
- gci - gci
- staticcheck - staticcheck
- paralleltest - paralleltest
- ineffassign
run:
skip-dirs:
- transport/simple-obfs
- transport/clashssr
- transport/cloudflaretls
- transport/shadowtls/tls
- transport/shadowtls/tls_go119
linters-settings: linters-settings:
gci: gci:
@@ -16,13 +23,4 @@ linters-settings:
- prefix(github.com/sagernet/) - prefix(github.com/sagernet/)
- default - default
staticcheck: staticcheck:
checks: go: '1.20'
- all
- -SA1003
run:
go: "1.23"
issues:
exclude-dirs:
- transport/simple-obfs

View File

@@ -26,7 +26,6 @@ builds:
- linux_arm_7 - linux_arm_7
- linux_s390x - linux_s390x
- linux_riscv64 - linux_riscv64
- linux_mips64le
mod_timestamp: '{{ .CommitTimestamp }}' mod_timestamp: '{{ .CommitTimestamp }}'
snapshot: snapshot:
name_template: "{{ .Version }}.{{ .ShortCommit }}" name_template: "{{ .Version }}.{{ .ShortCommit }}"
@@ -49,19 +48,10 @@ nfpms:
- src: release/config/config.json - src: release/config/config.json
dst: /etc/sing-box/config.json dst: /etc/sing-box/config.json
type: config type: config
- src: release/config/sing-box.service - src: release/config/sing-box.service
dst: /usr/lib/systemd/system/sing-box.service dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service - src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service dst: /usr/lib/systemd/system/sing-box@.service
- src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash
- src: release/completions/sing-box.fish
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
- src: release/completions/sing-box.zsh
dst: /usr/share/zsh/site-functions/_sing-box
- src: LICENSE - src: LICENSE
dst: /usr/share/licenses/sing-box/LICENSE dst: /usr/share/licenses/sing-box/LICENSE
deb: deb:

View File

@@ -1,4 +1,3 @@
version: 2
project_name: sing-box project_name: sing-box
builds: builds:
- &template - &template
@@ -8,9 +7,7 @@ builds:
- -v - -v
- -trimpath - -trimpath
ldflags: ldflags:
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} - -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
- -s
- -buildid=
tags: tags:
- with_gvisor - with_gvisor
- with_quic - with_quic
@@ -26,13 +23,13 @@ builds:
targets: targets:
- linux_386 - linux_386
- linux_amd64_v1 - linux_amd64_v1
- linux_amd64_v3
- linux_arm64 - linux_arm64
- linux_arm_6
- linux_arm_7 - linux_arm_7
- linux_s390x - linux_s390x
- linux_riscv64 - linux_riscv64
- linux_mips64le
- windows_amd64_v1 - windows_amd64_v1
- windows_amd64_v3
- windows_386 - windows_386
- windows_arm64 - windows_arm64
- darwin_amd64_v1 - darwin_amd64_v1
@@ -89,6 +86,8 @@ builds:
- android_arm64 - android_arm64
- android_386 - android_386
- android_amd64 - android_amd64
snapshot:
name_template: "{{ .Version }}.{{ .ShortCommit }}"
archives: archives:
- &template - &template
id: archive id: archive
@@ -102,7 +101,7 @@ archives:
wrap_in_directory: true wrap_in_directory: true
files: files:
- LICENSE - LICENSE
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}_{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}' name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
- id: archive-legacy - id: archive-legacy
<<: *template <<: *template
builds: builds:
@@ -111,7 +110,7 @@ archives:
nfpms: nfpms:
- id: package - id: package
package_name: sing-box package_name: sing-box
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}_{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}' file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
builds: builds:
- main - main
homepage: https://sing-box.sagernet.org/ homepage: https://sing-box.sagernet.org/
@@ -122,26 +121,15 @@ nfpms:
- deb - deb
- rpm - rpm
- archlinux - archlinux
# - apk
# - ipk
priority: extra priority: extra
contents: contents:
- src: release/config/config.json - src: release/config/config.json
dst: /etc/sing-box/config.json dst: /etc/sing-box/config.json
type: config type: config
- src: release/config/sing-box.service - src: release/config/sing-box.service
dst: /usr/lib/systemd/system/sing-box.service dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service - src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service dst: /usr/lib/systemd/system/sing-box@.service
- src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash
- src: release/completions/sing-box.fish
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
- src: release/completions/sing-box.zsh
dst: /usr/share/zsh/site-functions/_sing-box
- src: LICENSE - src: LICENSE
dst: /usr/share/licenses/sing-box/LICENSE dst: /usr/share/licenses/sing-box/LICENSE
deb: deb:
@@ -153,34 +141,13 @@ nfpms:
signature: signature:
key_file: "{{ .Env.NFPM_KEY_PATH }}" key_file: "{{ .Env.NFPM_KEY_PATH }}"
overrides: overrides:
apk: deb:
contents: conflicts:
- src: release/config/config.json - sing-box-beta
dst: /etc/sing-box/config.json rpm:
type: config conflicts:
- sing-box-beta
- src: release/config/sing-box.initd
dst: /etc/init.d/sing-box
- src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash
- src: release/completions/sing-box.fish
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
- src: release/completions/sing-box.zsh
dst: /usr/share/zsh/site-functions/_sing-box
- src: LICENSE
dst: /usr/share/licenses/sing-box/LICENSE
ipk:
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: config
- src: release/config/openwrt.init
dst: /etc/init.d/sing-box
- src: release/config/openwrt.conf
dst: /etc/config/sing-box
source: source:
enabled: false enabled: false
name_template: '{{ .ProjectName }}-{{ .Version }}.source' name_template: '{{ .ProjectName }}-{{ .Version }}.source'

View File

@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS builder
LABEL maintainer="nekohasekai <contact-git@sekai.icu>" LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
COPY . /go/src/github.com/sagernet/sing-box COPY . /go/src/github.com/sagernet/sing-box
WORKDIR /go/src/github.com/sagernet/sing-box WORKDIR /go/src/github.com/sagernet/sing-box
@@ -21,7 +21,7 @@ FROM --platform=$TARGETPLATFORM alpine AS dist
LABEL maintainer="nekohasekai <contact-git@sekai.icu>" LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
RUN set -ex \ RUN set -ex \
&& apk upgrade \ && apk upgrade \
&& apk add bash tzdata ca-certificates nftables \ && apk add bash tzdata ca-certificates \
&& rm -rf /var/cache/apk/* && rm -rf /var/cache/apk/*
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
ENTRYPOINT ["sing-box"] ENTRYPOINT ["sing-box"]

View File

@@ -1,6 +1,7 @@
NAME = sing-box NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD) COMMIT = $(shell git rev-parse --short HEAD)
TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api
TAGS_GO120 = with_quic,with_utls
TAGS_GO121 = with_ech TAGS_GO121 = with_ech
TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121) TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121)
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
@@ -19,17 +20,18 @@ PREFIX ?= $(shell go env GOPATH)
build: build:
go build $(MAIN_PARAMS) $(MAIN) go build $(MAIN_PARAMS) $(MAIN)
ci_build_go118:
go build $(PARAMS) $(MAIN)
go build $(PARAMS) -tags "$(TAGS_GO118)" $(MAIN)
ci_build_go120: ci_build_go120:
go build $(PARAMS) $(MAIN) go build $(PARAMS) $(MAIN)
go build $(PARAMS) -tags "$(TAGS_GO120)" $(MAIN) go build $(PARAMS) -tags "$(TAGS_GO118),$(TAGS_GO120)" $(MAIN)
ci_build: ci_build:
go build $(PARAMS) $(MAIN) go build $(PARAMS) $(MAIN)
go build $(MAIN_PARAMS) $(MAIN) go build $(MAIN_PARAMS) $(MAIN)
generate_completions:
go run -v --tags generate,generate_completions $(MAIN)
install: install:
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN) go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
@@ -69,6 +71,7 @@ release:
dist/*.deb \ dist/*.deb \
dist/*.rpm \ dist/*.rpm \
dist/*_amd64.pkg.tar.zst \ dist/*_amd64.pkg.tar.zst \
dist/*_amd64v3.pkg.tar.zst \
dist/*_arm64.pkg.tar.zst \ dist/*_arm64.pkg.tar.zst \
dist/release dist/release
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release
@@ -101,12 +104,10 @@ publish_android:
publish_android_appcenter: publish_android_appcenter:
cd ../sing-box-for-android && ./gradlew :app:appCenterAssembleAndUploadPlayRelease cd ../sing-box-for-android && ./gradlew :app:appCenterAssembleAndUploadPlayRelease
# TODO: find why and remove `-destination 'generic/platform=iOS'`
build_ios: build_ios:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
rm -rf build/SFI.xcarchive && \ 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: upload_ios_app_store:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
@@ -117,70 +118,55 @@ release_ios: build_ios upload_ios_app_store
build_macos: build_macos:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
rm -rf build/SFM.xcarchive && \ 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: upload_macos_app_store:
cd ../sing-box-for-apple && \ 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 release_macos: build_macos upload_macos_app_store
build_macos_standalone: build_macos_independent:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
rm -rf build/SFM.System.xcarchive && \ rm -rf build/SFT.System.xcarchive && \
xcodebuild archive -scheme SFM.System -configuration Release -archivePath build/SFM.System.xcarchive -allowProvisioningUpdates 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 rm -rf dist/SFM
mkdir -p dist/SFM mkdir -p dist/SFM
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
rm -rf build/SFM.System && \ xcodebuild -exportNotarizedApp -archivePath build/SFM.System.xcarchive -exportPath "../sing-box/dist/SFM"
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"
upload_macos_dmg: upload_macos_independent:
cd dist/SFM && \ cd dist/SFM && \
cp SFM.dmg "SFM-${VERSION}-universal.dmg" && \ rm -f *.zip && \
ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.dmg" zip -ry "SFM-${VERSION}-universal.zip" SFM.app && \
ghr --replace --draft --prerelease "v${VERSION}" *.zip
upload_macos_dsyms: release_macos_independent: build_macos_independent notarize_macos_independent wait_notarize_macos_independent export_macos_independent upload_macos_independent
pushd ../sing-box-for-apple/build/SFM.System.xcarchive && \
zip -r SFM.dSYMs.zip dSYMs && \
mv SFM.dSYMs.zip ../../../sing-box/dist/SFM && \
popd && \
cd dist/SFM && \
cp SFM.dSYMs.zip "SFM-${VERSION}-universal.dSYMs.zip" && \
ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.dSYMs.zip"
release_macos_standalone: build_macos_standalone build_macos_dmg upload_macos_dmg upload_macos_dsyms
build_tvos: build_tvos:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
rm -rf build/SFT.xcarchive && \ 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: upload_tvos_app_store:
cd ../sing-box-for-apple && \ 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 release_tvos: build_tvos upload_tvos_app_store
update_apple_version: update_apple_version:
go run ./cmd/internal/update_apple_version go run ./cmd/internal/update_apple_version
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_standalone 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 release_apple_beta: update_apple_version release_ios release_macos release_tvos
@@ -207,19 +193,17 @@ lib:
go run ./cmd/internal/build_libbox -target ios go run ./cmd/internal/build_libbox -target ios
lib_install: lib_install:
go install -v github.com/sagernet/gomobile/cmd/gomobile@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.4 go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.3
docs: docs:
venv/bin/mkdocs serve mkdocs serve
publish_docs: publish_docs:
venv/bin/mkdocs gh-deploy -m "Update" --force --ignore-version --no-history mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
docs_install: docs_install:
python -m venv venv pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*"
source ./venv/bin/activate && pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*"
clean: clean:
rm -rf bin dist sing-box rm -rf bin dist sing-box
rm -f $(shell go env GOPATH)/sing-box rm -f $(shell go env GOPATH)/sing-box

View File

@@ -8,6 +8,10 @@ The universal proxy platform.
https://sing-box.sagernet.org https://sing-box.sagernet.org
## Support
https://community.sagernet.org/c/sing-box/
## License ## License
``` ```

104
adapter/conn_router.go Normal file
View File

@@ -0,0 +1,104 @@
package adapter
import (
"context"
"net"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type ConnectionRouter interface {
RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
}
func NewRouteHandler(
metadata InboundContext,
router ConnectionRouter,
logger logger.ContextLogger,
) UpstreamHandlerAdapter {
return &routeHandlerWrapper{
metadata: metadata,
router: router,
logger: logger,
}
}
func NewRouteContextHandler(
router ConnectionRouter,
logger logger.ContextLogger,
) UpstreamHandlerAdapter {
return &routeContextHandlerWrapper{
router: router,
logger: logger,
}
}
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
type routeHandlerWrapper struct {
metadata InboundContext
router ConnectionRouter
logger logger.ContextLogger
}
func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.router.RouteConnection(ctx, conn, myMetadata)
}
func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.router.RoutePacketConnection(ctx, conn, myMetadata)
}
func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) {
w.logger.ErrorContext(ctx, err)
}
var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
type routeContextHandlerWrapper struct {
router ConnectionRouter
logger logger.ContextLogger
}
func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.router.RouteConnection(ctx, conn, *myMetadata)
}
func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.router.RoutePacketConnection(ctx, conn, *myMetadata)
}
func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) {
w.logger.ErrorContext(ctx, err)
}

View File

@@ -4,13 +4,14 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/binary" "encoding/binary"
"io"
"net" "net"
"time" "time"
"github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-dns" "github.com/sagernet/sing-dns"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/varbin" "github.com/sagernet/sing/common/rw"
) )
type ClashServer interface { type ClashServer interface {
@@ -55,15 +56,16 @@ func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = varbin.Write(&buffer, binary.BigEndian, s.Content) err = rw.WriteUVariant(&buffer, uint64(len(s.Content)))
if err != nil { if err != nil {
return nil, err return nil, err
} }
buffer.Write(s.Content)
err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix()) err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix())
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = varbin.Write(&buffer, binary.BigEndian, s.LastEtag) err = rw.WriteVString(&buffer, s.LastEtag)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -77,7 +79,12 @@ func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
if err != nil { if err != nil {
return err return err
} }
err = varbin.Read(reader, binary.BigEndian, &s.Content) contentLen, err := rw.ReadUVariant(reader)
if err != nil {
return err
}
s.Content = make([]byte, contentLen)
_, err = io.ReadFull(reader, s.Content)
if err != nil { if err != nil {
return err return err
} }
@@ -87,7 +94,7 @@ func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
return err return err
} }
s.LastUpdated = time.Unix(lastUpdated, 0) s.LastUpdated = time.Unix(lastUpdated, 0)
err = varbin.Read(reader, binary.BigEndian, &s.LastEtag) s.LastEtag, err = rw.ReadVString(reader)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -6,53 +6,27 @@ import (
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
// Deprecated
type ConnectionHandler interface { type ConnectionHandler interface {
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
} }
type ConnectionHandlerEx interface {
NewConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
}
// Deprecated: use PacketHandlerEx instead
type PacketHandler interface { type PacketHandler interface {
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error
} }
type PacketHandlerEx interface {
NewPacketEx(buffer *buf.Buffer, source M.Socksaddr)
}
// Deprecated: use OOBPacketHandlerEx instead
type OOBPacketHandler interface { type OOBPacketHandler interface {
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata InboundContext) error NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata InboundContext) error
} }
type OOBPacketHandlerEx interface {
NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr)
}
// Deprecated
type PacketConnectionHandler interface { type PacketConnectionHandler interface {
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
} }
type PacketConnectionHandlerEx interface {
NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
}
type UpstreamHandlerAdapter interface { type UpstreamHandlerAdapter interface {
N.TCPConnectionHandler N.TCPConnectionHandler
N.UDPConnectionHandler N.UDPConnectionHandler
E.Handler E.Handler
} }
type UpstreamHandlerAdapterEx interface {
N.TCPConnectionHandlerEx
N.UDPConnectionHandlerEx
}

View File

@@ -2,12 +2,13 @@ package adapter
import ( import (
"context" "context"
"net"
"net/netip" "net/netip"
"github.com/sagernet/sing-box/common/process" "github.com/sagernet/sing-box/common/process"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
) )
type Inbound interface { type Inbound interface {
@@ -16,19 +17,11 @@ type Inbound interface {
Tag() string Tag() string
} }
type TCPInjectableInbound interface { type InjectableInbound interface {
Inbound Inbound
ConnectionHandlerEx Network() []string
} NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
type UDPInjectableInbound interface {
Inbound
PacketConnectionHandlerEx
}
type InboundRegistry interface {
option.InboundOptionsRegistry
CreateInbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Inbound, error)
} }
type InboundContext struct { type InboundContext struct {
@@ -38,27 +31,17 @@ type InboundContext struct {
Network string Network string
Source M.Socksaddr Source M.Socksaddr
Destination M.Socksaddr Destination M.Socksaddr
Domain string
Protocol string
User string User string
Outbound string Outbound string
// sniffer
Protocol string
Domain string
Client string
SniffContext any
// cache // cache
// Deprecated: implement in rule action InboundDetour string
InboundDetour string LastInbound string
LastInbound string OriginDestination M.Socksaddr
OriginDestination M.Socksaddr InboundOptions option.InboundOptions
// Deprecated
InboundOptions option.InboundOptions
UDPDisableDomainUnmapping bool
DNSServer string
DestinationAddresses []netip.Addr DestinationAddresses []netip.Addr
SourceGeoIPCode string SourceGeoIPCode string
GeoIPCode string GeoIPCode string
@@ -68,9 +51,7 @@ type InboundContext struct {
// rule cache // rule cache
IPCIDRMatchSource bool IPCIDRMatchSource bool
IPCIDRAcceptEmpty bool
SourceAddressMatch bool SourceAddressMatch bool
SourcePortMatch bool SourcePortMatch bool
DestinationAddressMatch bool DestinationAddressMatch bool
@@ -81,7 +62,6 @@ type InboundContext struct {
func (c *InboundContext) ResetRuleCache() { func (c *InboundContext) ResetRuleCache() {
c.IPCIDRMatchSource = false c.IPCIDRMatchSource = false
c.IPCIDRAcceptEmpty = false
c.SourceAddressMatch = false c.SourceAddressMatch = false
c.SourcePortMatch = false c.SourcePortMatch = false
c.DestinationAddressMatch = false c.DestinationAddressMatch = false
@@ -103,6 +83,15 @@ func ContextFrom(ctx context.Context) *InboundContext {
return metadata.(*InboundContext) return metadata.(*InboundContext)
} }
func AppendContext(ctx context.Context) (context.Context, *InboundContext) {
metadata := ContextFrom(ctx)
if metadata != nil {
return ctx, metadata
}
metadata = new(InboundContext)
return WithContext(ctx, metadata), metadata
}
func ExtendContext(ctx context.Context) (context.Context, *InboundContext) { func ExtendContext(ctx context.Context) (context.Context, *InboundContext) {
var newMetadata InboundContext var newMetadata InboundContext
if metadata := ContextFrom(ctx); metadata != nil { if metadata := ContextFrom(ctx); metadata != nil {

View File

@@ -1,21 +0,0 @@
package inbound
type Adapter struct {
inboundType string
inboundTag string
}
func NewAdapter(inboundType string, inboundTag string) Adapter {
return Adapter{
inboundType: inboundType,
inboundTag: inboundTag,
}
}
func (a *Adapter) Type() string {
return a.inboundType
}
func (a *Adapter) Tag() string {
return a.inboundTag
}

View File

@@ -1,68 +0,0 @@
package inbound
import (
"context"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Inbound, error)
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
registry.register(outboundType, func() any {
return new(Options)
}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Inbound, error) {
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options.(*Options)))
})
}
var _ adapter.InboundRegistry = (*Registry)(nil)
type (
optionsConstructorFunc func() any
constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Inbound, error)
)
type Registry struct {
access sync.Mutex
optionsType map[string]optionsConstructorFunc
constructors map[string]constructorFunc
}
func NewRegistry() *Registry {
return &Registry{
optionsType: make(map[string]optionsConstructorFunc),
constructors: make(map[string]constructorFunc),
}
}
func (r *Registry) CreateOptions(outboundType string) (any, bool) {
r.access.Lock()
defer r.access.Unlock()
optionsConstructor, loaded := r.optionsType[outboundType]
if !loaded {
return nil, false
}
return optionsConstructor(), true
}
func (r *Registry) CreateInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
r.access.Lock()
defer r.access.Unlock()
constructor, loaded := r.constructors[outboundType]
if !loaded {
return nil, E.New("outbound type not found: " + outboundType)
}
return constructor(ctx, router, logger, tag, options)
}
func (r *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
r.access.Lock()
defer r.access.Unlock()
r.optionsType[outboundType] = optionsConstructor
r.constructors[outboundType] = constructor
}

View File

@@ -2,9 +2,8 @@ package adapter
import ( import (
"context" "context"
"net"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
@@ -16,9 +15,6 @@ type Outbound interface {
Network() []string Network() []string
Dependencies() []string Dependencies() []string
N.Dialer N.Dialer
} NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
type OutboundRegistry interface {
option.OutboundOptionsRegistry
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
} }

View File

@@ -1,45 +0,0 @@
package outbound
import (
"github.com/sagernet/sing-box/option"
)
type Adapter struct {
protocol string
network []string
tag string
dependencies []string
}
func NewAdapter(protocol string, network []string, tag string, dependencies []string) Adapter {
return Adapter{
protocol: protocol,
network: network,
tag: tag,
dependencies: dependencies,
}
}
func NewAdapterWithDialerOptions(protocol string, network []string, tag string, dialOptions option.DialerOptions) Adapter {
var dependencies []string
if dialOptions.Detour != "" {
dependencies = []string{dialOptions.Detour}
}
return NewAdapter(protocol, network, tag, dependencies)
}
func (a *Adapter) Type() string {
return a.protocol
}
func (a *Adapter) Tag() string {
return a.tag
}
func (a *Adapter) Network() []string {
return a.network
}
func (a *Adapter) Dependencies() []string {
return a.dependencies
}

View File

@@ -1,68 +0,0 @@
package outbound
import (
"context"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Outbound, error)
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
registry.register(outboundType, func() any {
return new(Options)
}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Outbound, error) {
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options.(*Options)))
})
}
var _ adapter.OutboundRegistry = (*Registry)(nil)
type (
optionsConstructorFunc func() any
constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Outbound, error)
)
type Registry struct {
access sync.Mutex
optionsType map[string]optionsConstructorFunc
constructors map[string]constructorFunc
}
func NewRegistry() *Registry {
return &Registry{
optionsType: make(map[string]optionsConstructorFunc),
constructors: make(map[string]constructorFunc),
}
}
func (r *Registry) CreateOptions(outboundType string) (any, bool) {
r.access.Lock()
defer r.access.Unlock()
optionsConstructor, loaded := r.optionsType[outboundType]
if !loaded {
return nil, false
}
return optionsConstructor(), true
}
func (r *Registry) CreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) {
r.access.Lock()
defer r.access.Unlock()
constructor, loaded := r.constructors[outboundType]
if !loaded {
return nil, E.New("outbound type not found: " + outboundType)
}
return constructor(ctx, router, logger, tag, options)
}
func (r *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
r.access.Lock()
defer r.access.Unlock()
r.optionsType[outboundType] = optionsConstructor
r.constructors[outboundType] = constructor
}

View File

@@ -2,30 +2,23 @@ package adapter
import ( import (
"context" "context"
"net"
"net/http" "net/http"
"net/netip" "net/netip"
"sync"
"github.com/sagernet/sing-box/common/geoip" "github.com/sagernet/sing-box/common/geoip"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-dns" "github.com/sagernet/sing-dns"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
mdns "github.com/miekg/dns" mdns "github.com/miekg/dns"
"go4.org/netipx"
) )
type Router interface { type Router interface {
Service Service
PreStarter PreStarter
PostStarter PostStarter
Cleanup() error
Outbounds() []Outbound Outbounds() []Outbound
Outbound(tag string) (Outbound, bool) Outbound(tag string) (Outbound, bool)
@@ -34,8 +27,6 @@ type Router interface {
FakeIPStore() FakeIPStore FakeIPStore() FakeIPStore
ConnectionRouter ConnectionRouter
PreMatch(metadata InboundContext) error
ConnectionRouterEx
GeoIPReader() *geoip.Reader GeoIPReader() *geoip.Reader
LoadGeosite(code string) (Rule, error) LoadGeosite(code string) (Rule, error)
@@ -54,9 +45,7 @@ type Router interface {
DefaultInterface() string DefaultInterface() string
AutoDetectInterface() bool AutoDetectInterface() bool
AutoDetectInterfaceFunc() control.Func AutoDetectInterfaceFunc() control.Func
DefaultMark() uint32 DefaultMark() int
RegisterAutoRedirectOutputMark(mark uint32) error
AutoRedirectOutputMark() uint32
NetworkMonitor() tun.NetworkUpdateMonitor NetworkMonitor() tun.NetworkUpdateMonitor
InterfaceMonitor() tun.DefaultInterfaceMonitor InterfaceMonitor() tun.DefaultInterfaceMonitor
PackageManager() tun.PackageManager PackageManager() tun.PackageManager
@@ -72,18 +61,6 @@ type Router interface {
ResetNetwork() error ResetNetwork() error
} }
// Deprecated: Use ConnectionRouterEx instead.
type ConnectionRouter interface {
RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
}
type ConnectionRouterEx interface {
ConnectionRouter
RouteConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
}
func ContextWithRouter(ctx context.Context, router Router) context.Context { func ContextWithRouter(ctx context.Context, router Router) context.Context {
return service.ContextWith(ctx, router) return service.ContextWith(ctx, router)
} }
@@ -92,64 +69,44 @@ func RouterFromContext(ctx context.Context) Router {
return service.FromContext[Router](ctx) return service.FromContext[Router](ctx)
} }
type HeadlessRule interface {
Match(metadata *InboundContext) bool
String() string
}
type Rule interface {
HeadlessRule
Service
Type() string
UpdateGeosite() error
Outbound() string
}
type DNSRule interface {
Rule
DisableCache() bool
RewriteTTL() *uint32
ClientSubnet() *netip.Prefix
WithAddressLimit() bool
MatchAddressLimit(metadata *InboundContext) bool
}
type RuleSet interface { type RuleSet interface {
Name() string StartContext(ctx context.Context, startContext RuleSetStartContext) error
StartContext(ctx context.Context, startContext *HTTPStartContext) error
PostStart() error
Metadata() RuleSetMetadata Metadata() RuleSetMetadata
ExtractIPSet() []*netipx.IPSet
IncRef()
DecRef()
Cleanup()
RegisterCallback(callback RuleSetUpdateCallback) *list.Element[RuleSetUpdateCallback]
UnregisterCallback(element *list.Element[RuleSetUpdateCallback])
Close() error Close() error
HeadlessRule HeadlessRule
} }
type RuleSetUpdateCallback func(it RuleSet)
type RuleSetMetadata struct { type RuleSetMetadata struct {
ContainsProcessRule bool ContainsProcessRule bool
ContainsWIFIRule bool ContainsWIFIRule bool
ContainsIPCIDRRule bool ContainsIPCIDRRule bool
} }
type HTTPStartContext struct {
access sync.Mutex
httpClientCache map[string]*http.Client
}
func NewHTTPStartContext() *HTTPStartContext { type RuleSetStartContext interface {
return &HTTPStartContext{ HTTPClient(detour string, dialer N.Dialer) *http.Client
httpClientCache: make(map[string]*http.Client), Close()
}
}
func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client {
c.access.Lock()
defer c.access.Unlock()
if httpClient, loaded := c.httpClientCache[detour]; loaded {
return httpClient
}
httpClient := &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: C.TCPTimeout,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
},
}
c.httpClientCache[detour] = httpClient
return httpClient
}
func (c *HTTPStartContext) Close() {
c.access.Lock()
defer c.access.Unlock()
for _, client := range c.httpClientCache {
client.CloseIdleConnections()
}
} }
type InterfaceUpdateListener interface { type InterfaceUpdateListener interface {

View File

@@ -1,38 +0,0 @@
package adapter
import (
C "github.com/sagernet/sing-box/constant"
)
type HeadlessRule interface {
Match(metadata *InboundContext) bool
String() string
}
type Rule interface {
HeadlessRule
Service
Type() string
UpdateGeosite() error
Action() RuleAction
}
type DNSRule interface {
Rule
WithAddressLimit() bool
MatchAddressLimit(metadata *InboundContext) bool
}
type RuleAction interface {
Type() string
String() string
}
func IsFinalAction(action RuleAction) bool {
switch action.Type() {
case C.RuleActionTypeSniff, C.RuleActionTypeResolve:
return false
default:
return true
}
}

View File

@@ -4,165 +4,112 @@ import (
"context" "context"
"net" "net"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
type ( type (
ConnectionHandlerFuncEx = func(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc) ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
PacketConnectionHandlerFuncEx = func(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
) )
func NewUpstreamHandlerEx( func NewUpstreamHandler(
metadata InboundContext, metadata InboundContext,
connectionHandler ConnectionHandlerFuncEx, connectionHandler ConnectionHandlerFunc,
packetHandler PacketConnectionHandlerFuncEx, packetHandler PacketConnectionHandlerFunc,
) UpstreamHandlerAdapterEx { errorHandler E.Handler,
return &myUpstreamHandlerWrapperEx{ ) UpstreamHandlerAdapter {
return &myUpstreamHandlerWrapper{
metadata: metadata, metadata: metadata,
connectionHandler: connectionHandler, connectionHandler: connectionHandler,
packetHandler: packetHandler, packetHandler: packetHandler,
errorHandler: errorHandler,
} }
} }
var _ UpstreamHandlerAdapterEx = (*myUpstreamHandlerWrapperEx)(nil) var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
type myUpstreamHandlerWrapperEx struct { type myUpstreamHandlerWrapper struct {
metadata InboundContext metadata InboundContext
connectionHandler ConnectionHandlerFuncEx connectionHandler ConnectionHandlerFunc
packetHandler PacketConnectionHandlerFuncEx packetHandler PacketConnectionHandlerFunc
errorHandler E.Handler
} }
func (w *myUpstreamHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := w.metadata myMetadata := w.metadata
if source.IsValid() { if metadata.Source.IsValid() {
myMetadata.Source = source myMetadata.Source = metadata.Source
} }
if destination.IsValid() { if metadata.Destination.IsValid() {
myMetadata.Destination = destination myMetadata.Destination = metadata.Destination
} }
w.connectionHandler(ctx, conn, myMetadata, onClose) return w.connectionHandler(ctx, conn, myMetadata)
} }
func (w *myUpstreamHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := w.metadata myMetadata := w.metadata
if source.IsValid() { if metadata.Source.IsValid() {
myMetadata.Source = source myMetadata.Source = metadata.Source
} }
if destination.IsValid() { if metadata.Destination.IsValid() {
myMetadata.Destination = destination myMetadata.Destination = metadata.Destination
} }
w.packetHandler(ctx, conn, myMetadata, onClose) return w.packetHandler(ctx, conn, myMetadata)
} }
var _ UpstreamHandlerAdapterEx = (*myUpstreamContextHandlerWrapperEx)(nil) func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
w.errorHandler.NewError(ctx, err)
type myUpstreamContextHandlerWrapperEx struct {
connectionHandler ConnectionHandlerFuncEx
packetHandler PacketConnectionHandlerFuncEx
} }
func NewUpstreamContextHandlerEx( func UpstreamMetadata(metadata InboundContext) M.Metadata {
connectionHandler ConnectionHandlerFuncEx, return M.Metadata{
packetHandler PacketConnectionHandlerFuncEx, Source: metadata.Source,
) UpstreamHandlerAdapterEx { Destination: metadata.Destination,
return &myUpstreamContextHandlerWrapperEx{ }
}
type myUpstreamContextHandlerWrapper struct {
connectionHandler ConnectionHandlerFunc
packetHandler PacketConnectionHandlerFunc
errorHandler E.Handler
}
func NewUpstreamContextHandler(
connectionHandler ConnectionHandlerFunc,
packetHandler PacketConnectionHandlerFunc,
errorHandler E.Handler,
) UpstreamHandlerAdapter {
return &myUpstreamContextHandlerWrapper{
connectionHandler: connectionHandler, connectionHandler: connectionHandler,
packetHandler: packetHandler, packetHandler: packetHandler,
errorHandler: errorHandler,
} }
} }
func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx) myMetadata := ContextFrom(ctx)
if source.IsValid() { if metadata.Source.IsValid() {
myMetadata.Source = source myMetadata.Source = metadata.Source
} }
if destination.IsValid() { if metadata.Destination.IsValid() {
myMetadata.Destination = destination myMetadata.Destination = metadata.Destination
} }
w.connectionHandler(ctx, conn, *myMetadata, onClose) return w.connectionHandler(ctx, conn, *myMetadata)
} }
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx) myMetadata := ContextFrom(ctx)
if source.IsValid() { if metadata.Source.IsValid() {
myMetadata.Source = source myMetadata.Source = metadata.Source
} }
if destination.IsValid() { if metadata.Destination.IsValid() {
myMetadata.Destination = destination myMetadata.Destination = metadata.Destination
} }
w.packetHandler(ctx, conn, *myMetadata, onClose) return w.packetHandler(ctx, conn, *myMetadata)
} }
func NewRouteHandlerEx( func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
metadata InboundContext, w.errorHandler.NewError(ctx, err)
router ConnectionRouterEx,
) UpstreamHandlerAdapterEx {
return &routeHandlerWrapperEx{
metadata: metadata,
router: router,
}
}
var _ UpstreamHandlerAdapterEx = (*routeHandlerWrapperEx)(nil)
type routeHandlerWrapperEx struct {
metadata InboundContext
router ConnectionRouterEx
}
func (r *routeHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
if source.IsValid() {
r.metadata.Source = source
}
if destination.IsValid() {
r.metadata.Destination = destination
}
r.router.RouteConnectionEx(ctx, conn, r.metadata, onClose)
}
func (r *routeHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
if source.IsValid() {
r.metadata.Source = source
}
if destination.IsValid() {
r.metadata.Destination = destination
}
r.router.RoutePacketConnectionEx(ctx, conn, r.metadata, onClose)
}
func NewRouteContextHandlerEx(
router ConnectionRouterEx,
) UpstreamHandlerAdapterEx {
return &routeContextHandlerWrapperEx{
router: router,
}
}
var _ UpstreamHandlerAdapterEx = (*routeContextHandlerWrapperEx)(nil)
type routeContextHandlerWrapperEx struct {
router ConnectionRouterEx
}
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
metadata := ContextFrom(ctx)
if source.IsValid() {
metadata.Source = source
}
if destination.IsValid() {
metadata.Destination = destination
}
r.router.RouteConnectionEx(ctx, conn, *metadata, onClose)
}
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
metadata := ContextFrom(ctx)
if source.IsValid() {
metadata.Source = source
}
if destination.IsValid() {
metadata.Destination = destination
}
r.router.RoutePacketConnectionEx(ctx, conn, *metadata, onClose)
} }

View File

@@ -1,216 +0,0 @@
package adapter
import (
"context"
"net"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type (
// Deprecated
ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
// Deprecated
PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
)
// Deprecated
func NewUpstreamHandler(
metadata InboundContext,
connectionHandler ConnectionHandlerFunc,
packetHandler PacketConnectionHandlerFunc,
errorHandler E.Handler,
) UpstreamHandlerAdapter {
return &myUpstreamHandlerWrapper{
metadata: metadata,
connectionHandler: connectionHandler,
packetHandler: packetHandler,
errorHandler: errorHandler,
}
}
var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
// Deprecated
type myUpstreamHandlerWrapper struct {
metadata InboundContext
connectionHandler ConnectionHandlerFunc
packetHandler PacketConnectionHandlerFunc
errorHandler E.Handler
}
func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.connectionHandler(ctx, conn, myMetadata)
}
func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.packetHandler(ctx, conn, myMetadata)
}
func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
w.errorHandler.NewError(ctx, err)
}
// Deprecated
func UpstreamMetadata(metadata InboundContext) M.Metadata {
return M.Metadata{
Source: metadata.Source,
Destination: metadata.Destination,
}
}
// Deprecated
type myUpstreamContextHandlerWrapper struct {
connectionHandler ConnectionHandlerFunc
packetHandler PacketConnectionHandlerFunc
errorHandler E.Handler
}
// Deprecated
func NewUpstreamContextHandler(
connectionHandler ConnectionHandlerFunc,
packetHandler PacketConnectionHandlerFunc,
errorHandler E.Handler,
) UpstreamHandlerAdapter {
return &myUpstreamContextHandlerWrapper{
connectionHandler: connectionHandler,
packetHandler: packetHandler,
errorHandler: errorHandler,
}
}
func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.connectionHandler(ctx, conn, *myMetadata)
}
func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.packetHandler(ctx, conn, *myMetadata)
}
func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
w.errorHandler.NewError(ctx, err)
}
// Deprecated: Use ConnectionRouterEx instead.
func NewRouteHandler(
metadata InboundContext,
router ConnectionRouter,
logger logger.ContextLogger,
) UpstreamHandlerAdapter {
return &routeHandlerWrapper{
metadata: metadata,
router: router,
logger: logger,
}
}
// Deprecated: Use ConnectionRouterEx instead.
func NewRouteContextHandler(
router ConnectionRouter,
logger logger.ContextLogger,
) UpstreamHandlerAdapter {
return &routeContextHandlerWrapper{
router: router,
logger: logger,
}
}
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
// Deprecated: Use ConnectionRouterEx instead.
type routeHandlerWrapper struct {
metadata InboundContext
router ConnectionRouter
logger logger.ContextLogger
}
func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.router.RouteConnection(ctx, conn, myMetadata)
}
func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.router.RoutePacketConnection(ctx, conn, myMetadata)
}
func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) {
w.logger.ErrorContext(ctx, err)
}
var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
// Deprecated: Use ConnectionRouterEx instead.
type routeContextHandlerWrapper struct {
router ConnectionRouter
logger logger.ContextLogger
}
func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.router.RouteConnection(ctx, conn, *myMetadata)
}
func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.router.RoutePacketConnection(ctx, conn, *myMetadata)
}
func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) {
w.logger.ErrorContext(ctx, err)
}

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"net" "net"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
@@ -15,10 +16,10 @@ type V2RayServerTransport interface {
} }
type V2RayServerTransportHandler interface { type V2RayServerTransportHandler interface {
N.TCPConnectionHandlerEx N.TCPConnectionHandler
E.Handler
} }
type V2RayClientTransport interface { type V2RayClientTransport interface {
DialContext(ctx context.Context) (net.Conn, error) DialContext(ctx context.Context) (net.Conn, error)
Close() error
} }

126
box.go
View File

@@ -14,9 +14,10 @@ import (
"github.com/sagernet/sing-box/experimental" "github.com/sagernet/sing-box/experimental"
"github.com/sagernet/sing-box/experimental/cachefile" "github.com/sagernet/sing-box/experimental/cachefile"
"github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/inbound"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/direct" "github.com/sagernet/sing-box/outbound"
"github.com/sagernet/sing-box/route" "github.com/sagernet/sing-box/route"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@@ -43,37 +44,16 @@ type Box struct {
type Options struct { type Options struct {
option.Options option.Options
Context context.Context Context context.Context
PlatformInterface platform.Interface
PlatformLogWriter log.PlatformWriter PlatformLogWriter log.PlatformWriter
} }
func Context(ctx context.Context, inboundRegistry adapter.InboundRegistry, outboundRegistry adapter.OutboundRegistry) context.Context {
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
ctx = service.ContextWith[option.InboundOptionsRegistry](ctx, inboundRegistry)
ctx = service.ContextWith[adapter.InboundRegistry](ctx, inboundRegistry)
}
if service.FromContext[option.OutboundOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.OutboundRegistry](ctx) == nil {
ctx = service.ContextWith[option.OutboundOptionsRegistry](ctx, outboundRegistry)
ctx = service.ContextWith[adapter.OutboundRegistry](ctx, outboundRegistry)
}
return ctx
}
func New(options Options) (*Box, error) { func New(options Options) (*Box, error) {
createdAt := time.Now() createdAt := time.Now()
ctx := options.Context ctx := options.Context
if ctx == nil { if ctx == nil {
ctx = context.Background() ctx = context.Background()
} }
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
if inboundRegistry == nil {
return nil, E.New("missing inbound registry in context")
}
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
if outboundRegistry == nil {
return nil, E.New("missing outbound registry in context")
}
ctx = service.ContextWithDefaultRegistry(ctx) ctx = service.ContextWithDefaultRegistry(ctx)
ctx = pause.WithDefaultManager(ctx) ctx = pause.WithDefaultManager(ctx)
experimentalOptions := common.PtrValueOrDefault(options.Experimental) experimentalOptions := common.PtrValueOrDefault(options.Experimental)
@@ -90,9 +70,8 @@ func New(options Options) (*Box, error) {
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" { if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
needV2RayAPI = true needV2RayAPI = true
} }
platformInterface := service.FromContext[platform.Interface](ctx)
var defaultLogWriter io.Writer var defaultLogWriter io.Writer
if platformInterface != nil { if options.PlatformInterface != nil {
defaultLogWriter = io.Discard defaultLogWriter = io.Discard
} }
logFactory, err := log.New(log.Options{ logFactory, err := log.New(log.Options{
@@ -113,92 +92,63 @@ func New(options Options) (*Box, error) {
common.PtrValueOrDefault(options.DNS), common.PtrValueOrDefault(options.DNS),
common.PtrValueOrDefault(options.NTP), common.PtrValueOrDefault(options.NTP),
options.Inbounds, options.Inbounds,
options.PlatformInterface,
) )
if err != nil { if err != nil {
return nil, E.Cause(err, "parse route options") return nil, E.Cause(err, "parse route options")
} }
//nolint:staticcheck
if len(options.LegacyInbounds) > 0 {
for _, legacyInbound := range options.LegacyInbounds {
options.Inbounds = append(options.Inbounds, option.Inbound{
Type: legacyInbound.Type,
Tag: legacyInbound.Tag,
Options: common.Must1(legacyInbound.RawOptions()),
})
}
}
inbounds := make([]adapter.Inbound, 0, len(options.Inbounds)) inbounds := make([]adapter.Inbound, 0, len(options.Inbounds))
//nolint:staticcheck
if len(options.LegacyOutbounds) > 0 {
for _, legacyOutbound := range options.LegacyOutbounds {
options.Outbounds = append(options.Outbounds, option.Outbound{
Type: legacyOutbound.Type,
Tag: legacyOutbound.Tag,
Options: common.Must1(legacyOutbound.RawOptions()),
})
}
}
outbounds := make([]adapter.Outbound, 0, len(options.Outbounds)) outbounds := make([]adapter.Outbound, 0, len(options.Outbounds))
for i, inboundOptions := range options.Inbounds { for i, inboundOptions := range options.Inbounds {
var currentInbound adapter.Inbound var in adapter.Inbound
var tag string var tag string
if inboundOptions.Tag != "" { if inboundOptions.Tag != "" {
tag = inboundOptions.Tag tag = inboundOptions.Tag
} else { } else {
tag = F.ToString(i) tag = F.ToString(i)
} }
currentInbound, err = inboundRegistry.CreateInbound( in, err = inbound.New(
ctx, ctx,
router, router,
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")), logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
tag, inboundOptions,
inboundOptions.Type, options.PlatformInterface,
inboundOptions.Options,
) )
if err != nil { if err != nil {
return nil, E.Cause(err, "parse inbound[", i, "]") return nil, E.Cause(err, "parse inbound[", i, "]")
} }
inbounds = append(inbounds, currentInbound) inbounds = append(inbounds, in)
} }
for i, outboundOptions := range options.Outbounds { for i, outboundOptions := range options.Outbounds {
var currentOutbound adapter.Outbound var out adapter.Outbound
var tag string var tag string
if outboundOptions.Tag != "" { if outboundOptions.Tag != "" {
tag = outboundOptions.Tag tag = outboundOptions.Tag
} else { } else {
tag = F.ToString(i) tag = F.ToString(i)
} }
outboundCtx := ctx out, err = outbound.New(
if tag != "" { ctx,
// TODO: remove this
outboundCtx = adapter.WithContext(outboundCtx, &adapter.InboundContext{
Outbound: tag,
})
}
currentOutbound, err = outboundRegistry.CreateOutbound(
outboundCtx,
router, router,
logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")), logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
tag, tag,
outboundOptions.Type, outboundOptions)
outboundOptions.Options,
)
if err != nil { if err != nil {
return nil, E.Cause(err, "parse outbound[", i, "]") return nil, E.Cause(err, "parse outbound[", i, "]")
} }
outbounds = append(outbounds, currentOutbound) outbounds = append(outbounds, out)
} }
err = router.Initialize(inbounds, outbounds, func() adapter.Outbound { err = router.Initialize(inbounds, outbounds, func() adapter.Outbound {
defaultOutbound, cErr := direct.NewOutbound(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.DirectOutboundOptions{}) out, oErr := outbound.New(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.Outbound{Type: "direct", Tag: "default"})
common.Must(cErr) common.Must(oErr)
outbounds = append(outbounds, defaultOutbound) outbounds = append(outbounds, out)
return defaultOutbound return out
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
if platformInterface != nil { if options.PlatformInterface != nil {
err = platformInterface.Initialize(ctx, router) err = options.PlatformInterface.Initialize(ctx, router)
if err != nil { if err != nil {
return nil, E.Cause(err, "initialize platform interface") return nil, E.Cause(err, "initialize platform interface")
} }
@@ -253,7 +203,7 @@ func (s *Box) PreStart() error {
defer func() { defer func() {
v := recover() v := recover()
if v != nil { if v != nil {
println(err.Error()) log.Error(E.Cause(err, "origin error"))
debug.PrintStack() debug.PrintStack()
panic("panic on early close: " + fmt.Sprint(v)) panic("panic on early close: " + fmt.Sprint(v))
} }
@@ -272,9 +222,9 @@ func (s *Box) Start() error {
defer func() { defer func() {
v := recover() v := recover()
if v != nil { if v != nil {
println(err.Error()) log.Error(E.Cause(err, "origin error"))
debug.PrintStack() debug.PrintStack()
println("panic on early start: " + fmt.Sprint(v)) panic("panic on early close: " + fmt.Sprint(v))
} }
}() }()
s.Close() s.Close()
@@ -352,11 +302,7 @@ func (s *Box) start() error {
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]") return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
} }
} }
err = s.postStart() return s.postStart()
if err != nil {
return err
}
return s.router.Cleanup()
} }
func (s *Box) postStart() error { func (s *Box) postStart() error {
@@ -366,28 +312,16 @@ func (s *Box) postStart() error {
return E.Cause(err, "start ", serviceName) return E.Cause(err, "start ", serviceName)
} }
} }
// TODO: reorganize ALL start order for _, outbound := range s.outbounds {
for _, out := range s.outbounds { if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound {
if lateOutbound, isLateOutbound := out.(adapter.PostStarter); isLateOutbound {
err := lateOutbound.PostStart() err := lateOutbound.PostStart()
if err != nil { if err != nil {
return E.Cause(err, "post-start outbound/", out.Tag()) return E.Cause(err, "post-start outbound/", outbound.Tag())
} }
} }
} }
err := s.router.PostStart()
if err != nil { return s.router.PostStart()
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 { func (s *Box) Close() error {

View File

@@ -45,9 +45,7 @@ func (s *Box) startOutbounds() error {
} }
started[outboundTag] = true started[outboundTag] = true
canContinue = true canContinue = true
if starter, isStarter := outboundToStart.(interface { if starter, isStarter := outboundToStart.(common.Starter); isStarter {
Start() error
}); isStarter {
monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]") monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
err := starter.Start() err := starter.Start()
monitor.Finish() monitor.Finish()

View File

@@ -93,7 +93,7 @@ func buildAndroid() {
const name = "libbox.aar" const name = "libbox.aar"
copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs") copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs")
if rw.IsDir(copyPath) { if rw.FileExists(copyPath) {
copyPath, _ = filepath.Abs(copyPath) copyPath, _ = filepath.Abs(copyPath)
err = rw.CopyFile(name, filepath.Join(copyPath, name)) err = rw.CopyFile(name, filepath.Join(copyPath, name))
if err != nil { if err != nil {
@@ -134,7 +134,7 @@ func buildiOS() {
} }
copyPath := filepath.Join("..", "sing-box-for-apple") copyPath := filepath.Join("..", "sing-box-for-apple")
if rw.IsDir(copyPath) { if rw.FileExists(copyPath) {
targetDir := filepath.Join(copyPath, "Libbox.xcframework") targetDir := filepath.Join(copyPath, "Libbox.xcframework")
targetDir, _ = filepath.Abs(targetDir) targetDir, _ = filepath.Abs(targetDir)
os.RemoveAll(targetDir) os.RemoveAll(targetDir)

View File

@@ -30,7 +30,7 @@ func FindSDK() {
} }
for _, path := range searchPath { for _, path := range searchPath {
path = os.ExpandEnv(path) path = os.ExpandEnv(path)
if rw.IsFile(filepath.Join(path, "licenses", "android-sdk-license")) { if rw.FileExists(filepath.Join(path, "licenses", "android-sdk-license")) {
androidSDKPath = path androidSDKPath = path
break break
} }
@@ -58,9 +58,9 @@ func FindSDK() {
} }
func findNDK() bool { func findNDK() bool {
const fixedVersion = "27.2.12479018" const fixedVersion = "26.2.11394342"
const versionFile = "source.properties" const versionFile = "source.properties"
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) { if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.FileExists(filepath.Join(fixedPath, versionFile)) {
androidNDKPath = fixedPath androidNDKPath = fixedPath
return true return true
} }
@@ -86,7 +86,7 @@ func findNDK() bool {
}) })
for _, versionName := range versionNames { for _, versionName := range versionNames {
currentNDKPath := filepath.Join(androidSDKPath, "ndk", versionName) currentNDKPath := filepath.Join(androidSDKPath, "ndk", versionName)
if rw.IsFile(filepath.Join(currentNDKPath, versionFile)) { if rw.FileExists(filepath.Join(androidSDKPath, versionFile)) {
androidNDKPath = currentNDKPath androidNDKPath = currentNDKPath
log.Warn("reproducibility warning: using NDK version " + versionName + " instead of " + fixedVersion) log.Warn("reproducibility warning: using NDK version " + versionName + " instead of " + fixedVersion)
return true return true
@@ -100,11 +100,11 @@ var GoBinPath string
func FindMobile() { func FindMobile() {
goBin := filepath.Join(build.Default.GOPATH, "bin") goBin := filepath.Join(build.Default.GOPATH, "bin")
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
if !rw.IsFile(filepath.Join(goBin, "gobind.exe")) { if !rw.FileExists(filepath.Join(goBin, "gobind.exe")) {
log.Fatal("missing gomobile installation") log.Fatal("missing gomobile installation")
} }
} else { } else {
if !rw.IsFile(filepath.Join(goBin, "gobind")) { if !rw.FileExists(filepath.Join(goBin, "gobind")) {
log.Fatal("missing gomobile installation") log.Fatal("missing gomobile installation")
} }
} }

View File

@@ -26,8 +26,8 @@ func main() {
common.Must(decoder.Decode(&project)) common.Must(decoder.Decode(&project))
objectsMap := project["objects"].(map[string]any) objectsMap := project["objects"].(map[string]any)
projectContent := string(common.Must1(os.ReadFile("sing-box.xcodeproj/project.pbxproj"))) projectContent := string(common.Must1(os.ReadFile("sing-box.xcodeproj/project.pbxproj")))
newContent, updated0 := findAndReplace(objectsMap, projectContent, []string{"io.nekohasekai.sfavt"}, newVersion.VersionString()) newContent, updated0 := findAndReplace(objectsMap, projectContent, []string{"io.nekohasekai.sfa"}, newVersion.VersionString())
newContent, updated1 := findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfavt.standalone", "io.nekohasekai.sfavt.system"}, newVersion.String()) newContent, updated1 := findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfa.independent", "io.nekohasekai.sfa.system"}, newVersion.String())
if updated0 || updated1 { if updated0 || updated1 {
log.Info("updated version to ", newVersion.VersionString(), " (", newVersion.String(), ")") log.Info("updated version to ", newVersion.VersionString(), " (", newVersion.String(), ")")
} }

View File

@@ -1,73 +0,0 @@
package main
import (
"context"
"os"
"os/user"
"strconv"
"time"
"github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
"github.com/spf13/cobra"
)
var (
globalCtx context.Context
configPaths []string
configDirectories []string
workingDir string
disableColor bool
)
var mainCommand = &cobra.Command{
Use: "sing-box",
PersistentPreRun: preRun,
}
func init() {
mainCommand.PersistentFlags().StringArrayVarP(&configPaths, "config", "c", nil, "set configuration file path")
mainCommand.PersistentFlags().StringArrayVarP(&configDirectories, "config-directory", "C", nil, "set configuration directory path")
mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
}
func preRun(cmd *cobra.Command, args []string) {
globalCtx = context.Background()
sudoUser := os.Getenv("SUDO_USER")
sudoUID, _ := strconv.Atoi(os.Getenv("SUDO_UID"))
sudoGID, _ := strconv.Atoi(os.Getenv("SUDO_GID"))
if sudoUID == 0 && sudoGID == 0 && sudoUser != "" {
sudoUserObject, _ := user.Lookup(sudoUser)
if sudoUserObject != nil {
sudoUID, _ = strconv.Atoi(sudoUserObject.Uid)
sudoGID, _ = strconv.Atoi(sudoUserObject.Gid)
}
}
if sudoUID > 0 && sudoGID > 0 {
globalCtx = filemanager.WithDefault(globalCtx, "", "", sudoUID, sudoGID)
}
if disableColor {
log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger())
}
if workingDir != "" {
_, err := os.Stat(workingDir)
if err != nil {
filemanager.MkdirAll(globalCtx, workingDir, 0o777)
}
err = os.Chdir(workingDir)
if err != nil {
log.Fatal(err)
}
}
if len(configPaths) == 0 && len(configDirectories) == 0 {
configPaths = append(configPaths, "config.json")
}
globalCtx = service.ContextWith(globalCtx, deprecated.NewEnvManager(log.StdLogger()))
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry())
}

View File

@@ -2,7 +2,6 @@ package main
import ( import (
"bytes" "bytes"
"context"
"os" "os"
"path/filepath" "path/filepath"
@@ -39,7 +38,7 @@ func format() error {
return err return err
} }
for _, optionsEntry := range optionsList { for _, optionsEntry := range optionsList {
optionsEntry.options, err = badjson.Omitempty(context.TODO(), optionsEntry.options) optionsEntry.options, err = badjson.Omitempty(optionsEntry.options)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -87,7 +87,7 @@ func geoipExport(countryCode string) error {
headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String()) headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String())
} }
var plainRuleSet option.PlainRuleSetCompat var plainRuleSet option.PlainRuleSetCompat
plainRuleSet.Version = C.RuleSetVersion2 plainRuleSet.Version = C.RuleSetVersion1
plainRuleSet.Options.Rules = []option.HeadlessRule{ plainRuleSet.Options.Rules = []option.HeadlessRule{
{ {
Type: C.RuleTypeDefault, Type: C.RuleTypeDefault,

View File

@@ -70,7 +70,7 @@ func geositeExport(category string) error {
headlessRule.DomainKeyword = defaultRule.DomainKeyword headlessRule.DomainKeyword = defaultRule.DomainKeyword
headlessRule.DomainRegex = defaultRule.DomainRegex headlessRule.DomainRegex = defaultRule.DomainRegex
var plainRuleSet option.PlainRuleSetCompat var plainRuleSet option.PlainRuleSetCompat
plainRuleSet.Version = C.RuleSetVersion2 plainRuleSet.Version = C.RuleSetVersion1
plainRuleSet.Options.Rules = []option.HeadlessRule{ plainRuleSet.Options.Rules = []option.HeadlessRule{
{ {
Type: C.RuleTypeDefault, Type: C.RuleTypeDefault,

View File

@@ -54,11 +54,7 @@ func merge(outputPath string) error {
return nil return nil
} }
} }
err = rw.MkdirParent(outputPath) err = rw.WriteFile(outputPath, buffer.Bytes())
if err != nil {
return err
}
err = os.WriteFile(outputPath, buffer.Bytes(), 0o644)
if err != nil { if err != nil {
return err return err
} }
@@ -68,19 +64,29 @@ func merge(outputPath string) error {
} }
func mergePathResources(options *option.Options) error { func mergePathResources(options *option.Options) error {
for _, inbound := range options.Inbounds { for index, inbound := range options.Inbounds {
if tlsOptions, containsTLSOptions := inbound.Options.(option.InboundTLSOptionsWrapper); containsTLSOptions { rawOptions, err := inbound.RawOptions()
if err != nil {
return err
}
if tlsOptions, containsTLSOptions := rawOptions.(option.InboundTLSOptionsWrapper); containsTLSOptions {
tlsOptions.ReplaceInboundTLSOptions(mergeTLSInboundOptions(tlsOptions.TakeInboundTLSOptions())) tlsOptions.ReplaceInboundTLSOptions(mergeTLSInboundOptions(tlsOptions.TakeInboundTLSOptions()))
} }
options.Inbounds[index] = inbound
} }
for _, outbound := range options.Outbounds { for index, outbound := range options.Outbounds {
rawOptions, err := outbound.RawOptions()
if err != nil {
return err
}
switch outbound.Type { switch outbound.Type {
case C.TypeSSH: case C.TypeSSH:
mergeSSHOutboundOptions(outbound.Options.(*option.SSHOutboundOptions)) outbound.SSHOptions = mergeSSHOutboundOptions(outbound.SSHOptions)
} }
if tlsOptions, containsTLSOptions := outbound.Options.(option.OutboundTLSOptionsWrapper); containsTLSOptions { if tlsOptions, containsTLSOptions := rawOptions.(option.OutboundTLSOptionsWrapper); containsTLSOptions {
tlsOptions.ReplaceOutboundTLSOptions(mergeTLSOutboundOptions(tlsOptions.TakeOutboundTLSOptions())) tlsOptions.ReplaceOutboundTLSOptions(mergeTLSOutboundOptions(tlsOptions.TakeOutboundTLSOptions()))
} }
options.Outbounds[index] = outbound
} }
return nil return nil
} }
@@ -128,12 +134,13 @@ func mergeTLSOutboundOptions(options *option.OutboundTLSOptions) *option.Outboun
return options return options
} }
func mergeSSHOutboundOptions(options *option.SSHOutboundOptions) { func mergeSSHOutboundOptions(options option.SSHOutboundOptions) option.SSHOutboundOptions {
if options.PrivateKeyPath != "" { if options.PrivateKeyPath != "" {
if content, err := os.ReadFile(os.ExpandEnv(options.PrivateKeyPath)); err == nil { if content, err := os.ReadFile(os.ExpandEnv(options.PrivateKeyPath)); err == nil {
options.PrivateKey = trimStringArray(strings.Split(string(content), "\n")) options.PrivateKey = trimStringArray(strings.Split(string(content), "\n"))
} }
} }
return options
} }
func trimStringArray(array []string) []string { func trimStringArray(array []string) []string {

View File

@@ -6,7 +6,7 @@ import (
var commandRuleSet = &cobra.Command{ var commandRuleSet = &cobra.Command{
Use: "rule-set", Use: "rule-set",
Short: "Manage rule-sets", Short: "Manage rule sets",
} }
func init() { func init() {

View File

@@ -6,7 +6,6 @@ import (
"strings" "strings"
"github.com/sagernet/sing-box/common/srs" "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/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
@@ -56,10 +55,10 @@ func compileRuleSet(sourcePath string) error {
if err != nil { if err != nil {
return err return err
} }
ruleSet, err := plainRuleSet.Upgrade()
if err != nil { if err != nil {
return err return err
} }
ruleSet := plainRuleSet.Upgrade()
var outputPath string var outputPath string
if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput { if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".json") { if strings.HasSuffix(sourcePath, ".json") {
@@ -74,7 +73,7 @@ func compileRuleSet(sourcePath string) error {
if err != nil { if err != nil {
return err return err
} }
err = srs.Write(outputFile, ruleSet, plainRuleSet.Version == C.RuleSetVersion2) err = srs.Write(outputFile, ruleSet)
if err != nil { if err != nil {
outputFile.Close() outputFile.Close()
os.Remove(outputPath) os.Remove(outputPath)

View File

@@ -1,88 +0,0 @@
package main
import (
"io"
"os"
"strings"
"github.com/sagernet/sing-box/cmd/sing-box/internal/convertor/adguard"
"github.com/sagernet/sing-box/common/srs"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/spf13/cobra"
)
var (
flagRuleSetConvertType string
flagRuleSetConvertOutput string
)
var commandRuleSetConvert = &cobra.Command{
Use: "convert [source-path]",
Short: "Convert adguard DNS filter to rule-set",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := convertRuleSet(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandRuleSet.AddCommand(commandRuleSetConvert)
commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertType, "type", "t", "", "Source type, available: adguard")
commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file")
}
func convertRuleSet(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
}
}
var rules []option.HeadlessRule
switch flagRuleSetConvertType {
case "adguard":
rules, err = adguard.Convert(reader)
case "":
return E.New("source type is required")
default:
return E.New("unsupported source type: ", flagRuleSetConvertType)
}
if err != nil {
return err
}
var outputPath string
if flagRuleSetConvertOutput == flagRuleSetCompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".txt") {
outputPath = sourcePath[:len(sourcePath)-4] + ".srs"
} else {
outputPath = sourcePath + ".srs"
}
} else {
outputPath = flagRuleSetConvertOutput
}
outputFile, err := os.Create(outputPath)
if err != nil {
return err
}
defer outputFile.Close()
err = srs.Write(outputFile, option.PlainRuleSet{Rules: rules}, true)
if err != nil {
outputFile.Close()
os.Remove(outputPath)
return err
}
outputFile.Close()
return nil
}

View File

@@ -1,83 +0,0 @@
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
}

View File

@@ -10,11 +10,9 @@ import (
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing-box/route"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
M "github.com/sagernet/sing/common/metadata"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -22,8 +20,8 @@ import (
var flagRuleSetMatchFormat string var flagRuleSetMatchFormat string
var commandRuleSetMatch = &cobra.Command{ var commandRuleSetMatch = &cobra.Command{
Use: "match <rule-set path> <IP address/domain>", Use: "match <rule-set path> <domain>",
Short: "Check if an IP address or a domain matches the rule-set", Short: "Check if a domain matches the rule set",
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
err := ruleSetMatch(args[0], args[1]) err := ruleSetMatch(args[0], args[1])
@@ -63,33 +61,25 @@ func ruleSetMatch(sourcePath string, domain string) error {
if err != nil { if err != nil {
return err return err
} }
plainRuleSet, err = compat.Upgrade() plainRuleSet = compat.Upgrade()
if err != nil {
return err
}
case C.RuleSetFormatBinary: case C.RuleSetFormatBinary:
plainRuleSet, err = srs.Read(bytes.NewReader(content), false) plainRuleSet, err = srs.Read(bytes.NewReader(content), false)
if err != nil { if err != nil {
return err return err
} }
default: 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 { for i, ruleOptions := range plainRuleSet.Rules {
var currentRule adapter.HeadlessRule var currentRule adapter.HeadlessRule
currentRule, err = rule.NewHeadlessRule(nil, ruleOptions) currentRule, err = route.NewHeadlessRule(nil, ruleOptions)
if err != nil { if err != nil {
return E.Cause(err, "parse rule_set.rules.[", i, "]") return E.Cause(err, "parse rule_set.rules.[", i, "]")
} }
if currentRule.Match(&metadata) { if currentRule.Match(&adapter.InboundContext{
println(F.ToString("match rules.[", i, "]: ", currentRule)) Domain: domain,
}) {
println("match rules.[", i, "]: "+currentRule.String())
} }
} }
return nil return nil

View File

@@ -1,94 +0,0 @@
package main
import (
"bytes"
"io"
"os"
"path/filepath"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/spf13/cobra"
)
var commandRuleSetUpgradeFlagWrite bool
var commandRuleSetUpgrade = &cobra.Command{
Use: "upgrade <source-path>",
Short: "Upgrade rule-set json",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := upgradeRuleSet(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandRuleSetUpgrade.Flags().BoolVarP(&commandRuleSetUpgradeFlagWrite, "write", "w", false, "write result to (source) file instead of stdout")
commandRuleSet.AddCommand(commandRuleSetUpgrade)
}
func upgradeRuleSet(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
}
}
content, err := io.ReadAll(reader)
if err != nil {
return err
}
plainRuleSetCompat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content)
if err != nil {
return err
}
switch plainRuleSetCompat.Version {
case C.RuleSetVersion1:
default:
log.Info("already up-to-date")
return nil
}
plainRuleSet, err := plainRuleSetCompat.Upgrade()
if err != nil {
return err
}
buffer := new(bytes.Buffer)
encoder := json.NewEncoder(buffer)
encoder.SetIndent("", " ")
err = encoder.Encode(plainRuleSet)
if err != nil {
return E.Cause(err, "encode config")
}
outputPath, _ := filepath.Abs(sourcePath)
if !commandRuleSetUpgradeFlagWrite || sourcePath == "stdin" {
os.Stdout.WriteString(buffer.String() + "\n")
return nil
}
if bytes.Equal(content, buffer.Bytes()) {
return nil
}
output, err := os.Create(sourcePath)
if err != nil {
return E.Cause(err, "open output")
}
_, err = output.Write(buffer.Bytes())
output.Close()
if err != nil {
return E.Cause(err, "write output")
}
os.Stderr.WriteString(outputPath + "\n")
return nil
}

View File

@@ -57,7 +57,7 @@ func readConfigAt(path string) (*OptionsEntry, error) {
if err != nil { if err != nil {
return nil, E.Cause(err, "read config at ", path) return nil, E.Cause(err, "read config at ", path)
} }
options, err := json.UnmarshalExtendedContext[option.Options](globalCtx, configContent) options, err := json.UnmarshalExtended[option.Options](configContent)
if err != nil { if err != nil {
return nil, E.Cause(err, "decode config at ", path) return nil, E.Cause(err, "decode config at ", path)
} }
@@ -109,13 +109,13 @@ func readConfigAndMerge() (option.Options, error) {
} }
var mergedMessage json.RawMessage var mergedMessage json.RawMessage
for _, options := range optionsList { for _, options := range optionsList {
mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false) mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage)
if err != nil { if err != nil {
return option.Options{}, E.Cause(err, "merge config at ", options.path) return option.Options{}, E.Cause(err, "merge config at ", options.path)
} }
} }
var mergedOptions option.Options var mergedOptions option.Options
err = mergedOptions.UnmarshalJSONContext(globalCtx, mergedMessage) err = mergedOptions.UnmarshalJSON(mergedMessage)
if err != nil { if err != nil {
return option.Options{}, E.Cause(err, "unmarshal merged config") return option.Options{}, E.Cause(err, "unmarshal merged config")
} }
@@ -188,12 +188,9 @@ func run() error {
cancel() cancel()
closeCtx, closed := context.WithCancel(context.Background()) closeCtx, closed := context.WithCancel(context.Background())
go closeMonitor(closeCtx) go closeMonitor(closeCtx)
err = instance.Close() instance.Close()
closed() closed()
if osSignal != syscall.SIGHUP { if osSignal != syscall.SIGHUP {
if err != nil {
log.Error(E.Cause(err, "sing-box did not closed properly"))
}
return nil return nil
} }
break break

View File

@@ -1,9 +1,6 @@
package main package main
import ( import (
"errors"
"os"
"github.com/sagernet/sing-box" "github.com/sagernet/sing-box"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
@@ -26,9 +23,7 @@ func init() {
func createPreStartedClient() (*box.Box, error) { func createPreStartedClient() (*box.Box, error) {
options, err := readConfigAndMerge() options, err := readConfigAndMerge()
if err != nil { if err != nil {
if !(errors.Is(err, os.ErrNotExist) && len(configDirectories) == 0 && len(configPaths) == 1) || configPaths[0] != "config.json" { return nil, err
return nil, err
}
} }
instance, err := box.New(box.Options{Options: options}) instance, err := box.New(box.Options{Options: options})
if err != nil { if err != nil {

View File

@@ -9,10 +9,8 @@ import (
"net/url" "net/url"
"os" "os"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -34,10 +32,7 @@ func init() {
commandTools.AddCommand(commandFetch) commandTools.AddCommand(commandFetch)
} }
var ( var httpClient *http.Client
httpClient *http.Client
http3Client *http.Client
)
func fetch(args []string) error { func fetch(args []string) error {
instance, err := createPreStartedClient() instance, err := createPreStartedClient()
@@ -58,16 +53,8 @@ func fetch(args []string) error {
}, },
} }
defer httpClient.CloseIdleConnections() defer httpClient.CloseIdleConnections()
if C.WithQUIC {
err = initializeHTTP3Client(instance)
if err != nil {
return err
}
defer http3Client.CloseIdleConnections()
}
for _, urlString := range args { for _, urlString := range args {
var parsedURL *url.URL parsedURL, err := url.Parse(urlString)
parsedURL, err = url.Parse(urlString)
if err != nil { if err != nil {
return err return err
} }
@@ -76,27 +63,16 @@ func fetch(args []string) error {
parsedURL.Scheme = "http" parsedURL.Scheme = "http"
fallthrough fallthrough
case "http", "https": case "http", "https":
err = fetchHTTP(httpClient, parsedURL) err = fetchHTTP(parsedURL)
if err != nil { if err != nil {
return err 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 return nil
} }
func fetchHTTP(httpClient *http.Client, parsedURL *url.URL) error { func fetchHTTP(parsedURL *url.URL) error {
request, err := http.NewRequest("GET", parsedURL.String(), nil) request, err := http.NewRequest("GET", parsedURL.String(), nil)
if err != nil { if err != nil {
return err return err

View File

@@ -1,36 +0,0 @@
//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
}

View File

@@ -1,18 +0,0 @@
//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
}

View File

@@ -1,28 +0,0 @@
//go:build generate && generate_completions
package main
import "github.com/sagernet/sing-box/log"
func main() {
err := generateCompletions()
if err != nil {
log.Fatal(err)
}
}
func generateCompletions() error {
err := mainCommand.GenBashCompletionFile("release/completions/sing-box.bash")
if err != nil {
return err
}
err = mainCommand.GenFishCompletionFile("release/completions/sing-box.fish", true)
if err != nil {
return err
}
err = mainCommand.GenZshCompletionFile("release/completions/sing-box.zsh")
if err != nil {
return err
}
return nil
}

View File

@@ -1,346 +0,0 @@
package adguard
import (
"bufio"
"io"
"net/netip"
"os"
"strconv"
"strings"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
)
type agdguardRuleLine struct {
ruleLine string
isRawDomain bool
isExclude bool
isSuffix bool
hasStart bool
hasEnd bool
isRegexp bool
isImportant bool
}
func Convert(reader io.Reader) ([]option.HeadlessRule, error) {
scanner := bufio.NewScanner(reader)
var (
ruleLines []agdguardRuleLine
ignoredLines int
)
parseLine:
for scanner.Scan() {
ruleLine := scanner.Text()
if ruleLine == "" || ruleLine[0] == '!' || ruleLine[0] == '#' {
continue
}
originRuleLine := ruleLine
if M.IsDomainName(ruleLine) {
ruleLines = append(ruleLines, agdguardRuleLine{
ruleLine: ruleLine,
isRawDomain: true,
})
continue
}
hostLine, err := parseAdGuardHostLine(ruleLine)
if err == nil {
if hostLine != "" {
ruleLines = append(ruleLines, agdguardRuleLine{
ruleLine: hostLine,
isRawDomain: true,
hasStart: true,
hasEnd: true,
})
}
continue
}
if strings.HasSuffix(ruleLine, "|") {
ruleLine = ruleLine[:len(ruleLine)-1]
}
var (
isExclude bool
isSuffix bool
hasStart bool
hasEnd bool
isRegexp bool
isImportant bool
)
if !strings.HasPrefix(ruleLine, "/") && strings.Contains(ruleLine, "$") {
params := common.SubstringAfter(ruleLine, "$")
for _, param := range strings.Split(params, ",") {
paramParts := strings.Split(param, "=")
var ignored bool
if len(paramParts) > 0 && len(paramParts) <= 2 {
switch paramParts[0] {
case "app", "network":
// maybe support by package_name/process_name
case "dnstype":
// maybe support by query_type
case "important":
ignored = true
isImportant = true
case "dnsrewrite":
if len(paramParts) == 2 && M.ParseAddr(paramParts[1]).IsUnspecified() {
ignored = true
}
}
}
if !ignored {
ignoredLines++
log.Debug("ignored unsupported rule with modifier: ", paramParts[0], ": ", ruleLine)
continue parseLine
}
}
ruleLine = common.SubstringBefore(ruleLine, "$")
}
if strings.HasPrefix(ruleLine, "@@") {
ruleLine = ruleLine[2:]
isExclude = true
}
if strings.HasSuffix(ruleLine, "|") {
ruleLine = ruleLine[:len(ruleLine)-1]
}
if strings.HasPrefix(ruleLine, "||") {
ruleLine = ruleLine[2:]
isSuffix = true
} else if strings.HasPrefix(ruleLine, "|") {
ruleLine = ruleLine[1:]
hasStart = true
}
if strings.HasSuffix(ruleLine, "^") {
ruleLine = ruleLine[:len(ruleLine)-1]
hasEnd = true
}
if strings.HasPrefix(ruleLine, "/") && strings.HasSuffix(ruleLine, "/") {
ruleLine = ruleLine[1 : len(ruleLine)-1]
if ignoreIPCIDRRegexp(ruleLine) {
ignoredLines++
log.Debug("ignored unsupported rule with IPCIDR regexp: ", ruleLine)
continue
}
isRegexp = true
} else {
if strings.Contains(ruleLine, "://") {
ruleLine = common.SubstringAfter(ruleLine, "://")
}
if strings.Contains(ruleLine, "/") {
ignoredLines++
log.Debug("ignored unsupported rule with path: ", ruleLine)
continue
}
if strings.Contains(ruleLine, "##") {
ignoredLines++
log.Debug("ignored unsupported rule with element hiding: ", ruleLine)
continue
}
if strings.Contains(ruleLine, "#$#") {
ignoredLines++
log.Debug("ignored unsupported rule with element hiding: ", ruleLine)
continue
}
var domainCheck string
if strings.HasPrefix(ruleLine, ".") || strings.HasPrefix(ruleLine, "-") {
domainCheck = "r" + ruleLine
} else {
domainCheck = ruleLine
}
if ruleLine == "" {
ignoredLines++
log.Debug("ignored unsupported rule with empty domain", originRuleLine)
continue
} else {
domainCheck = strings.ReplaceAll(domainCheck, "*", "x")
if !M.IsDomainName(domainCheck) {
_, ipErr := parseADGuardIPCIDRLine(ruleLine)
if ipErr == nil {
ignoredLines++
log.Debug("ignored unsupported rule with IPCIDR: ", ruleLine)
continue
}
if M.ParseSocksaddr(domainCheck).Port != 0 {
log.Debug("ignored unsupported rule with port: ", ruleLine)
} else {
log.Debug("ignored unsupported rule with invalid domain: ", ruleLine)
}
ignoredLines++
continue
}
}
}
ruleLines = append(ruleLines, agdguardRuleLine{
ruleLine: ruleLine,
isExclude: isExclude,
isSuffix: isSuffix,
hasStart: hasStart,
hasEnd: hasEnd,
isRegexp: isRegexp,
isImportant: isImportant,
})
}
if len(ruleLines) == 0 {
return nil, E.New("AdGuard rule-set is empty or all rules are unsupported")
}
if common.All(ruleLines, func(it agdguardRuleLine) bool {
return it.isRawDomain
}) {
return []option.HeadlessRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultHeadlessRule{
Domain: common.Map(ruleLines, func(it agdguardRuleLine) string {
return it.ruleLine
}),
},
},
}, nil
}
mapDomain := func(it agdguardRuleLine) string {
ruleLine := it.ruleLine
if it.isSuffix {
ruleLine = "||" + ruleLine
} else if it.hasStart {
ruleLine = "|" + ruleLine
}
if it.hasEnd {
ruleLine += "^"
}
return ruleLine
}
importantDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && !it.isRegexp && !it.isExclude }), mapDomain)
importantDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && it.isRegexp && !it.isExclude }), mapDomain)
importantExcludeDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && !it.isRegexp && it.isExclude }), mapDomain)
importantExcludeDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && it.isRegexp && it.isExclude }), mapDomain)
domain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && !it.isRegexp && !it.isExclude }), mapDomain)
domainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && it.isRegexp && !it.isExclude }), mapDomain)
excludeDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && !it.isRegexp && it.isExclude }), mapDomain)
excludeDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && it.isRegexp && it.isExclude }), mapDomain)
currentRule := option.HeadlessRule{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultHeadlessRule{
AdGuardDomain: domain,
DomainRegex: domainRegex,
},
}
if len(excludeDomain) > 0 || len(excludeDomainRegex) > 0 {
currentRule = option.HeadlessRule{
Type: C.RuleTypeLogical,
LogicalOptions: option.LogicalHeadlessRule{
Mode: C.LogicalTypeAnd,
Rules: []option.HeadlessRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultHeadlessRule{
AdGuardDomain: excludeDomain,
DomainRegex: excludeDomainRegex,
Invert: true,
},
},
currentRule,
},
},
}
}
if len(importantDomain) > 0 || len(importantDomainRegex) > 0 {
currentRule = option.HeadlessRule{
Type: C.RuleTypeLogical,
LogicalOptions: option.LogicalHeadlessRule{
Mode: C.LogicalTypeOr,
Rules: []option.HeadlessRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultHeadlessRule{
AdGuardDomain: importantDomain,
DomainRegex: importantDomainRegex,
},
},
currentRule,
},
},
}
}
if len(importantExcludeDomain) > 0 || len(importantExcludeDomainRegex) > 0 {
currentRule = option.HeadlessRule{
Type: C.RuleTypeLogical,
LogicalOptions: option.LogicalHeadlessRule{
Mode: C.LogicalTypeAnd,
Rules: []option.HeadlessRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultHeadlessRule{
AdGuardDomain: importantExcludeDomain,
DomainRegex: importantExcludeDomainRegex,
Invert: true,
},
},
currentRule,
},
},
}
}
log.Info("parsed rules: ", len(ruleLines), "/", len(ruleLines)+ignoredLines)
return []option.HeadlessRule{currentRule}, nil
}
func ignoreIPCIDRRegexp(ruleLine string) bool {
if strings.HasPrefix(ruleLine, "(http?:\\/\\/)") {
ruleLine = ruleLine[12:]
} else if strings.HasPrefix(ruleLine, "(https?:\\/\\/)") {
ruleLine = ruleLine[13:]
} else if strings.HasPrefix(ruleLine, "^") {
ruleLine = ruleLine[1:]
} else {
return false
}
_, parseErr := strconv.ParseUint(common.SubstringBefore(ruleLine, "\\."), 10, 8)
return parseErr == nil
}
func parseAdGuardHostLine(ruleLine string) (string, error) {
idx := strings.Index(ruleLine, " ")
if idx == -1 {
return "", os.ErrInvalid
}
address, err := netip.ParseAddr(ruleLine[:idx])
if err != nil {
return "", err
}
if !address.IsUnspecified() {
return "", nil
}
domain := ruleLine[idx+1:]
if !M.IsDomainName(domain) {
return "", E.New("invalid domain name: ", domain)
}
return domain, nil
}
func parseADGuardIPCIDRLine(ruleLine string) (netip.Prefix, error) {
var isPrefix bool
if strings.HasSuffix(ruleLine, ".") {
isPrefix = true
ruleLine = ruleLine[:len(ruleLine)-1]
}
ruleStringParts := strings.Split(ruleLine, ".")
if len(ruleStringParts) > 4 || len(ruleStringParts) < 4 && !isPrefix {
return netip.Prefix{}, os.ErrInvalid
}
ruleParts := make([]uint8, 0, len(ruleStringParts))
for _, part := range ruleStringParts {
rulePart, err := strconv.ParseUint(part, 10, 8)
if err != nil {
return netip.Prefix{}, err
}
ruleParts = append(ruleParts, uint8(rulePart))
}
bitLen := len(ruleParts) * 8
for len(ruleParts) < 4 {
ruleParts = append(ruleParts, 0)
}
return netip.PrefixFrom(netip.AddrFrom4(*(*[4]byte)(ruleParts)), bitLen), nil
}

View File

@@ -1,140 +0,0 @@
package adguard
import (
"strings"
"testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/route/rule"
"github.com/stretchr/testify/require"
)
func TestConverter(t *testing.T) {
t.Parallel()
rules, err := Convert(strings.NewReader(`
||example.org^
|example.com^
example.net^
||example.edu
||example.edu.tw^
|example.gov
example.arpa
@@|sagernet.example.org|
||sagernet.org^$important
@@|sing-box.sagernet.org^$important
`))
require.NoError(t, err)
require.Len(t, rules, 1)
rule, err := rule.NewHeadlessRule(nil, rules[0])
require.NoError(t, err)
matchDomain := []string{
"example.org",
"www.example.org",
"example.com",
"example.net",
"isexample.net",
"www.example.net",
"example.edu",
"example.edu.cn",
"example.edu.tw",
"www.example.edu",
"www.example.edu.cn",
"example.gov",
"example.gov.cn",
"example.arpa",
"www.example.arpa",
"isexample.arpa",
"example.arpa.cn",
"www.example.arpa.cn",
"isexample.arpa.cn",
"sagernet.org",
"www.sagernet.org",
}
notMatchDomain := []string{
"example.org.cn",
"notexample.org",
"example.com.cn",
"www.example.com.cn",
"example.net.cn",
"notexample.edu",
"notexample.edu.cn",
"www.example.gov",
"notexample.gov",
"sagernet.example.org",
"sing-box.sagernet.org",
}
for _, domain := range matchDomain {
require.True(t, rule.Match(&adapter.InboundContext{
Domain: domain,
}), domain)
}
for _, domain := range notMatchDomain {
require.False(t, rule.Match(&adapter.InboundContext{
Domain: domain,
}), domain)
}
}
func TestHosts(t *testing.T) {
t.Parallel()
rules, err := Convert(strings.NewReader(`
127.0.0.1 localhost
::1 localhost #[IPv6]
0.0.0.0 google.com
`))
require.NoError(t, err)
require.Len(t, rules, 1)
rule, err := rule.NewHeadlessRule(nil, rules[0])
require.NoError(t, err)
matchDomain := []string{
"google.com",
}
notMatchDomain := []string{
"www.google.com",
"notgoogle.com",
"localhost",
}
for _, domain := range matchDomain {
require.True(t, rule.Match(&adapter.InboundContext{
Domain: domain,
}), domain)
}
for _, domain := range notMatchDomain {
require.False(t, rule.Match(&adapter.InboundContext{
Domain: domain,
}), domain)
}
}
func TestSimpleHosts(t *testing.T) {
t.Parallel()
rules, err := Convert(strings.NewReader(`
example.com
www.example.org
`))
require.NoError(t, err)
require.Len(t, rules, 1)
rule, err := rule.NewHeadlessRule(nil, rules[0])
require.NoError(t, err)
matchDomain := []string{
"example.com",
"www.example.org",
}
notMatchDomain := []string{
"example.com.cn",
"www.example.com",
"notexample.com",
"example.org",
}
for _, domain := range matchDomain {
require.True(t, rule.Match(&adapter.InboundContext{
Domain: domain,
}), domain)
}
for _, domain := range notMatchDomain {
require.False(t, rule.Match(&adapter.InboundContext{
Domain: domain,
}), domain)
}
}

View File

@@ -1,11 +1,74 @@
//go:build !generate
package main package main
import "github.com/sagernet/sing-box/log" import (
"context"
"os"
"os/user"
"strconv"
"time"
_ "github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/service/filemanager"
"github.com/spf13/cobra"
)
var (
globalCtx context.Context
configPaths []string
configDirectories []string
workingDir string
disableColor bool
)
var mainCommand = &cobra.Command{
Use: "sing-box",
PersistentPreRun: preRun,
}
func init() {
mainCommand.PersistentFlags().StringArrayVarP(&configPaths, "config", "c", nil, "set configuration file path")
mainCommand.PersistentFlags().StringArrayVarP(&configDirectories, "config-directory", "C", nil, "set configuration directory path")
mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
}
func main() { func main() {
if err := mainCommand.Execute(); err != nil { if err := mainCommand.Execute(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
func preRun(cmd *cobra.Command, args []string) {
globalCtx = context.Background()
sudoUser := os.Getenv("SUDO_USER")
sudoUID, _ := strconv.Atoi(os.Getenv("SUDO_UID"))
sudoGID, _ := strconv.Atoi(os.Getenv("SUDO_GID"))
if sudoUID == 0 && sudoGID == 0 && sudoUser != "" {
sudoUserObject, _ := user.Lookup(sudoUser)
if sudoUserObject != nil {
sudoUID, _ = strconv.Atoi(sudoUserObject.Uid)
sudoGID, _ = strconv.Atoi(sudoUserObject.Gid)
}
}
if sudoUID > 0 && sudoGID > 0 {
globalCtx = filemanager.WithDefault(globalCtx, "", "", sudoUID, sudoGID)
}
if disableColor {
log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger())
}
if workingDir != "" {
_, err := os.Stat(workingDir)
if err != nil {
filemanager.MkdirAll(globalCtx, workingDir, 0o777)
}
err = os.Chdir(workingDir)
if err != nil {
log.Fatal(err)
}
}
if len(configPaths) == 0 && len(configDirectories) == 0 {
configPaths = append(configPaths, "config.json")
}
}

View File

@@ -50,26 +50,12 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
dialer.Control = control.Append(dialer.Control, bindFunc) dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)
} }
var autoRedirectOutputMark uint32 if options.RoutingMark != 0 {
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)) dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark)) listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark))
if autoRedirectOutputMark > 0 { } else if router != nil && router.DefaultMark() != 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())) dialer.Control = control.Append(dialer.Control, control.RoutingMark(router.DefaultMark()))
listener.Control = control.Append(listener.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 { if options.ReuseAddr {
listener.Control = control.Append(listener.Control, control.ReuseAddr()) listener.Control = control.Append(listener.Control, control.ReuseAddr())
@@ -81,7 +67,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
if options.ConnectTimeout != 0 { if options.ConnectTimeout != 0 {
dialer.Timeout = time.Duration(options.ConnectTimeout) dialer.Timeout = time.Duration(options.ConnectTimeout)
} else { } else {
dialer.Timeout = C.TCPConnectTimeout dialer.Timeout = C.TCPTimeout
} }
// TODO: Add an option to customize the keep alive period // TODO: Add an option to customize the keep alive period
dialer.KeepAlive = C.TCPKeepAliveInitial dialer.KeepAlive = C.TCPKeepAliveInitial
@@ -125,7 +111,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
setMultiPathTCP(&dialer4) setMultiPathTCP(&dialer4)
} }
if options.IsWireGuardListener { if options.IsWireGuardListener {
for _, controlFn := range WgControlFns { for _, controlFn := range wgControlFns {
listener.Control = control.Append(listener.Control, controlFn) listener.Control = control.Append(listener.Control, controlFn)
} }
} }

View File

@@ -5,7 +5,7 @@ package dialer
import ( import (
"net" "net"
"github.com/metacubex/tfo-go" "github.com/sagernet/tfo-go"
) )
type tcpDialer = tfo.Dialer type tcpDialer = tfo.Dialer

View File

@@ -28,12 +28,13 @@ func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error)
} else { } else {
dialer = NewDetour(router, options.Detour) dialer = NewDetour(router, options.Detour)
} }
if options.Detour == "" { domainStrategy := dns.DomainStrategy(options.DomainStrategy)
if domainStrategy != dns.DomainStrategyAsIS || options.Detour == "" {
dialer = NewResolveDialer( dialer = NewResolveDialer(
router, router,
dialer, dialer,
options.Detour == "" && !options.TCPFastOpen, options.Detour == "" && !options.TCPFastOpen,
dns.DomainStrategy(options.DomainStrategy), domainStrategy,
time.Duration(options.FallbackDelay)) time.Duration(options.FallbackDelay))
} }
return dialer, nil return dialer, nil

View File

@@ -15,8 +15,7 @@ import (
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/tfo-go"
"github.com/metacubex/tfo-go"
) )
type slowOpenConn struct { type slowOpenConn struct {

View File

@@ -2,12 +2,8 @@ package dialer
import ( import (
"net" "net"
"github.com/sagernet/sing/common/control"
) )
type WireGuardListener interface { type WireGuardListener interface {
ListenPacketCompat(network, address string) (net.PacketConn, error) ListenPacketCompat(network, address string) (net.PacketConn, error)
} }
var WgControlFns []control.Func

View File

@@ -0,0 +1,11 @@
//go:build with_wireguard
package dialer
import (
"github.com/sagernet/wireguard-go/conn"
)
var _ WireGuardListener = (conn.Listener)(nil)
var wgControlFns = conn.ControlFns

View File

@@ -0,0 +1,9 @@
//go:build !with_wireguard
package dialer
import (
"github.com/sagernet/sing/common/control"
)
var wgControlFns []control.Func

View File

@@ -1,34 +0,0 @@
package geosite_test
import (
"bytes"
"testing"
"github.com/sagernet/sing-box/common/geosite"
"github.com/stretchr/testify/require"
)
func TestGeosite(t *testing.T) {
t.Parallel()
var buffer bytes.Buffer
err := geosite.Write(&buffer, map[string][]geosite.Item{
"test": {
{
Type: geosite.RuleTypeDomain,
Value: "example.org",
},
},
})
require.NoError(t, err)
reader, codes, err := geosite.NewReader(bytes.NewReader(buffer.Bytes()))
require.NoError(t, err)
require.Equal(t, []string{"test"}, codes)
items, err := reader.Read("test")
require.NoError(t, err)
require.Equal(t, []geosite.Item{{
Type: geosite.RuleTypeDomain,
Value: "example.org",
}}, items)
}

View File

@@ -1,24 +1,17 @@
package geosite package geosite
import ( import (
"bufio"
"encoding/binary"
"io" "io"
"os" "os"
"sync"
"sync/atomic"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin" "github.com/sagernet/sing/common/rw"
) )
type Reader struct { type Reader struct {
access sync.Mutex reader io.ReadSeeker
reader io.ReadSeeker domainIndex map[string]int
bufferedReader *bufio.Reader domainLength map[string]int
metadataIndex int64
domainIndex map[string]int
domainLength map[string]int
} }
func Open(path string) (*Reader, []string, error) { func Open(path string) (*Reader, []string, error) {
@@ -26,22 +19,14 @@ func Open(path string) (*Reader, []string, error) {
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
reader, codes, err := NewReader(content) reader := &Reader{
reader: content,
}
err = reader.readMetadata()
if err != nil { if err != nil {
content.Close() content.Close()
return nil, nil, err return nil, nil, err
} }
return reader, codes, nil
}
func NewReader(readSeeker io.ReadSeeker) (*Reader, []string, error) {
reader := &Reader{
reader: readSeeker,
}
err := reader.readMetadata()
if err != nil {
return nil, nil, err
}
codes := make([]string, 0, len(reader.domainIndex)) codes := make([]string, 0, len(reader.domainIndex))
for code := range reader.domainIndex { for code := range reader.domainIndex {
codes = append(codes, code) codes = append(codes, code)
@@ -49,23 +34,15 @@ func NewReader(readSeeker io.ReadSeeker) (*Reader, []string, error) {
return reader, codes, nil return reader, codes, nil
} }
type geositeMetadata struct {
Code string
Index uint64
Length uint64
}
func (r *Reader) readMetadata() error { func (r *Reader) readMetadata() error {
counter := &readCounter{Reader: r.reader} version, err := rw.ReadByte(r.reader)
reader := bufio.NewReader(counter)
version, err := reader.ReadByte()
if err != nil { if err != nil {
return err return err
} }
if version != 0 { if version != 0 {
return E.New("unknown version") return E.New("unknown version")
} }
entryLength, err := binary.ReadUvarint(reader) entryLength, err := rw.ReadUVariant(r.reader)
if err != nil { if err != nil {
return err return err
} }
@@ -78,16 +55,16 @@ func (r *Reader) readMetadata() error {
codeIndex uint64 codeIndex uint64
codeLength uint64 codeLength uint64
) )
code, err = varbin.ReadValue[string](reader, binary.BigEndian) code, err = rw.ReadVString(r.reader)
if err != nil { if err != nil {
return err return err
} }
keys[i] = code keys[i] = code
codeIndex, err = binary.ReadUvarint(reader) codeIndex, err = rw.ReadUVariant(r.reader)
if err != nil { if err != nil {
return err return err
} }
codeLength, err = binary.ReadUvarint(reader) codeLength, err = rw.ReadUVariant(r.reader)
if err != nil { if err != nil {
return err return err
} }
@@ -96,8 +73,6 @@ func (r *Reader) readMetadata() error {
} }
r.domainIndex = domainIndex r.domainIndex = domainIndex
r.domainLength = domainLength r.domainLength = domainLength
r.metadataIndex = counter.count - int64(reader.Buffered())
r.bufferedReader = reader
return nil return nil
} }
@@ -106,32 +81,31 @@ func (r *Reader) Read(code string) ([]Item, error) {
if !exists { if !exists {
return nil, E.New("code ", code, " not exists!") return nil, E.New("code ", code, " not exists!")
} }
_, err := r.reader.Seek(r.metadataIndex+int64(index), io.SeekStart) _, err := r.reader.Seek(int64(index), io.SeekCurrent)
if err != nil { if err != nil {
return nil, err return nil, err
} }
r.bufferedReader.Reset(r.reader) counter := &rw.ReadCounter{Reader: r.reader}
itemList := make([]Item, r.domainLength[code]) domain := make([]Item, r.domainLength[code])
err = varbin.Read(r.bufferedReader, binary.BigEndian, &itemList) for i := range domain {
if err != nil { var (
return nil, err 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
} }
return itemList, nil _, err = r.reader.Seek(int64(-index)-counter.Count(), io.SeekCurrent)
return domain, err
} }
func (r *Reader) Upstream() any { func (r *Reader) Upstream() any {
return r.reader 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
}

View File

@@ -2,13 +2,13 @@ package geosite
import ( import (
"bytes" "bytes"
"encoding/binary" "io"
"sort" "sort"
"github.com/sagernet/sing/common/varbin" "github.com/sagernet/sing/common/rw"
) )
func Write(writer varbin.Writer, domains map[string][]Item) error { func Write(writer io.Writer, domains map[string][]Item) error {
keys := make([]string, 0, len(domains)) keys := make([]string, 0, len(domains))
for code := range domains { for code := range domains {
keys = append(keys, code) keys = append(keys, code)
@@ -19,34 +19,35 @@ func Write(writer varbin.Writer, domains map[string][]Item) error {
index := make(map[string]int) index := make(map[string]int)
for _, code := range keys { for _, code := range keys {
index[code] = content.Len() index[code] = content.Len()
for _, item := range domains[code] { for _, domain := range domains[code] {
err := varbin.Write(content, binary.BigEndian, item) content.WriteByte(domain.Type)
err := rw.WriteVString(content, domain.Value)
if err != nil { if err != nil {
return err return err
} }
} }
} }
err := writer.WriteByte(0) err := rw.WriteByte(writer, 0)
if err != nil { if err != nil {
return err return err
} }
_, err = varbin.WriteUvarint(writer, uint64(len(keys))) err = rw.WriteUVariant(writer, uint64(len(keys)))
if err != nil { if err != nil {
return err return err
} }
for _, code := range keys { for _, code := range keys {
err = varbin.Write(writer, binary.BigEndian, code) err = rw.WriteVString(writer, code)
if err != nil { if err != nil {
return err return err
} }
_, err = varbin.WriteUvarint(writer, uint64(index[code])) err = rw.WriteUVariant(writer, uint64(index[code]))
if err != nil { if err != nil {
return err return err
} }
_, err = varbin.WriteUvarint(writer, uint64(len(domains[code]))) err = rw.WriteUVariant(writer, uint64(len(domains[code])))
if err != nil { if err != nil {
return err return err
} }

View File

@@ -1,29 +0,0 @@
BSD 3-Clause License
Copyright (c) 2018, Open Systems AG
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,3 +0,0 @@
# JA3
mod from: https://github.com/open-ch/ja3

View File

@@ -1,31 +0,0 @@
// Copyright (c) 2018, Open Systems AG. All rights reserved.
//
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file in the root of the source
// tree.
package ja3
import "fmt"
// Error types
const (
LengthErr string = "length check %v failed"
ContentTypeErr string = "content type not matching"
VersionErr string = "version check %v failed"
HandshakeTypeErr string = "handshake type not matching"
SNITypeErr string = "SNI type not supported"
)
// ParseError can be encountered while parsing a segment
type ParseError struct {
errType string
check int
}
func (e *ParseError) Error() string {
if e.errType == LengthErr || e.errType == VersionErr {
return fmt.Sprintf(e.errType, e.check)
}
return fmt.Sprint(e.errType)
}

View File

@@ -1,83 +0,0 @@
// Copyright (c) 2018, Open Systems AG. All rights reserved.
//
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file in the root of the source
// tree.
package ja3
import (
"crypto/md5"
"encoding/hex"
"golang.org/x/exp/slices"
)
type ClientHello struct {
Version uint16
CipherSuites []uint16
Extensions []uint16
EllipticCurves []uint16
EllipticCurvePF []uint8
Versions []uint16
SignatureAlgorithms []uint16
ServerName string
ja3ByteString []byte
ja3Hash string
}
func (j *ClientHello) Equals(another *ClientHello, ignoreExtensionsSequence bool) bool {
if j.Version != another.Version {
return false
}
if !slices.Equal(j.CipherSuites, another.CipherSuites) {
return false
}
if !ignoreExtensionsSequence && !slices.Equal(j.Extensions, another.Extensions) {
return false
}
if ignoreExtensionsSequence && !slices.Equal(j.Extensions, another.sortedExtensions()) {
return false
}
if !slices.Equal(j.EllipticCurves, another.EllipticCurves) {
return false
}
if !slices.Equal(j.EllipticCurvePF, another.EllipticCurvePF) {
return false
}
if !slices.Equal(j.SignatureAlgorithms, another.SignatureAlgorithms) {
return false
}
return true
}
func (j *ClientHello) sortedExtensions() []uint16 {
extensions := make([]uint16, len(j.Extensions))
copy(extensions, j.Extensions)
slices.Sort(extensions)
return extensions
}
func Compute(payload []byte) (*ClientHello, error) {
ja3 := ClientHello{}
err := ja3.parseSegment(payload)
return &ja3, err
}
func (j *ClientHello) String() string {
if j.ja3ByteString == nil {
j.marshalJA3()
}
return string(j.ja3ByteString)
}
func (j *ClientHello) Hash() string {
if j.ja3ByteString == nil {
j.marshalJA3()
}
if j.ja3Hash == "" {
h := md5.Sum(j.ja3ByteString)
j.ja3Hash = hex.EncodeToString(h[:])
}
return j.ja3Hash
}

View File

@@ -1,357 +0,0 @@
// Copyright (c) 2018, Open Systems AG. All rights reserved.
//
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file in the root of the source
// tree.
package ja3
import (
"encoding/binary"
"strconv"
)
const (
// Constants used for parsing
recordLayerHeaderLen int = 5
handshakeHeaderLen int = 6
randomDataLen int = 32
sessionIDHeaderLen int = 1
cipherSuiteHeaderLen int = 2
compressMethodHeaderLen int = 1
extensionsHeaderLen int = 2
extensionHeaderLen int = 4
sniExtensionHeaderLen int = 5
ecExtensionHeaderLen int = 2
ecpfExtensionHeaderLen int = 1
versionExtensionHeaderLen int = 1
signatureAlgorithmsExtensionHeaderLen int = 2
contentType uint8 = 22
handshakeType uint8 = 1
sniExtensionType uint16 = 0
sniNameDNSHostnameType uint8 = 0
ecExtensionType uint16 = 10
ecpfExtensionType uint16 = 11
versionExtensionType uint16 = 43
signatureAlgorithmsExtensionType uint16 = 13
// Versions
// The bitmask covers the versions SSL3.0 to TLS1.2
tlsVersionBitmask uint16 = 0xFFFC
tls13 uint16 = 0x0304
// GREASE values
// The bitmask covers all GREASE values
GreaseBitmask uint16 = 0x0F0F
// Constants used for marshalling
dashByte = byte(45)
commaByte = byte(44)
)
// parseSegment to populate the corresponding ClientHello object or return an error
func (j *ClientHello) parseSegment(segment []byte) error {
// Check if we can decode the next fields
if len(segment) < recordLayerHeaderLen {
return &ParseError{LengthErr, 1}
}
// Check if we have "Content Type: Handshake (22)"
contType := uint8(segment[0])
if contType != contentType {
return &ParseError{errType: ContentTypeErr}
}
// Check if TLS record layer version is supported
tlsRecordVersion := uint16(segment[1])<<8 | uint16(segment[2])
if tlsRecordVersion&tlsVersionBitmask != 0x0300 && tlsRecordVersion != tls13 {
return &ParseError{VersionErr, 1}
}
// Check that the Handshake is as long as expected from the length field
segmentLen := uint16(segment[3])<<8 | uint16(segment[4])
if len(segment[recordLayerHeaderLen:]) < int(segmentLen) {
return &ParseError{LengthErr, 2}
}
// Keep the Handshake messege, ignore any additional following record types
hs := segment[recordLayerHeaderLen : recordLayerHeaderLen+int(segmentLen)]
err := j.parseHandshake(hs)
return err
}
// parseHandshake body
func (j *ClientHello) parseHandshake(hs []byte) error {
// Check if we can decode the next fields
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen {
return &ParseError{LengthErr, 3}
}
// Check if we have "Handshake Type: Client Hello (1)"
handshType := uint8(hs[0])
if handshType != handshakeType {
return &ParseError{errType: HandshakeTypeErr}
}
// Check if actual length of handshake matches (this is a great exclusion criterion for false positives,
// as these fields have to match the actual length of the rest of the segment)
handshakeLen := uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3])
if len(hs[4:]) != int(handshakeLen) {
return &ParseError{LengthErr, 4}
}
// Check if Client Hello version is supported
tlsVersion := uint16(hs[4])<<8 | uint16(hs[5])
if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 {
return &ParseError{VersionErr, 2}
}
j.Version = tlsVersion
// Check if we can decode the next fields
sessionIDLen := uint8(hs[38])
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen) {
return &ParseError{LengthErr, 5}
}
// Cipher Suites
cs := hs[handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen):]
// Check if we can decode the next fields
if len(cs) < cipherSuiteHeaderLen {
return &ParseError{LengthErr, 6}
}
csLen := uint16(cs[0])<<8 | uint16(cs[1])
numCiphers := int(csLen / 2)
cipherSuites := make([]uint16, 0, numCiphers)
// Check if we can decode the next fields
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen {
return &ParseError{LengthErr, 7}
}
for i := 0; i < numCiphers; i++ {
cipherSuite := uint16(cs[2+i<<1])<<8 | uint16(cs[3+i<<1])
cipherSuites = append(cipherSuites, cipherSuite)
}
j.CipherSuites = cipherSuites
// Check if we can decode the next fields
compressMethodLen := uint16(cs[cipherSuiteHeaderLen+int(csLen)])
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen) {
return &ParseError{LengthErr, 8}
}
// Extensions
exs := cs[cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen):]
err := j.parseExtensions(exs)
return err
}
// parseExtensions of the handshake
func (j *ClientHello) parseExtensions(exs []byte) error {
// Check for no extensions, this fields header is nonexistent if no body is used
if len(exs) == 0 {
return nil
}
// Check if we can decode the next fields
if len(exs) < extensionsHeaderLen {
return &ParseError{LengthErr, 9}
}
exsLen := uint16(exs[0])<<8 | uint16(exs[1])
exs = exs[extensionsHeaderLen:]
// Check if we can decode the next fields
if len(exs) < int(exsLen) {
return &ParseError{LengthErr, 10}
}
var sni []byte
var extensions, ellipticCurves []uint16
var ellipticCurvePF []uint8
var versions []uint16
var signatureAlgorithms []uint16
for len(exs) > 0 {
// Check if we can decode the next fields
if len(exs) < extensionHeaderLen {
return &ParseError{LengthErr, 11}
}
exType := uint16(exs[0])<<8 | uint16(exs[1])
exLen := uint16(exs[2])<<8 | uint16(exs[3])
// Ignore any GREASE extensions
extensions = append(extensions, exType)
// Check if we can decode the next fields
if len(exs) < extensionHeaderLen+int(exLen) {
return &ParseError{LengthErr, 12}
}
sex := exs[extensionHeaderLen : extensionHeaderLen+int(exLen)]
switch exType {
case sniExtensionType: // Extensions: server_name
// Check if we can decode the next fields
if len(sex) < sniExtensionHeaderLen {
return &ParseError{LengthErr, 13}
}
sniType := uint8(sex[2])
sniLen := uint16(sex[3])<<8 | uint16(sex[4])
sex = sex[sniExtensionHeaderLen:]
// Check if we can decode the next fields
if len(sex) != int(sniLen) {
return &ParseError{LengthErr, 14}
}
switch sniType {
case sniNameDNSHostnameType:
sni = sex
default:
return &ParseError{errType: SNITypeErr}
}
case ecExtensionType: // Extensions: supported_groups
// Check if we can decode the next fields
if len(sex) < ecExtensionHeaderLen {
return &ParseError{LengthErr, 15}
}
ecsLen := uint16(sex[0])<<8 | uint16(sex[1])
numCurves := int(ecsLen / 2)
ellipticCurves = make([]uint16, 0, numCurves)
sex = sex[ecExtensionHeaderLen:]
// Check if we can decode the next fields
if len(sex) != int(ecsLen) {
return &ParseError{LengthErr, 16}
}
for i := 0; i < numCurves; i++ {
ecType := uint16(sex[i*2])<<8 | uint16(sex[1+i*2])
ellipticCurves = append(ellipticCurves, ecType)
}
case ecpfExtensionType: // Extensions: ec_point_formats
// Check if we can decode the next fields
if len(sex) < ecpfExtensionHeaderLen {
return &ParseError{LengthErr, 17}
}
ecpfsLen := uint8(sex[0])
numPF := int(ecpfsLen)
ellipticCurvePF = make([]uint8, numPF)
sex = sex[ecpfExtensionHeaderLen:]
// Check if we can decode the next fields
if len(sex) != numPF {
return &ParseError{LengthErr, 18}
}
for i := 0; i < numPF; i++ {
ellipticCurvePF[i] = uint8(sex[i])
}
case versionExtensionType:
if len(sex) < versionExtensionHeaderLen {
return &ParseError{LengthErr, 19}
}
versionsLen := int(sex[0])
for i := 0; i < versionsLen; i += 2 {
versions = append(versions, binary.BigEndian.Uint16(sex[1:][i:]))
}
case signatureAlgorithmsExtensionType:
if len(sex) < signatureAlgorithmsExtensionHeaderLen {
return &ParseError{LengthErr, 20}
}
ssaLen := binary.BigEndian.Uint16(sex)
for i := 0; i < int(ssaLen); i += 2 {
signatureAlgorithms = append(signatureAlgorithms, binary.BigEndian.Uint16(sex[2:][i:]))
}
}
exs = exs[4+exLen:]
}
j.ServerName = string(sni)
j.Extensions = extensions
j.EllipticCurves = ellipticCurves
j.EllipticCurvePF = ellipticCurvePF
j.Versions = versions
j.SignatureAlgorithms = signatureAlgorithms
return nil
}
// marshalJA3 into a byte string
func (j *ClientHello) marshalJA3() {
// An uint16 can contain numbers with up to 5 digits and an uint8 can contain numbers with up to 3 digits, but we
// also need a byte for each separating character, except at the end.
byteStringLen := 6*(1+len(j.CipherSuites)+len(j.Extensions)+len(j.EllipticCurves)) + 4*len(j.EllipticCurvePF) - 1
byteString := make([]byte, 0, byteStringLen)
// Version
byteString = strconv.AppendUint(byteString, uint64(j.Version), 10)
byteString = append(byteString, commaByte)
// Cipher Suites
if len(j.CipherSuites) != 0 {
for _, val := range j.CipherSuites {
if val&GreaseBitmask != 0x0A0A {
continue
}
byteString = strconv.AppendUint(byteString, uint64(val), 10)
byteString = append(byteString, dashByte)
}
// Replace last dash with a comma
byteString[len(byteString)-1] = commaByte
} else {
byteString = append(byteString, commaByte)
}
// Extensions
if len(j.Extensions) != 0 {
for _, val := range j.Extensions {
if val&GreaseBitmask != 0x0A0A {
continue
}
byteString = strconv.AppendUint(byteString, uint64(val), 10)
byteString = append(byteString, dashByte)
}
// Replace last dash with a comma
byteString[len(byteString)-1] = commaByte
} else {
byteString = append(byteString, commaByte)
}
// Elliptic curves
if len(j.EllipticCurves) != 0 {
for _, val := range j.EllipticCurves {
if val&GreaseBitmask != 0x0A0A {
continue
}
byteString = strconv.AppendUint(byteString, uint64(val), 10)
byteString = append(byteString, dashByte)
}
// Replace last dash with a comma
byteString[len(byteString)-1] = commaByte
} else {
byteString = append(byteString, commaByte)
}
// ECPF
if len(j.EllipticCurvePF) != 0 {
for _, val := range j.EllipticCurvePF {
byteString = strconv.AppendUint(byteString, uint64(val), 10)
byteString = append(byteString, dashByte)
}
// Remove last dash
byteString = byteString[:len(byteString)-1]
}
j.ja3ByteString = byteString
}

View File

@@ -1,136 +0,0 @@
package listener
import (
"context"
"net"
"sync/atomic"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/settings"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type Listener struct {
ctx context.Context
logger logger.ContextLogger
network []string
listenOptions option.ListenOptions
connHandler adapter.ConnectionHandlerEx
packetHandler adapter.PacketHandlerEx
oobPacketHandler adapter.OOBPacketHandlerEx
threadUnsafePacketWriter bool
disablePacketOutput bool
setSystemProxy bool
systemProxySOCKS bool
tcpListener net.Listener
systemProxy settings.SystemProxy
udpConn *net.UDPConn
udpAddr M.Socksaddr
packetOutbound chan *N.PacketBuffer
packetOutboundClosed chan struct{}
shutdown atomic.Bool
}
type Options struct {
Context context.Context
Logger logger.ContextLogger
Network []string
Listen option.ListenOptions
ConnectionHandler adapter.ConnectionHandlerEx
PacketHandler adapter.PacketHandlerEx
OOBPacketHandler adapter.OOBPacketHandlerEx
ThreadUnsafePacketWriter bool
DisablePacketOutput bool
SetSystemProxy bool
SystemProxySOCKS bool
}
func New(
options Options,
) *Listener {
return &Listener{
ctx: options.Context,
logger: options.Logger,
network: options.Network,
listenOptions: options.Listen,
connHandler: options.ConnectionHandler,
packetHandler: options.PacketHandler,
oobPacketHandler: options.OOBPacketHandler,
threadUnsafePacketWriter: options.ThreadUnsafePacketWriter,
disablePacketOutput: options.DisablePacketOutput,
setSystemProxy: options.SetSystemProxy,
systemProxySOCKS: options.SystemProxySOCKS,
}
}
func (l *Listener) Start() error {
if common.Contains(l.network, N.NetworkTCP) {
_, err := l.ListenTCP()
if err != nil {
return err
}
go l.loopTCPIn()
}
if common.Contains(l.network, N.NetworkUDP) {
_, err := l.ListenUDP()
if err != nil {
return err
}
l.packetOutboundClosed = make(chan struct{})
l.packetOutbound = make(chan *N.PacketBuffer, 64)
go l.loopUDPIn()
if !l.disablePacketOutput {
go l.loopUDPOut()
}
}
if l.setSystemProxy {
listenPort := M.SocksaddrFromNet(l.tcpListener.Addr()).Port
var listenAddrString string
listenAddr := l.listenOptions.Listen.Build()
if listenAddr.IsUnspecified() {
listenAddrString = "127.0.0.1"
} else {
listenAddrString = listenAddr.String()
}
systemProxy, err := settings.NewSystemProxy(l.ctx, M.ParseSocksaddrHostPort(listenAddrString, listenPort), l.systemProxySOCKS)
if err != nil {
return E.Cause(err, "initialize system proxy")
}
err = systemProxy.Enable()
if err != nil {
return E.Cause(err, "set system proxy")
}
l.systemProxy = systemProxy
}
return nil
}
func (l *Listener) Close() error {
l.shutdown.Store(true)
var err error
if l.systemProxy != nil && l.systemProxy.IsEnabled() {
err = l.systemProxy.Disable()
}
return E.Errors(err, common.Close(
l.tcpListener,
common.PtrOrNil(l.udpConn),
))
}
func (l *Listener) TCPListener() net.Listener {
return l.tcpListener
}
func (l *Listener) UDPConn() *net.UDPConn {
return l.udpConn
}
func (l *Listener) ListenOptions() option.ListenOptions {
return l.listenOptions
}

View File

@@ -1,16 +0,0 @@
//go:build go1.23
package listener
import (
"net"
"time"
)
func setKeepAliveConfig(listener *net.ListenConfig, idle time.Duration, interval time.Duration) {
listener.KeepAliveConfig = net.KeepAliveConfig{
Enable: true,
Idle: idle,
Interval: interval,
}
}

View File

@@ -1,15 +0,0 @@
//go:build !go1.23
package listener
import (
"net"
"time"
"github.com/sagernet/sing/common/control"
)
func setKeepAliveConfig(listener *net.ListenConfig, idle time.Duration, interval time.Duration) {
listener.KeepAlive = idle
listener.Control = control.Append(listener.Control, control.SetKeepAlivePeriod(idle, interval))
}

View File

@@ -1,85 +0,0 @@
package listener
import (
"net"
"time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
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"
)
func (l *Listener) ListenTCP() (net.Listener, error) {
var err error
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(), l.listenOptions.ListenPort)
var tcpListener net.Listener
var listenConfig net.ListenConfig
if l.listenOptions.TCPKeepAlive >= 0 {
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
if keepIdle == 0 {
keepIdle = C.TCPKeepAliveInitial
}
keepInterval := time.Duration(l.listenOptions.TCPKeepAliveInterval)
if keepInterval == 0 {
keepInterval = C.TCPKeepAliveInterval
}
setKeepAliveConfig(&listenConfig, keepIdle, keepInterval)
}
if l.listenOptions.TCPMultiPath {
if !go121Available {
return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.")
}
setMultiPathTCP(&listenConfig)
}
if l.listenOptions.TCPFastOpen {
var tfoConfig tfo.ListenConfig
tfoConfig.ListenConfig = listenConfig
tcpListener, err = tfoConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
} else {
tcpListener, err = listenConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
}
if err == nil {
l.logger.Info("tcp server started at ", tcpListener.Addr())
}
//nolint:staticcheck
if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader {
return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0")
}
l.tcpListener = tcpListener
return tcpListener, err
}
func (l *Listener) loopTCPIn() {
tcpListener := l.tcpListener
var metadata adapter.InboundContext
for {
conn, err := tcpListener.Accept()
if err != nil {
//nolint:staticcheck
if netError, isNetError := err.(net.Error); isNetError && netError.Temporary() {
l.logger.Error(err)
continue
}
if l.shutdown.Load() && E.IsClosed(err) {
return
}
l.tcpListener.Close()
l.logger.Error("tcp listener closed: ", err)
continue
}
//nolint:staticcheck
metadata.InboundDetour = l.listenOptions.Detour
//nolint:staticcheck
metadata.InboundOptions = l.listenOptions.InboundOptions
metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap()
metadata.OriginDestination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap()
ctx := log.ContextWithNewID(l.ctx)
l.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
go l.connHandler.NewConnectionEx(ctx, conn, metadata, nil)
}
}

View File

@@ -1,154 +0,0 @@
package listener
import (
"net"
"os"
"time"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func (l *Listener) ListenUDP() (net.PacketConn, error) {
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(), l.listenOptions.ListenPort)
var lc net.ListenConfig
var udpFragment bool
if l.listenOptions.UDPFragment != nil {
udpFragment = *l.listenOptions.UDPFragment
} else {
udpFragment = l.listenOptions.UDPFragmentDefault
}
if !udpFragment {
lc.Control = control.Append(lc.Control, control.DisableUDPFragment())
}
udpConn, err := lc.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String())
if err != nil {
return nil, err
}
l.udpConn = udpConn.(*net.UDPConn)
l.udpAddr = bindAddr
l.logger.Info("udp server started at ", udpConn.LocalAddr())
return udpConn, err
}
func (l *Listener) UDPAddr() M.Socksaddr {
return l.udpAddr
}
func (l *Listener) PacketWriter() N.PacketWriter {
return (*packetWriter)(l)
}
func (l *Listener) loopUDPIn() {
defer close(l.packetOutboundClosed)
var buffer *buf.Buffer
if !l.threadUnsafePacketWriter {
buffer = buf.NewPacket()
defer buffer.Release()
}
buffer.IncRef()
defer buffer.DecRef()
if l.oobPacketHandler != nil {
oob := make([]byte, 1024)
for {
if l.threadUnsafePacketWriter {
buffer = buf.NewPacket()
} else {
buffer.Reset()
}
n, oobN, _, addr, err := l.udpConn.ReadMsgUDPAddrPort(buffer.FreeBytes(), oob)
if err != nil {
if l.threadUnsafePacketWriter {
buffer.Release()
}
if l.shutdown.Load() && E.IsClosed(err) {
return
}
l.udpConn.Close()
l.logger.Error("udp listener closed: ", err)
return
}
buffer.Truncate(n)
l.oobPacketHandler.NewPacketEx(buffer, oob[:oobN], M.SocksaddrFromNetIP(addr).Unwrap())
}
} else {
for {
if l.threadUnsafePacketWriter {
buffer = buf.NewPacket()
} else {
buffer.Reset()
}
n, addr, err := l.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes())
if err != nil {
if l.threadUnsafePacketWriter {
buffer.Release()
}
if l.shutdown.Load() && E.IsClosed(err) {
return
}
l.udpConn.Close()
l.logger.Error("udp listener closed: ", err)
return
}
buffer.Truncate(n)
l.packetHandler.NewPacketEx(buffer, M.SocksaddrFromNetIP(addr).Unwrap())
}
}
}
func (l *Listener) loopUDPOut() {
for {
select {
case packet := <-l.packetOutbound:
destination := packet.Destination.AddrPort()
_, err := l.udpConn.WriteToUDPAddrPort(packet.Buffer.Bytes(), destination)
packet.Buffer.Release()
N.PutPacketBuffer(packet)
if err != nil {
if l.shutdown.Load() && E.IsClosed(err) {
return
}
l.udpConn.Close()
l.logger.Error("udp listener write back: ", destination, ": ", err)
return
}
continue
case <-l.packetOutboundClosed:
}
for {
select {
case packet := <-l.packetOutbound:
packet.Buffer.Release()
N.PutPacketBuffer(packet)
case <-time.After(time.Second):
return
}
}
}
}
type packetWriter Listener
func (w *packetWriter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
packet := N.NewPacketBuffer()
packet.Buffer = buffer
packet.Destination = destination
select {
case w.packetOutbound <- packet:
return nil
default:
buffer.Release()
N.PutPacketBuffer(packet)
if w.shutdown.Load() {
return os.ErrClosed
}
w.logger.Trace("dropped packet to ", destination)
return nil
}
}
func (w *packetWriter) WriteIsThreadUnsafe() {
}

View File

@@ -15,11 +15,11 @@ import (
) )
type Router struct { type Router struct {
router adapter.ConnectionRouterEx router adapter.ConnectionRouter
service *mux.Service service *mux.Service
} }
func NewRouterWithOptions(router adapter.ConnectionRouterEx, logger logger.ContextLogger, options option.InboundMultiplexOptions) (adapter.ConnectionRouterEx, error) { func NewRouterWithOptions(router adapter.ConnectionRouter, logger logger.ContextLogger, options option.InboundMultiplexOptions) (adapter.ConnectionRouter, error) {
if !options.Enabled { if !options.Enabled {
return router, nil return router, nil
} }
@@ -54,7 +54,6 @@ func NewRouterWithOptions(router adapter.ConnectionRouterEx, logger logger.Conte
func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
if metadata.Destination == mux.Destination { if metadata.Destination == mux.Destination {
// TODO: check if WithContext is necessary
return r.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, adapter.UpstreamMetadata(metadata)) return r.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, adapter.UpstreamMetadata(metadata))
} else { } else {
return r.router.RouteConnection(ctx, conn, metadata) return r.router.RouteConnection(ctx, conn, metadata)
@@ -64,15 +63,3 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
return r.router.RoutePacketConnection(ctx, conn, metadata) return r.router.RoutePacketConnection(ctx, conn, metadata)
} }
func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
if metadata.Destination == mux.Destination {
r.service.NewConnectionEx(adapter.WithContext(ctx, &metadata), conn, metadata.Source, metadata.Destination, onClose)
return
}
r.router.RouteConnectionEx(ctx, conn, metadata, onClose)
}
func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
r.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
}

View File

@@ -0,0 +1,32 @@
package mux
import (
"context"
"net"
"github.com/sagernet/sing-box/adapter"
vmess "github.com/sagernet/sing-vmess"
"github.com/sagernet/sing/common/logger"
N "github.com/sagernet/sing/common/network"
)
type V2RayLegacyRouter struct {
router adapter.ConnectionRouter
logger logger.ContextLogger
}
func NewV2RayLegacyRouter(router adapter.ConnectionRouter, logger logger.ContextLogger) adapter.ConnectionRouter {
return &V2RayLegacyRouter{router, logger}
}
func (r *V2RayLegacyRouter) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
if metadata.Destination.Fqdn == vmess.MuxDestination.Fqdn {
r.logger.InfoContext(ctx, "inbound legacy multiplex connection")
return vmess.HandleMuxConnection(ctx, conn, adapter.NewRouteHandler(metadata, r.router, r.logger))
}
return r.router.RouteConnection(ctx, conn, metadata)
}
func (r *V2RayLegacyRouter) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
return r.router.RoutePacketConnection(ctx, conn, metadata)
}

View File

@@ -60,12 +60,12 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
isIPv4 := ip.Is4() isIPv4 := ip.Is4()
value, err := unix.SysctlRaw(spath) value, err := syscall.Sysctl(spath)
if err != nil { if err != nil {
return "", err return "", err
} }
buf := value buf := []byte(value)
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n // from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
// size/offset are round up (aligned) to 8 bytes in darwin // size/offset are round up (aligned) to 8 bytes in darwin

View File

@@ -1,99 +0,0 @@
package sniff
import (
"bytes"
"context"
"encoding/binary"
"io"
"os"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
)
const (
trackerConnectFlag = 0
trackerProtocolID = 0x41727101980
trackerConnectMinSize = 16
)
// 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, metadata *adapter.InboundContext, reader io.Reader) error {
var first byte
err := binary.Read(reader, binary.BigEndian, &first)
if err != nil {
return err
}
if first != 19 {
return os.ErrInvalid
}
var protocol [19]byte
_, err = reader.Read(protocol[:])
if err != nil {
return err
}
if string(protocol[:]) != "BitTorrent protocol" {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolBitTorrent
return 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, metadata *adapter.InboundContext, packet []byte) error {
// A valid uTP packet must be at least 20 bytes long.
if len(packet) < 20 {
return os.ErrInvalid
}
version := packet[0] & 0x0F
ty := packet[0] >> 4
if version != 1 || ty > 4 {
return 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 err
}
var length byte
err = binary.Read(reader, binary.BigEndian, &length)
if err != nil {
return err
}
_, err = reader.Seek(int64(length), io.SeekCurrent)
if err != nil {
return err
}
}
metadata.Protocol = C.ProtocolBitTorrent
return 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, metadata *adapter.InboundContext, packet []byte) error {
if len(packet) < trackerConnectMinSize {
return os.ErrInvalid
}
if binary.BigEndian.Uint64(packet[:8]) != trackerProtocolID {
return os.ErrInvalid
}
if binary.BigEndian.Uint32(packet[8:12]) != trackerConnectFlag {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolBitTorrent
return nil
}

View File

@@ -1,73 +0,0 @@
package sniff_test
import (
"bytes"
"context"
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/adapter"
"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)
var metadata adapter.InboundContext
err = sniff.BitTorrent(context.TODO(), &metadata, 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)
var metadata adapter.InboundContext
err = sniff.UTP(context.TODO(), &metadata, pkt)
require.NoError(t, err)
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
}
}
func TestSniffUDPTracker(t *testing.T) {
t.Parallel()
connectPackets := []string{
"00000417271019800000000078e90560",
"00000417271019800000000022c5d64d",
"000004172710198000000000b3863541",
}
for _, pkt := range connectPackets {
pkt, err := hex.DecodeString(pkt)
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.UDPTracker(context.TODO(), &metadata, pkt)
require.NoError(t, err)
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
}
}

View File

@@ -17,17 +17,18 @@ import (
mDNS "github.com/miekg/dns" mDNS "github.com/miekg/dns"
) )
func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundContext, reader io.Reader) error { func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.InboundContext, error) {
var length uint16 var length uint16
err := binary.Read(reader, binary.BigEndian, &length) err := binary.Read(reader, binary.BigEndian, &length)
if err != nil { if err != nil {
return os.ErrInvalid return nil, err
} }
if length == 0 { if length == 0 {
return os.ErrInvalid return nil, os.ErrInvalid
} }
buffer := buf.NewSize(int(length)) buffer := buf.NewSize(int(length))
defer buffer.Release() defer buffer.Release()
readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100) readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100)
var readTask task.Group var readTask task.Group
readTask.Append0(func(ctx context.Context) error { readTask.Append0(func(ctx context.Context) error {
@@ -36,20 +37,19 @@ func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundCon
err = readTask.Run(readCtx) err = readTask.Run(readCtx)
cancel() cancel()
if err != nil { if err != nil {
return err return nil, err
} }
return DomainNameQuery(readCtx, metadata, buffer.Bytes()) return DomainNameQuery(readCtx, buffer.Bytes())
} }
func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { func DomainNameQuery(ctx context.Context, packet []byte) (*adapter.InboundContext, error) {
var msg mDNS.Msg var msg mDNS.Msg
err := msg.Unpack(packet) err := msg.Unpack(packet)
if err != nil { if err != nil {
return err return nil, err
} }
if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) { if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) {
return os.ErrInvalid return nil, os.ErrInvalid
} }
metadata.Protocol = C.ProtocolDNS return &adapter.InboundContext{Protocol: C.ProtocolDNS}, nil
return nil
} }

View File

@@ -1,32 +0,0 @@
package sniff
import (
"context"
"os"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
)
func DTLSRecord(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
const fixedHeaderSize = 13
if len(packet) < fixedHeaderSize {
return os.ErrInvalid
}
contentType := packet[0]
switch contentType {
case 20, 21, 22, 23, 25:
default:
return os.ErrInvalid
}
versionMajor := packet[1]
if versionMajor != 0xfe {
return os.ErrInvalid
}
versionMinor := packet[2]
if versionMinor != 0xff && versionMinor != 0xfd {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolDTLS
return nil
}

View File

@@ -1,33 +0,0 @@
package sniff_test
import (
"context"
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/adapter"
"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)
var metadata adapter.InboundContext
err = sniff.DTLSRecord(context.Background(), &metadata, 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)
var metadata adapter.InboundContext
err = sniff.DTLSRecord(context.Background(), &metadata, packet)
require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolDTLS)
}

View File

@@ -11,12 +11,10 @@ import (
"github.com/sagernet/sing/protocol/http" "github.com/sagernet/sing/protocol/http"
) )
func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { func HTTPHost(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error) {
request, err := http.ReadRequest(std_bufio.NewReader(reader)) request, err := http.ReadRequest(std_bufio.NewReader(reader))
if err != nil { if err != nil {
return err return nil, err
} }
metadata.Protocol = C.ProtocolHTTP return &adapter.InboundContext{Protocol: C.ProtocolHTTP, Domain: M.ParseSocksaddr(request.Host).AddrString()}, nil
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
return nil
} }

View File

@@ -5,7 +5,6 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff" "github.com/sagernet/sing-box/common/sniff"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -14,8 +13,7 @@ import (
func TestSniffHTTP1(t *testing.T) { func TestSniffHTTP1(t *testing.T) {
t.Parallel() t.Parallel()
pkt := "GET / HTTP/1.1\r\nHost: www.google.com\r\nAccept: */*\r\n\r\n" pkt := "GET / HTTP/1.1\r\nHost: www.google.com\r\nAccept: */*\r\n\r\n"
var metadata adapter.InboundContext metadata, err := sniff.HTTPHost(context.Background(), strings.NewReader(pkt))
err := sniff.HTTPHost(context.Background(), &metadata, strings.NewReader(pkt))
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, metadata.Domain, "www.google.com") require.Equal(t, metadata.Domain, "www.google.com")
} }
@@ -23,8 +21,7 @@ func TestSniffHTTP1(t *testing.T) {
func TestSniffHTTP1WithPort(t *testing.T) { func TestSniffHTTP1WithPort(t *testing.T) {
t.Parallel() t.Parallel()
pkt := "GET / HTTP/1.1\r\nHost: www.gov.cn:8080\r\nAccept: */*\r\n\r\n" pkt := "GET / HTTP/1.1\r\nHost: www.gov.cn:8080\r\nAccept: */*\r\n\r\n"
var metadata adapter.InboundContext metadata, err := sniff.HTTPHost(context.Background(), strings.NewReader(pkt))
err := sniff.HTTPHost(context.Background(), &metadata, strings.NewReader(pkt))
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, metadata.Domain, "www.gov.cn") require.Equal(t, metadata.Domain, "www.gov.cn")
} }

View File

@@ -5,99 +5,95 @@ import (
"context" "context"
"crypto" "crypto"
"crypto/aes" "crypto/aes"
"crypto/tls"
"encoding/binary" "encoding/binary"
"io" "io"
"os" "os"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/ja3"
"github.com/sagernet/sing-box/common/sniff/internal/qtls" "github.com/sagernet/sing-box/common/sniff/internal/qtls"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/crypto/hkdf" "golang.org/x/crypto/hkdf"
) )
var ErrClientHelloFragmented = E.New("need more packet for chromium QUIC connection") func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContext, error) {
func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
reader := bytes.NewReader(packet) reader := bytes.NewReader(packet)
typeByte, err := reader.ReadByte() typeByte, err := reader.ReadByte()
if err != nil { if err != nil {
return err return nil, err
} }
if typeByte&0x40 == 0 { if typeByte&0x40 == 0 {
return E.New("bad type byte") return nil, E.New("bad type byte")
} }
var versionNumber uint32 var versionNumber uint32
err = binary.Read(reader, binary.BigEndian, &versionNumber) err = binary.Read(reader, binary.BigEndian, &versionNumber)
if err != nil { if err != nil {
return err return nil, err
} }
if versionNumber != qtls.VersionDraft29 && versionNumber != qtls.Version1 && versionNumber != qtls.Version2 { if versionNumber != qtls.VersionDraft29 && versionNumber != qtls.Version1 && versionNumber != qtls.Version2 {
return E.New("bad version") return nil, E.New("bad version")
} }
packetType := (typeByte & 0x30) >> 4 packetType := (typeByte & 0x30) >> 4
if packetType == 0 && versionNumber == qtls.Version2 || packetType == 2 && versionNumber != qtls.Version2 || packetType > 2 { if packetType == 0 && versionNumber == qtls.Version2 || packetType == 2 && versionNumber != qtls.Version2 || packetType > 2 {
return E.New("bad packet type") return nil, E.New("bad packet type")
} }
destConnIDLen, err := reader.ReadByte() destConnIDLen, err := reader.ReadByte()
if err != nil { if err != nil {
return err return nil, err
} }
if destConnIDLen == 0 || destConnIDLen > 20 { if destConnIDLen == 0 || destConnIDLen > 20 {
return E.New("bad destination connection id length") return nil, E.New("bad destination connection id length")
} }
destConnID := make([]byte, destConnIDLen) destConnID := make([]byte, destConnIDLen)
_, err = io.ReadFull(reader, destConnID) _, err = io.ReadFull(reader, destConnID)
if err != nil { if err != nil {
return err return nil, err
} }
srcConnIDLen, err := reader.ReadByte() srcConnIDLen, err := reader.ReadByte()
if err != nil { if err != nil {
return err return nil, err
} }
_, err = io.CopyN(io.Discard, reader, int64(srcConnIDLen)) _, err = io.CopyN(io.Discard, reader, int64(srcConnIDLen))
if err != nil { if err != nil {
return err return nil, err
} }
tokenLen, err := qtls.ReadUvarint(reader) tokenLen, err := qtls.ReadUvarint(reader)
if err != nil { if err != nil {
return err return nil, err
} }
_, err = io.CopyN(io.Discard, reader, int64(tokenLen)) _, err = io.CopyN(io.Discard, reader, int64(tokenLen))
if err != nil { if err != nil {
return err return nil, err
} }
packetLen, err := qtls.ReadUvarint(reader) packetLen, err := qtls.ReadUvarint(reader)
if err != nil { if err != nil {
return err return nil, err
} }
hdrLen := int(reader.Size()) - reader.Len() hdrLen := int(reader.Size()) - reader.Len()
if hdrLen+int(packetLen) > len(packet) { if hdrLen+int(packetLen) > len(packet) {
return os.ErrInvalid return nil, os.ErrInvalid
} }
_, err = io.CopyN(io.Discard, reader, 4) _, err = io.CopyN(io.Discard, reader, 4)
if err != nil { if err != nil {
return err return nil, err
} }
pnBytes := make([]byte, aes.BlockSize) pnBytes := make([]byte, aes.BlockSize)
_, err = io.ReadFull(reader, pnBytes) _, err = io.ReadFull(reader, pnBytes)
if err != nil { if err != nil {
return err return nil, err
} }
var salt []byte var salt []byte
@@ -121,7 +117,7 @@ func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, pack
hpKey := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, hkdfHeaderProtectionLabel, 16) hpKey := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, hkdfHeaderProtectionLabel, 16)
block, err := aes.NewCipher(hpKey) block, err := aes.NewCipher(hpKey)
if err != nil { if err != nil {
return err return nil, err
} }
mask := make([]byte, aes.BlockSize) mask := make([]byte, aes.BlockSize)
block.Encrypt(mask, pnBytes) block.Encrypt(mask, pnBytes)
@@ -133,7 +129,7 @@ func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, pack
} }
packetNumberLength := newPacket[0]&0x3 + 1 packetNumberLength := newPacket[0]&0x3 + 1
if hdrLen+int(packetNumberLength) > int(packetLen)+hdrLen { if hdrLen+int(packetNumberLength) > int(packetLen)+hdrLen {
return os.ErrInvalid return nil, os.ErrInvalid
} }
var packetNumber uint32 var packetNumber uint32
switch packetNumberLength { switch packetNumberLength {
@@ -146,7 +142,7 @@ func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, pack
case 4: case 4:
packetNumber = binary.BigEndian.Uint32(newPacket[hdrLen:]) packetNumber = binary.BigEndian.Uint32(newPacket[hdrLen:])
default: default:
return E.New("bad packet number length") return nil, E.New("bad packet number length")
} }
extHdrLen := hdrLen + int(packetNumberLength) extHdrLen := hdrLen + int(packetNumberLength)
copy(newPacket[extHdrLen:hdrLen+4], packet[extHdrLen:]) copy(newPacket[extHdrLen:hdrLen+4], packet[extHdrLen:])
@@ -170,208 +166,138 @@ func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, pack
binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(packetNumber)) binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(packetNumber))
decrypted, err := cipher.Open(newPacket[extHdrLen:extHdrLen], nonce, data, newPacket[:extHdrLen]) decrypted, err := cipher.Open(newPacket[extHdrLen:extHdrLen], nonce, data, newPacket[:extHdrLen])
if err != nil { if err != nil {
return err return nil, err
} }
var frameType byte var frameType byte
var fragments []qCryptoFragment var frameLen uint64
var fragments []struct {
offset uint64
length uint64
payload []byte
}
decryptedReader := bytes.NewReader(decrypted) decryptedReader := bytes.NewReader(decrypted)
const (
frameTypePadding = 0x00
frameTypePing = 0x01
frameTypeAck = 0x02
frameTypeAck2 = 0x03
frameTypeCrypto = 0x06
frameTypeConnectionClose = 0x1c
)
var frameTypeList []uint8
for { for {
frameType, err = decryptedReader.ReadByte() frameType, err = decryptedReader.ReadByte()
if err == io.EOF { if err == io.EOF {
break break
} }
frameTypeList = append(frameTypeList, frameType)
switch frameType { switch frameType {
case frameTypePadding: case 0x00: // PADDING
continue continue
case frameTypePing: case 0x01: // PING
continue continue
case frameTypeAck, frameTypeAck2: case 0x02, 0x03: // ACK
_, err = qtls.ReadUvarint(decryptedReader) // Largest Acknowledged _, err = qtls.ReadUvarint(decryptedReader) // Largest Acknowledged
if err != nil { if err != nil {
return err return nil, err
} }
_, err = qtls.ReadUvarint(decryptedReader) // ACK Delay _, err = qtls.ReadUvarint(decryptedReader) // ACK Delay
if err != nil { if err != nil {
return err return nil, err
} }
ackRangeCount, err := qtls.ReadUvarint(decryptedReader) // ACK Range Count ackRangeCount, err := qtls.ReadUvarint(decryptedReader) // ACK Range Count
if err != nil { if err != nil {
return err return nil, err
} }
_, err = qtls.ReadUvarint(decryptedReader) // First ACK Range _, err = qtls.ReadUvarint(decryptedReader) // First ACK Range
if err != nil { if err != nil {
return err return nil, err
} }
for i := 0; i < int(ackRangeCount); i++ { for i := 0; i < int(ackRangeCount); i++ {
_, err = qtls.ReadUvarint(decryptedReader) // Gap _, err = qtls.ReadUvarint(decryptedReader) // Gap
if err != nil { if err != nil {
return err return nil, err
} }
_, err = qtls.ReadUvarint(decryptedReader) // ACK Range Length _, err = qtls.ReadUvarint(decryptedReader) // ACK Range Length
if err != nil { if err != nil {
return err return nil, err
} }
} }
if frameType == 0x03 { if frameType == 0x03 {
_, err = qtls.ReadUvarint(decryptedReader) // ECT0 Count _, err = qtls.ReadUvarint(decryptedReader) // ECT0 Count
if err != nil { if err != nil {
return err return nil, err
} }
_, err = qtls.ReadUvarint(decryptedReader) // ECT1 Count _, err = qtls.ReadUvarint(decryptedReader) // ECT1 Count
if err != nil { if err != nil {
return err return nil, err
} }
_, err = qtls.ReadUvarint(decryptedReader) // ECN-CE Count _, err = qtls.ReadUvarint(decryptedReader) // ECN-CE Count
if err != nil { if err != nil {
return err return nil, err
} }
} }
case frameTypeCrypto: case 0x06: // CRYPTO
var offset uint64 var offset uint64
offset, err = qtls.ReadUvarint(decryptedReader) offset, err = qtls.ReadUvarint(decryptedReader)
if err != nil { if err != nil {
return err return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err
} }
var length uint64 var length uint64
length, err = qtls.ReadUvarint(decryptedReader) length, err = qtls.ReadUvarint(decryptedReader)
if err != nil { if err != nil {
return err return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err
} }
index := len(decrypted) - decryptedReader.Len() index := len(decrypted) - decryptedReader.Len()
fragments = append(fragments, qCryptoFragment{offset, length, decrypted[index : index+int(length)]}) fragments = append(fragments, struct {
offset uint64
length uint64
payload []byte
}{offset, length, decrypted[index : index+int(length)]})
frameLen += length
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent) _, err = decryptedReader.Seek(int64(length), io.SeekCurrent)
if err != nil { if err != nil {
return err return nil, err
} }
case frameTypeConnectionClose: case 0x1c: // CONNECTION_CLOSE
_, err = qtls.ReadUvarint(decryptedReader) // Error Code _, err = qtls.ReadUvarint(decryptedReader) // Error Code
if err != nil { if err != nil {
return err return nil, err
} }
_, err = qtls.ReadUvarint(decryptedReader) // Frame Type _, err = qtls.ReadUvarint(decryptedReader) // Frame Type
if err != nil { if err != nil {
return err return nil, err
} }
var length uint64 var length uint64
length, err = qtls.ReadUvarint(decryptedReader) // Reason Phrase Length length, err = qtls.ReadUvarint(decryptedReader) // Reason Phrase Length
if err != nil { if err != nil {
return err return nil, err
} }
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent) // Reason Phrase _, err = decryptedReader.Seek(int64(length), io.SeekCurrent) // Reason Phrase
if err != nil { if err != nil {
return err return nil, err
} }
default: default:
return os.ErrInvalid return nil, os.ErrInvalid
} }
} }
if metadata.SniffContext != nil { tlsHdr := make([]byte, 5)
fragments = append(fragments, metadata.SniffContext.([]qCryptoFragment)...) tlsHdr[0] = 0x16
metadata.SniffContext = nil binary.BigEndian.PutUint16(tlsHdr[1:], uint16(0x0303))
} binary.BigEndian.PutUint16(tlsHdr[3:], uint16(frameLen))
var frameLen uint64
for _, fragment := range fragments {
frameLen += fragment.length
}
buffer := buf.NewSize(5 + int(frameLen))
defer buffer.Release()
buffer.WriteByte(0x16)
binary.Write(buffer, binary.BigEndian, uint16(0x0303))
binary.Write(buffer, binary.BigEndian, uint16(frameLen))
var index uint64 var index uint64
var length int var length int
var readers []io.Reader
readers = append(readers, bytes.NewReader(tlsHdr))
find: find:
for { for {
for _, fragment := range fragments { for _, fragment := range fragments {
if fragment.offset == index { if fragment.offset == index {
buffer.Write(fragment.payload) readers = append(readers, bytes.NewReader(fragment.payload))
index = fragment.offset + fragment.length index = fragment.offset + fragment.length
length++ length++
continue find continue find
} }
} }
break if length == len(fragments) {
break
}
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, E.New("bad fragments")
}
metadata, err := TLSClientHello(ctx, io.MultiReader(readers...))
if err != nil {
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err
} }
metadata.Protocol = C.ProtocolQUIC metadata.Protocol = C.ProtocolQUIC
fingerprint, err := ja3.Compute(buffer.Bytes()) return metadata, nil
if err != nil {
metadata.Protocol = C.ProtocolQUIC
metadata.Client = C.ClientChromium
metadata.SniffContext = fragments
return ErrClientHelloFragmented
}
metadata.Domain = fingerprint.ServerName
for metadata.Client == "" {
if len(frameTypeList) == 1 {
metadata.Client = C.ClientFirefox
break
}
if frameTypeList[0] == frameTypeCrypto && isZero(frameTypeList[1:]) {
if len(fingerprint.Versions) == 2 && fingerprint.Versions[0]&ja3.GreaseBitmask == 0x0A0A &&
len(fingerprint.EllipticCurves) == 5 && fingerprint.EllipticCurves[0]&ja3.GreaseBitmask == 0x0A0A {
metadata.Client = C.ClientSafari
break
}
if len(fingerprint.CipherSuites) == 1 && fingerprint.CipherSuites[0] == tls.TLS_AES_256_GCM_SHA384 &&
len(fingerprint.EllipticCurves) == 1 && fingerprint.EllipticCurves[0] == uint16(tls.X25519) &&
len(fingerprint.SignatureAlgorithms) == 1 && fingerprint.SignatureAlgorithms[0] == uint16(tls.ECDSAWithP256AndSHA256) {
metadata.Client = C.ClientSafari
break
}
}
if frameTypeList[len(frameTypeList)-1] == frameTypeCrypto && isZero(frameTypeList[:len(frameTypeList)-1]) {
metadata.Client = C.ClientQUICGo
break
}
if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 {
if maybeUQUIC(fingerprint) {
metadata.Client = C.ClientQUICGo
} else {
metadata.Client = C.ClientChromium
}
break
}
metadata.Client = C.ClientUnknown
//nolint:staticcheck
break
}
return nil
}
func isZero(slices []uint8) bool {
for _, slice := range slices {
if slice != 0 {
return false
}
}
return true
}
func count(slices []uint8, value uint8) int {
var times int
for _, slice := range slices {
if slice == value {
times++
}
}
return times
}
type qCryptoFragment struct {
offset uint64
length uint64
payload []byte
} }

View File

@@ -1,24 +0,0 @@
package sniff
import (
"crypto/tls"
"github.com/sagernet/sing-box/common/ja3"
)
// Chromium sends separate client hello packets, but UQUIC has not yet implemented this behavior
// The cronet without this behavior does not have version 115
var uQUICChrome115 = &ja3.ClientHello{
Version: tls.VersionTLS12,
CipherSuites: []uint16{4865, 4866, 4867},
Extensions: []uint16{0, 10, 13, 16, 27, 43, 45, 51, 57, 17513},
EllipticCurves: []uint16{29, 23, 24},
SignatureAlgorithms: []uint16{1027, 2052, 1025, 1283, 2053, 1281, 2054, 1537, 513},
}
func maybeUQUIC(fingerprint *ja3.ClientHello) bool {
if uQUICChrome115.Equals(fingerprint, true) {
return true
}
return false
}

View File

@@ -5,69 +5,31 @@ import (
"encoding/hex" "encoding/hex"
"testing" "testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff" "github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestSniffQUICChromium(t *testing.T) { func TestSniffQUICv1(t *testing.T) {
t.Parallel() t.Parallel()
pkt, err := hex.DecodeString("c30000000108f40d654cc09b27f5000044d08a94548e57e43cc5483f129986187c432d58d46674830442988f869566a6e31e2ae37c9f7acbf61cc81621594fab0b3dfdc1635460b32389563dc8e74006315661cd22694114612973c1c45910621713a48b375854f095e8a77ccf3afa64e972f0f7f7002f50e0b014b1b146ea47c07fb20b73ad5587872b51a0b3fafdf1c4cf4fe6f8b112142392efa25d993abe2f42582be145148bdfe12edcd96c3655b65a4781b093e5594ba8e3ae5320f12e8314fc3ca374128cc43381046c322b964681ed4395c813b28534505118201459665a44b8f0abead877de322e9040631d20b05f15b81fa7ff785d4041aecc37c7e2ccdc5d1532787ce566517e8985fd5c200dbfd1e67bc255efaba94cfc07bb52fea4a90887413b134f2715b5643542aa897c6116486f428d82da64d2a2c1e1bdd40bd592558901a554b003d6966ac5a7b8b9413eddbf6ef21f28386c74981e3ce1d724c341e95494907626659692720c81114ca4acea35a14c402cfa3dc2228446e78dc1b81fa4325cf7e314a9cad6a6bdff33b3351dcba74eb15fae67f1227283aa4cdd64bcadf8f19358333f8549b596f4350297b5c65274565869d497398339947b9d3d064e5b06d39d34b436d8a41c1a3880de10bd26c3b1c5b4e2a49b0d4d07b8d90cd9e92bc611564d19ea8ec33099e92033caf21f5307dbeaa4708b99eb313bff99e2081ac25fd12d6a72e8335e0724f6718fe023cd0ad0d6e6a6309f09c9c391eec2bc08e9c3210a043c08e1759f354c121f6517fff4d6e20711a871e41285d48d930352fddffb92c96ba57df045ce99f8bfdfa8edc0969ce68a51e9fbb4f54b956d9df74a9e4af27ed2b27839bce1cffeca8333c0aaee81a570217442f9029ba8fedb84a2cf4be4d910982d891ea00e816c7fb98e8020e896a9c6fdd9106611da0a99dde18df1b7a8f6327acb1eed9ad93314451e48cb0dfb9571728521ca3db2ac0968159d5622556a55d51a422d11995b650949aaefc5d24c16080446dfc4fbc10353f9f93ce161ab513367bb89ab83988e0630b689e174e27bcfcc31996ee7b0bca909e251b82d69a28fee5a5d662e127508cd19dbbe5097b7d5b62a49203d66764197a527e472e2627e44a93d44177dace9d60e7d0e03305ddf4cfe47cdf2362e14de79ef46a6763ce696cd7854a48d9419a0817507a4713ffd4977b906d4f2b5fb6dbe1bd15bc505d5fea582190bf531a45d5ee026da8918547fd5105f15e5d061c7b0cf80a34990366ed8e91e13c2f0d85e5dad537298808d193cf54b7eaac33f10051f74cb6b75e52f81618c36f03d86aef613ba237a1a793ba1539938a38f62ccaf7bd5f6c5e0ce53cde4012fcf2b758214a0422d2faaa798e86e19d7481b42df2b36a73d287ff28c20cce01ce598771fec16a8f1f00305c06010126013a6c1de9f589b4e79d693717cd88ad1c42a2d99fa96617ba0bc6365b68e21a70ebc447904aa27979e1514433cfd83bfec09f137c747d47582cb63eb28f873fb94cf7a59ff764ddfbb687d79a58bb10f85949269f7f72c611a5e0fbb52adfa298ff060ec2eb7216fd7302ea8fb07798cbb3be25cb53ac8161aac2b5bbcfbcfb01c113d28bd1cb0333fb89ac82a95930f7abded0a2f5a623cc6a1f62bf3f38ef1b81c1e50a634f657dbb6770e4af45879e2fb1e00c742e7b52205c8015b5c0f5b1e40186ff9aa7288ab3e01a51fb87761f9bc6837082af109b39cc9f620") pkt, err := hex.DecodeString("cc0000000108d2dc7bad02241f5003796e71004215a71bfcb05159416c724be418537389acdd9a4047306283dcb4d7a9cad5cc06322042d204da67a8dbaa328ab476bb428b48fd001501863afd203f8d4ef085629d664f1a734a65969a47e4a63d4e01a21f18c1d90db0c027180906dc135f9ae421bb8617314c8d54c175fef3d3383d310d0916ebcbd6eed9329befbbb109d8fd4af1d2cf9d6adce8e6c1260a7f8256e273e326da0aa7cc148d76e7a08489dc9d52ade89c027cbc3491ada46417c2c04e2ca768e9a7dd6aa00c594e48b678927325da796817693499bb727050cb3baf3d3291a397c3a8d868e8ec7b8f7295e347455c9dadbe2252ae917ac793d958c7fb8a3d2cdb34e3891eb4286f18617556ff7216dd60256aa5b1d11ff4753459fc5f9dedf11d483a26a0835dc6cd50e1c1f54f86e8f1e502821183cd874f6447a74e818bf3445c7795acf4559d1c1fac474911d2ead5c8d23e4aa4f67afb66efe305a30a0b5d825679b31ddc186cbea936535795c7e8c378c87b8c5adc065154d15bae8f85ac8fec2da40c3aa623b682a065440831555011d7647cde44446a0fb4cf5892f2c088ae1920643094be72e3c499fe8d265caf939e8ab607a5b9317917d2a32a812e8a0e6a2f84721bbb5984ffd242838f705d13f4cfb249bc6a5c80d58ac2595edf56648ec3fe21d787573c253a79805252d6d81e26d367d4ff29ef66b5fe8992086af7bada8cad10b82a7c0dc406c5b6d0c5ec3c583e767f759ce08cad6c3c8f91e5a8")
require.NoError(t, err) require.NoError(t, err)
var metadata adapter.InboundContext metadata, err := sniff.QUICClientHello(context.Background(), pkt)
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientChromium)
require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
require.NoError(t, err) require.NoError(t, err)
err = sniff.QUICClientHello(context.Background(), &metadata, pkt) require.Equal(t, metadata.Domain, "cloudflare-quic.com")
require.NoError(t, err)
require.Equal(t, metadata.Domain, "google.com")
} }
func TestSniffUQUICChrome115(t *testing.T) { func TestSniffQUICFragment(t *testing.T) {
t.Parallel() t.Parallel()
pkt, err := hex.DecodeString("cb0000000108181e17c387120abc000044d0705b6a3ef9ee37a8d3949a7d393ed078243c2ee2c3627fad1c3f107c117f4f071131ad61848068fcbbe5c65803c147f7f8ec5e2cd77b77beea23ba779d936dccac540f8396400e3190ea35cc2942af4171a04cb14272491920f90124959f44e80143678c0b52f5d31af319aaa589db2f940f004562724d0af40f737e1bb0002a071e6a1dbc9f52c64f070806a5010abed0298053634d9c9126bd7949ae5087998ade762c0ad06691d99c0875a38c601fc1ee77bfc3b8c11381829f2c9bdd022f4499c43ff1d6aee1a0d296861461dda217d22c568b276016ef3929e59d2f7d7ddf7809920fb7dc805641608949f3f8466ab3d37149aac501f0b107d808f3add4acfc657e4a82e2b88e97a6c74a00c419548760ab3414ba13915c78a1ca79dceee8d59fbe299f20b671ac44823218368b2a026baa55170cf549519ac21dbb6d31d248bd339438a4e663bcdca1fe3ae3f045a5dc19b122e9db9d7af9757076666dda4e9ace1c67def77fa14786f0cab3ebf7a270ea6e2b37838318c95779f80c3b8471948d0046c3614b3a13477c939a39a7855d85d13522a45ae0765739cd5eedef87237e824a929983ace27640c6495dbf5a72fa0b96893dc5d28f3988249a57bdb458d460b4a57043de3da750a76b6e5d2259247ca27cd864ea18f0d09aa62ab6eb7c014fb43179b2a1963d170b756cce83eeaebff78a828d025c811848e16ff862a8080d093478cd2208c8ab0803178325bc0d9d6bb25e62fa50c4ad15cf80916da6578796932036c72e43eb480d1e423ed812ac75a97722f8416529b82ba8ee2219c535012282bb17066bd53e78b87a71abdb7ebdb2a7c2766ff8397962e87d0f85485b64b4ee81cc84f99c47f33f2b0872716441992773f59186e38d32dbf5609a6fda94cb928cd25f5a7a3ab736b5a4236b6d5409ab18892c6a4d3480fc2350abfdf0bab1cedb55bdf0760fdb703e6688f4de596254eed4ed3e67eb03d0717b8e15b31e735214e588c87ae36bc6c310e1894b4c15143e4ccf287b2dbc707a946bf9671ae3c574f9486b2c82eec784bba4cbc76113cbe0f97ac8c13cfa38f2925ab9d06887a612ce48280a91d7e074e6caf898d88e2bbf71360899abf48a03f9a70cf2891199f2d63b116f4871af0ebb4f4906792f66cc21d1609f189138532875c129a68c73e7bcd3b5d8100beac1d8ac4b20d94a59ac8df5a5af58a9acb20413eadf97189f5f19ff889155f0c4d37514ec184eb6903967ff38a41fc087abb0f2cad3761d6e3f95f92a09a72f5c065b16e188088b87460241f27ecdb1bc6ece92c8d36b2d68b58d0fb4d4b3c928c579ade8ae5a995833aadd297c30a37f7bc35440fc97070e1b198e0fac00157452177d16d2803b4239997452b4ad3a951173bdec47a033fd7f8a7942accaa9aaa905b3c5a2175e7c3e07c48bf25331727fd69cd1e64d74d8c9d4a6f8f4491adb7bc911505cb19877083d8f21a12475e313fccf57877ff3556318e81ed9145dd9427f2b65275440893035f417481f721c69215af8ae103530cd0a1d35bf2cb5a27628f8d44d7c6f5ec12ce79d0a8333e0eb48771115d0a191304e46b8db19bbe5c40f1c346dde98e76ff5e21ff38d2c34e60cb07766ed529dd6d2cbacd7fbf1ed8a0e6e40decad0ca5021e91552be87c156d3ae2fffef41c65b14ba6d488f2c3227a1ab11ffce0e2dc47723a69da27a67a7f26e1cb13a7103af9b87a8db8e18ea") pkt, err := hex.DecodeString("cc00000001082e3d5d1b64040c55000044d0ccea69e773f6631c1d18b04ae9ee75fcfc34ef74fa62533c93534338a86f101a05d70e0697fb483063fa85db1c59ccfbda5c35234931d8524d8aac37eaaad649470a67794cd754b23c98695238b8363452333bc8c4858376b4166e001da2006e35cf98a91e11a56419b2786775284942d0f7163982f7c248867d12dd374957481dbc564013ff785e1916195eef671f725908f761099d992d69231336ba81d9e25fe2fa3a6eff4318a6ccf10176fc841a1b315f7b35c5b292266fc869d76ca533e7d14e86d82db2e22eacd350977e47d2e012d8a5891c5aaf2a0f4c2b2dae897c161e5b68cbb4dee952472bdc1e21504b8f02534ec4366ce3f8bf86efc78e0232778fbd554457567112abdcafcf6d4d8fcf35083c25d9495679614aba21696e338c62b585046cc55ba8c09c844361d889a47c3ea703b4e23545a9ab2c0bb369693a9ddfb5daffa85cf80fdd6ad66738664e5b0a551729b4955cff7255afcb04dee88c2f072c9de7400947a1bd9327ac5d012a33000ada021d4c03d249fb017d6ac9200b2f9436beab8183ddfbe2d8aee31ffb7df9e1cc181c1af80c39a89965d18ed12da8e3ebe2ae1fbe4b348f83ba19e3e3d1c9b22bcf03ab6ad9b30fe180623faa291ebad83bcd71d7b57f2f5e2f3b8e81d24fb70b2f2159239e8f21ffafef2747aba47d97ab4081e603c018b10678cf99cab1fb42156a14486fa435153979d7279fd22cd40af7088bfc7eff41af2f4b3c0c8864d0040d74dff427f7bffdb8c278474ea00311326cf4925471a8cf596cb92119f19e0f789490ba9cb77b98015a987d93e0324cf1a38b55109f00c3e6ddc5180fb107bf468323afec9bb49fd6a86418569789d66cafe3b8253c2aebb3af3782c1c54dd560487d031d28e6a6e23e159581bb1d47efc4da3fe1d169f9ffb0ca9ba61af0a38a92fde5bc5e6ec026e8378a6315a7b95abf1d2da790a391306ce74d0baf8e2ce648ca74c487f2c0a76a28a80cdf5bd34316eb607684fe7e6d9e83824a00e07660d0b90e3cddd61ebf10748263474afa88c300549e64ce2e90560bb1a12dee7e9484f729a8a4ee7c5651adb5194b3b3ae38e501567c7dbf36e7bb37a2c20b74655f47f2d9af18e52e9d4c9c9eee8e63745779b8f0b06f3a09d846ba62eb978ad77c85de1ee2fee3fbb4c2d283c73e1ccba56a4658e48a2665d200f7f9342f8e84c2ba490094a4f94feec89e42d2f654f564c2beb2997bafa1fc2c68ad8e160b63587d49abc31b834878d52acfb05fb73d0e059b206162e3c90b40c4bc08407ffcb3c08431895b691a3fea923f1f3b48db75d3e6b91fd319ffe4d486e0e14bd5c6affc838dee63d9e0b80f169b5e6c02c7321dcb20deb2b8e707b60e345a308d505bbf26a93d8f18b39d62632e9a77cbe48b3b32eb8819d6311a49820d40f5acbf0273c91c36b2269a03e72ee64df3dfb10ddefe73c64ef60870b2b77bd99dea655f5fe791b538a929a14d99f6d69685d72431ea5f0f4b27a044f2f575ab474fcc3857895934de1ca2581798eaef2c17fe5aaf2e6add97fa32997c7026f15c1b1ad0e6043ae506027a7c0242546fdc851cca39a204e56879f2cef838be8ec66e0f2292f8c862e06f810eb9b80c7a467ce6e90155206352c7f82b1173ba3b98d35bb72c259a60db20dd1a43fe6d7aef0265e6eaa5caafd9b64b448ff745a2046acbdb65cf2a5007809808a4828dc99097feedc734c236260c584")
require.NoError(t, err) require.NoError(t, err)
var metadata adapter.InboundContext metadata, err := sniff.QUICClientHello(context.Background(), pkt)
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC) require.Equal(t, metadata.Domain, "cloudflare-quic.com")
require.Equal(t, metadata.Client, C.ClientQUICGo)
require.Equal(t, metadata.Domain, "www.google.com")
}
func TestSniffQUICFirefox(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("c8000000010867f174d7ebfe1b0803cd9c20004286de068f7963cf1736349ee6ebe0ddcd3e4cd0041a51ced3f7ce9eea1fb595458e74bdb4b792b16449bd8cae71419862c4fcbe766eaec7d1af65cd298e1dd46f8bd94a77ab4ca28c54b8e9773de3f02d7cb2463c9f7dcacfb311f024b0266ec6ab7bfb615b4148333fb4d4ece7c4cd90029ca30c2cbae2216b428499ec873fa125797e71c5a5da85087760ad37ca610020f71b76e82651c47576e20bf33cf676cb2d400b8c09d3c8cb4e21c47d2b21f6b68732bef30c8cefd5c723fc23eb29e6f7f65a5e52aad9055c1fb3d8b1811f0380b38d7e2eee8eb37dd5bd5d4ca4b66540175d916289d88a9df7c161964d713999c5057d27edb298ef5164352568b0d4bac3c15d90456e8fd460e41b81d0ec1b1e94b87d3333cc6908b018e0914ae1f214d73e75398da3d55a0106161d3a75897b4eb66e98c59010fae75f0d367d38be48c3a5c58bc8a30773c3fff50690ac9d487822f85d4f5713d626baa92d36e858dd21259cf814bce0b90d18da88a1ade40113e5a088cdb304a2558879152a8cf15c1839e056378aa41acba6fcb9974dee54bd50b5d4eb2c475654e06c0ec06b7f18f4462c808684843a1071041b9bfb2688324e0120144944416e30e83eedbbbcbc275b1f53762d3db18f0998ce54f0e1c512946b4098f07781d49264fa148f4c8220a3b02e73d7f15554aa370aafeff73cb75c52c494edf90f0261abfdd32a4d670f729de50266162687aa8efe14b8506f313b058b02aaaab5825428f5f4510b8e49451fdcb7b5a4af4b59c831afcb89fb4f64dba78e3b38387e87e9e8cdaa1f3b700a87c7d442388863b8950296e5773b38f308d62f52548c0bbf308e40540747cca5bf99b1345bc0d70b8f0e69a83b85a8d69f795b87f93e2bfccf52b529afea4ff6fd456957000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientFirefox)
require.Equal(t, metadata.Domain, "www.google.com")
}
func TestSniffQUICSafari(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("c70000000108e4e75af2e223198a0000449ef2d83cb4473a62765eba67424cd4a5817315cbf55a9e8daaca360904b0bae60b1629cfeba11e2dfbbf5ea4c588cb134e31af36fd7a409fb0fcc0187e9b56037ac37964ed20a8c1ca19fd6cfd53398324b3d0c71537294f769db208fa998b6811234a4a7eb3b5eceb457ae92e3a2d98f7c110702db8064b5c29fa3298eb1d0529fd445a84a5fd6ff8709be90f8af4f94998d8a8f2953bb05ad08c80668eca784c6aec959114e68e5b827e7c41c79f2277c716a967e7fcc8d1b77442e6cb18329dbedb34b473516b468cba5fc20659e655fbe37f36408289b9a475fcee091bd82828d3be00367e9e5cec9423bb97854abdada1d7562a3777756eb3bddef826ddc1ef46137cb01bb504a54d410d9bcb74cd5f959050c84edf343fa6a49708c228a758ee7adbbadf260b2f1984911489712e2cb364a3d6520badba4b7e539b9c163eeddfd96c0abb0de151e47496bb9750be76ee17ccdb61d35d2c6795174037d6f9d282c3f36c4d9a90b64f3b6ddd0cf4d9ed8e6f7805e25928fa04b087e63ae02761df30720cc01dfc32b64c575c8a66ef82e9a17400ff80cd8609b93ba16d668f4aa734e71c4a5d145f14ee1151bec970214e0ff83fc3e1e85d8694f2975f9155c57c18b7b69bb6a36832a9435f1f4b346a7be188f3a75f9ad2cc6ad0a3d26d6fa7d4c1179bd49bd5989d15ba43ff602890107db96484695086627356750d7b2b3b714ba65d564654e8f60ac10f5b6d3bfb507e8eaa31bab1da2d676195046d165c7f8b32829c9f9b68d97b2af7ac04a1369357e4b65de2b2f24eaf27cc8d95e05db001adebe726f927a94e43e62ce671e6e306e16f05aafcbe6c49080e80286d7939f375023d110a5ad9069364ae928ca480454a9dcddd61bc48b7efeb716a5bd6c7cd39c486ceb20c738af6abf22ba1ddd8b4a3b781fc2f251173409e1aadccbd7514e97106d0ebfc3af6e59445f74cd733a1ba99b10fce3fb4e9f7c88f5e25b567f5ba2b8dabacd375e7faf7634bfa178cbe51aee63032c5126b196ea47b02385fc3062a000fb7e4b4d0d12e74579f8830ede20d10829496032b2cc56743287f9a9b4d5091877a82fea44deb2cffac8a379f78a151d99e28cbc74d732c083bf06d50584e3f18f254e71a48d6ababaf6fff6f425e9be001510dfbe6a32a27792c00ada036b62ddb90c706d7b882c76a7072f5dd11c69a1f49d4ba183cb0b57545419fa27b9b9706098848935ae9c9e8fbe9fac165d1339128b991a73d20e7795e8d6a8c6adfbf20bf13ada43f2aef3ba78c14697910507132623f721387dce60c4707225b84d9782d469a5d9eaa099f35d6a590ef142ddef766495cf3337815ceef5ff2b3ed352637e72b5c23a2a8ff7d7440236a19b981d47f8e519a0431ebfbc0b78d8a36798b4c060c0c6793499f1e2e818862560a5b501c8d02ba1517be1941da2af5b174e0189c62978d878eb0f9c9db3a9221c28fb94645cf6e85ff2eea8c65ba3083a7382b131b83102dd67aa5453ad7375a4eb8c69fc479fbd29dab8924f801d253f2c997120b705c6e5217fb74702e2f1038917dd5fb0eeb7ae1bf7a668fc7d50c034b4cd5a057a8482e6bc9c921297f44e76967265623a167cd9883eb6e64bc77856dc333bd605d7df3bed0e5cecb5a99fe8b62873d58530f")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientSafari)
require.Equal(t, metadata.Domain, "www.google.com")
} }
func FuzzSniffQUIC(f *testing.F) { func FuzzSniffQUIC(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) { f.Fuzz(func(t *testing.T, data []byte) {
var metadata adapter.InboundContext sniff.QUICClientHello(context.Background(), data)
err := sniff.QUICClientHello(context.Background(), &metadata, data)
require.Error(t, err)
}) })
} }

View File

@@ -1,90 +0,0 @@
package sniff
import (
"context"
"encoding/binary"
"io"
"os"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common/rw"
)
func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
var tpktVersion uint8
err := binary.Read(reader, binary.BigEndian, &tpktVersion)
if err != nil {
return err
}
if tpktVersion != 0x03 {
return os.ErrInvalid
}
var tpktReserved uint8
err = binary.Read(reader, binary.BigEndian, &tpktReserved)
if err != nil {
return err
}
if tpktReserved != 0x00 {
return os.ErrInvalid
}
var tpktLength uint16
err = binary.Read(reader, binary.BigEndian, &tpktLength)
if err != nil {
return err
}
if tpktLength != 19 {
return os.ErrInvalid
}
var cotpLength uint8
err = binary.Read(reader, binary.BigEndian, &cotpLength)
if err != nil {
return err
}
if cotpLength != 14 {
return os.ErrInvalid
}
var cotpTpduType uint8
err = binary.Read(reader, binary.BigEndian, &cotpTpduType)
if err != nil {
return err
}
if cotpTpduType != 0xE0 {
return os.ErrInvalid
}
err = rw.SkipN(reader, 5)
if err != nil {
return err
}
var rdpType uint8
err = binary.Read(reader, binary.BigEndian, &rdpType)
if err != nil {
return err
}
if rdpType != 0x01 {
return os.ErrInvalid
}
var rdpFlags uint8
err = binary.Read(reader, binary.BigEndian, &rdpFlags)
if err != nil {
return err
}
var rdpLength uint8
err = binary.Read(reader, binary.BigEndian, &rdpLength)
if err != nil {
return err
}
if rdpLength != 8 {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolRDP
return nil
}

View File

@@ -1,25 +0,0 @@
package sniff_test
import (
"bytes"
"context"
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require"
)
func TestSniffRDP(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("030000130ee00000000000010008000b000000010008000b000000")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.RDP(context.TODO(), &metadata, bytes.NewReader(pkt))
require.NoError(t, err)
require.Equal(t, C.ProtocolRDP, metadata.Protocol)
}

View File

@@ -14,65 +14,49 @@ import (
) )
type ( type (
StreamSniffer = func(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error StreamSniffer = func(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error)
PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error PacketSniffer = func(ctx context.Context, packet []byte) (*adapter.InboundContext, error)
) )
func Skip(metadata *adapter.InboundContext) bool { func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) (*adapter.InboundContext, error) {
// 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, metadata *adapter.InboundContext, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) error {
if timeout == 0 { if timeout == 0 {
timeout = C.ReadPayloadTimeout timeout = C.ReadPayloadTimeout
} }
deadline := time.Now().Add(timeout) deadline := time.Now().Add(timeout)
var errors []error var errors []error
for i := 0; ; i++ {
for i := 0; i < 3; i++ {
err := conn.SetReadDeadline(deadline) err := conn.SetReadDeadline(deadline)
if err != nil { if err != nil {
return E.Cause(err, "set read deadline") return nil, E.Cause(err, "set read deadline")
} }
_, err = buffer.ReadOnceFrom(conn) _, err = buffer.ReadOnceFrom(conn)
_ = conn.SetReadDeadline(time.Time{}) err = E.Errors(err, conn.SetReadDeadline(time.Time{}))
if err != nil { if err != nil {
if i > 0 { if i > 0 {
break break
} }
return E.Cause(err, "read payload") return nil, E.Cause(err, "read payload")
} }
errors = nil
for _, sniffer := range sniffers { for _, sniffer := range sniffers {
err = sniffer(ctx, metadata, bytes.NewReader(buffer.Bytes())) metadata, err := sniffer(ctx, bytes.NewReader(buffer.Bytes()))
if err == nil { if metadata != nil {
return nil return metadata, nil
} }
errors = append(errors, err) errors = append(errors, err)
} }
} }
return E.Errors(errors...) return nil, E.Errors(errors...)
} }
func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error { func PeekPacket(ctx context.Context, packet []byte, sniffers ...PacketSniffer) (*adapter.InboundContext, error) {
var errors []error var errors []error
for _, sniffer := range sniffers { for _, sniffer := range sniffers {
err := sniffer(ctx, metadata, packet) metadata, err := sniffer(ctx, packet)
if err == nil { if metadata != nil {
return nil return metadata, nil
} }
errors = append(errors, err) errors = append(errors, err)
} }
return E.Errors(errors...) return nil, E.Errors(errors...)
} }

View File

@@ -1,26 +0,0 @@
package sniff
import (
"bufio"
"context"
"io"
"os"
"strings"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
)
func SSH(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
scanner := bufio.NewScanner(reader)
if !scanner.Scan() {
return os.ErrInvalid
}
fistLine := scanner.Text()
if !strings.HasPrefix(fistLine, "SSH-2.0-") {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolSSH
metadata.Client = fistLine[8:]
return nil
}

View File

@@ -1,26 +0,0 @@
package sniff_test
import (
"bytes"
"context"
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require"
)
func TestSniffSSH(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("5353482d322e302d64726f70626561720d0a000001a40a1492892570d1223aef61b0d647972c8bd30000009f637572766532353531392d7368613235362c637572766532353531392d736861323536406c69627373682e6f72672c6469666669652d68656c6c6d616e2d67726f757031342d7368613235362c6469666669652d68656c6c6d616e2d67726f757031342d736861312c6b6578677565737332406d6174742e7563632e61736e2e61752c6b65782d7374726963742d732d763030406f70656e7373682e636f6d000000207373682d656432353531392c7273612d736861322d3235362c7373682d7273610000003363686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d6374722c6165733235362d6374720000003363686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d6374722c6165733235362d63747200000017686d61632d736861312c686d61632d736861322d32353600000017686d61632d736861312c686d61632d736861322d323536000000046e6f6e65000000046e6f6e65000000000000000000000000002aa6ed090585b7d635b6")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt))
require.NoError(t, err)
require.Equal(t, C.ProtocolSSH, metadata.Protocol)
require.Equal(t, "dropbear", metadata.Client)
}

View File

@@ -9,17 +9,16 @@ import (
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
) )
func STUNMessage(_ context.Context, metadata *adapter.InboundContext, packet []byte) error { func STUNMessage(ctx context.Context, packet []byte) (*adapter.InboundContext, error) {
pLen := len(packet) pLen := len(packet)
if pLen < 20 { if pLen < 20 {
return os.ErrInvalid return nil, os.ErrInvalid
} }
if binary.BigEndian.Uint32(packet[4:8]) != 0x2112A442 { if binary.BigEndian.Uint32(packet[4:8]) != 0x2112A442 {
return os.ErrInvalid return nil, os.ErrInvalid
} }
if len(packet) < 20+int(binary.BigEndian.Uint16(packet[2:4])) { if len(packet) < 20+int(binary.BigEndian.Uint16(packet[2:4])) {
return os.ErrInvalid return nil, os.ErrInvalid
} }
metadata.Protocol = C.ProtocolSTUN return &adapter.InboundContext{Protocol: C.ProtocolSTUN}, nil
return nil
} }

View File

@@ -5,7 +5,6 @@ import (
"encoding/hex" "encoding/hex"
"testing" "testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff" "github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
@@ -16,16 +15,14 @@ func TestSniffSTUN(t *testing.T) {
t.Parallel() t.Parallel()
packet, err := hex.DecodeString("000100002112a44224b1a025d0c180c484341306") packet, err := hex.DecodeString("000100002112a44224b1a025d0c180c484341306")
require.NoError(t, err) require.NoError(t, err)
var metadata adapter.InboundContext metadata, err := sniff.STUNMessage(context.Background(), packet)
err = sniff.STUNMessage(context.Background(), &metadata, packet)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolSTUN) require.Equal(t, metadata.Protocol, C.ProtocolSTUN)
} }
func FuzzSniffSTUN(f *testing.F) { func FuzzSniffSTUN(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) { f.Fuzz(func(t *testing.T, data []byte) {
var metadata adapter.InboundContext if _, err := sniff.STUNMessage(context.Background(), data); err == nil {
if err := sniff.STUNMessage(context.Background(), &metadata, data); err == nil {
t.Fail() t.Fail()
} }
}) })

View File

@@ -10,7 +10,7 @@ import (
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
) )
func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error { func TLSClientHello(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error) {
var clientHello *tls.ClientHelloInfo var clientHello *tls.ClientHelloInfo
err := tls.Server(bufio.NewReadOnlyConn(reader), &tls.Config{ err := tls.Server(bufio.NewReadOnlyConn(reader), &tls.Config{
GetConfigForClient: func(argHello *tls.ClientHelloInfo) (*tls.Config, error) { GetConfigForClient: func(argHello *tls.ClientHelloInfo) (*tls.Config, error) {
@@ -19,9 +19,7 @@ func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reade
}, },
}).HandshakeContext(ctx) }).HandshakeContext(ctx)
if clientHello != nil { if clientHello != nil {
metadata.Protocol = C.ProtocolTLS return &adapter.InboundContext{Protocol: C.ProtocolTLS, Domain: clientHello.ServerName}, nil
metadata.Domain = clientHello.ServerName
return nil
} }
return err return nil, err
} }

View File

@@ -1,7 +1,6 @@
package srs package srs
import ( import (
"bufio"
"compress/zlib" "compress/zlib"
"encoding/binary" "encoding/binary"
"io" "io"
@@ -12,7 +11,7 @@ import (
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/domain" "github.com/sagernet/sing/common/domain"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin" "github.com/sagernet/sing/common/rw"
"go4.org/netipx" "go4.org/netipx"
) )
@@ -36,19 +35,17 @@ const (
ruleItemPackageName ruleItemPackageName
ruleItemWIFISSID ruleItemWIFISSID
ruleItemWIFIBSSID ruleItemWIFIBSSID
ruleItemAdGuardDomain
ruleItemProcessPathRegex
ruleItemFinal uint8 = 0xFF ruleItemFinal uint8 = 0xFF
) )
func Read(reader io.Reader, recover bool) (ruleSet option.PlainRuleSet, err error) { func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) {
var magicBytes [3]byte var magicBytes [3]byte
_, err = io.ReadFull(reader, magicBytes[:]) _, err = io.ReadFull(reader, magicBytes[:])
if err != nil { if err != nil {
return return
} }
if magicBytes != MagicBytes { if magicBytes != MagicBytes {
err = E.New("invalid sing-box rule-set file") err = E.New("invalid sing-box rule set file")
return return
} }
var version uint8 var version uint8
@@ -56,21 +53,20 @@ func Read(reader io.Reader, recover bool) (ruleSet option.PlainRuleSet, err erro
if err != nil { if err != nil {
return ruleSet, err return ruleSet, err
} }
if version > C.RuleSetVersion2 { if version != 1 {
return ruleSet, E.New("unsupported version: ", version) return ruleSet, E.New("unsupported version: ", version)
} }
compressReader, err := zlib.NewReader(reader) zReader, err := zlib.NewReader(reader)
if err != nil { if err != nil {
return return
} }
bReader := bufio.NewReader(compressReader) length, err := rw.ReadUVariant(zReader)
length, err := binary.ReadUvarint(bReader)
if err != nil { if err != nil {
return return
} }
ruleSet.Rules = make([]option.HeadlessRule, length) ruleSet.Rules = make([]option.HeadlessRule, length)
for i := uint64(0); i < length; i++ { for i := uint64(0); i < length; i++ {
ruleSet.Rules[i], err = readRule(bReader, recover) ruleSet.Rules[i], err = readRule(zReader, recovery)
if err != nil { if err != nil {
err = E.Cause(err, "read rule[", i, "]") err = E.Cause(err, "read rule[", i, "]")
return return
@@ -79,44 +75,33 @@ func Read(reader io.Reader, recover bool) (ruleSet option.PlainRuleSet, err erro
return return
} }
func Write(writer io.Writer, ruleSet option.PlainRuleSet, generateUnstable bool) error { func Write(writer io.Writer, ruleSet option.PlainRuleSet) error {
_, err := writer.Write(MagicBytes[:]) _, err := writer.Write(MagicBytes[:])
if err != nil { if err != nil {
return err return err
} }
var version uint8 err = binary.Write(writer, binary.BigEndian, uint8(1))
if generateUnstable {
version = C.RuleSetVersion2
} else {
version = C.RuleSetVersion1
}
err = binary.Write(writer, binary.BigEndian, version)
if err != nil { if err != nil {
return err return err
} }
compressWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression) zWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression)
if err != nil { if err != nil {
return err return err
} }
bWriter := bufio.NewWriter(compressWriter) err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules)))
_, err = varbin.WriteUvarint(bWriter, uint64(len(ruleSet.Rules)))
if err != nil { if err != nil {
return err return err
} }
for _, rule := range ruleSet.Rules { for _, rule := range ruleSet.Rules {
err = writeRule(bWriter, rule, generateUnstable) err = writeRule(zWriter, rule)
if err != nil { if err != nil {
return err return err
} }
} }
err = bWriter.Flush() return zWriter.Close()
if err != nil {
return err
}
return compressWriter.Close()
} }
func readRule(reader varbin.Reader, recover bool) (rule option.HeadlessRule, err error) { func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) {
var ruleType uint8 var ruleType uint8
err = binary.Read(reader, binary.BigEndian, &ruleType) err = binary.Read(reader, binary.BigEndian, &ruleType)
if err != nil { if err != nil {
@@ -125,28 +110,28 @@ func readRule(reader varbin.Reader, recover bool) (rule option.HeadlessRule, err
switch ruleType { switch ruleType {
case 0: case 0:
rule.Type = C.RuleTypeDefault rule.Type = C.RuleTypeDefault
rule.DefaultOptions, err = readDefaultRule(reader, recover) rule.DefaultOptions, err = readDefaultRule(reader, recovery)
case 1: case 1:
rule.Type = C.RuleTypeLogical rule.Type = C.RuleTypeLogical
rule.LogicalOptions, err = readLogicalRule(reader, recover) rule.LogicalOptions, err = readLogicalRule(reader, recovery)
default: default:
err = E.New("unknown rule type: ", ruleType) err = E.New("unknown rule type: ", ruleType)
} }
return return
} }
func writeRule(writer varbin.Writer, rule option.HeadlessRule, generateUnstable bool) error { func writeRule(writer io.Writer, rule option.HeadlessRule) error {
switch rule.Type { switch rule.Type {
case C.RuleTypeDefault: case C.RuleTypeDefault:
return writeDefaultRule(writer, rule.DefaultOptions, generateUnstable) return writeDefaultRule(writer, rule.DefaultOptions)
case C.RuleTypeLogical: case C.RuleTypeLogical:
return writeLogicalRule(writer, rule.LogicalOptions, generateUnstable) return writeLogicalRule(writer, rule.LogicalOptions)
default: default:
panic("unknown rule type: " + rule.Type) panic("unknown rule type: " + rule.Type)
} }
} }
func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHeadlessRule, err error) { func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) {
var lastItemType uint8 var lastItemType uint8
for { for {
var itemType uint8 var itemType uint8
@@ -173,9 +158,6 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
return return
} }
rule.DomainMatcher = matcher rule.DomainMatcher = matcher
if recover {
rule.Domain, rule.DomainSuffix = matcher.Dump()
}
case ruleItemDomainKeyword: case ruleItemDomainKeyword:
rule.DomainKeyword, err = readRuleItemString(reader) rule.DomainKeyword, err = readRuleItemString(reader)
case ruleItemDomainRegex: case ruleItemDomainRegex:
@@ -185,7 +167,7 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
if err != nil { if err != nil {
return return
} }
if recover { if recovery {
rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String) rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String)
} }
case ruleItemIPCIDR: case ruleItemIPCIDR:
@@ -193,7 +175,7 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
if err != nil { if err != nil {
return return
} }
if recover { if recovery {
rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String) rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String)
} }
case ruleItemSourcePort: case ruleItemSourcePort:
@@ -208,25 +190,12 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
rule.ProcessName, err = readRuleItemString(reader) rule.ProcessName, err = readRuleItemString(reader)
case ruleItemProcessPath: case ruleItemProcessPath:
rule.ProcessPath, err = readRuleItemString(reader) rule.ProcessPath, err = readRuleItemString(reader)
case ruleItemProcessPathRegex:
rule.ProcessPathRegex, err = readRuleItemString(reader)
case ruleItemPackageName: case ruleItemPackageName:
rule.PackageName, err = readRuleItemString(reader) rule.PackageName, err = readRuleItemString(reader)
case ruleItemWIFISSID: case ruleItemWIFISSID:
rule.WIFISSID, err = readRuleItemString(reader) rule.WIFISSID, err = readRuleItemString(reader)
case ruleItemWIFIBSSID: case ruleItemWIFIBSSID:
rule.WIFIBSSID, err = readRuleItemString(reader) rule.WIFIBSSID, err = readRuleItemString(reader)
case ruleItemAdGuardDomain:
if recover {
err = E.New("unable to decompile binary AdGuard rules to rule-set")
return
}
var matcher *domain.AdGuardMatcher
matcher, err = domain.ReadAdGuardMatcher(reader)
if err != nil {
return
}
rule.AdGuardDomainMatcher = matcher
case ruleItemFinal: case ruleItemFinal:
err = binary.Read(reader, binary.BigEndian, &rule.Invert) err = binary.Read(reader, binary.BigEndian, &rule.Invert)
return return
@@ -240,7 +209,7 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
} }
} }
func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, generateUnstable bool) error { func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error {
err := binary.Write(writer, binary.BigEndian, uint8(0)) err := binary.Write(writer, binary.BigEndian, uint8(0))
if err != nil { if err != nil {
return err return err
@@ -264,7 +233,7 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
if err != nil { if err != nil {
return err return err
} }
err = domain.NewMatcher(rule.Domain, rule.DomainSuffix, !generateUnstable).Write(writer) err = domain.NewMatcher(rule.Domain, rule.DomainSuffix).Write(writer)
if err != nil { if err != nil {
return err return err
} }
@@ -329,12 +298,6 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
return err return err
} }
} }
if len(rule.ProcessPathRegex) > 0 {
err = writeRuleItemString(writer, ruleItemProcessPathRegex, rule.ProcessPathRegex)
if err != nil {
return err
}
}
if len(rule.PackageName) > 0 { if len(rule.PackageName) > 0 {
err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName) err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName)
if err != nil { if err != nil {
@@ -353,16 +316,6 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
return err return err
} }
} }
if len(rule.AdGuardDomain) > 0 {
err = binary.Write(writer, binary.BigEndian, ruleItemAdGuardDomain)
if err != nil {
return err
}
err = domain.NewAdGuardMatcher(rule.AdGuardDomain).Write(writer)
if err != nil {
return err
}
}
err = binary.Write(writer, binary.BigEndian, ruleItemFinal) err = binary.Write(writer, binary.BigEndian, ruleItemFinal)
if err != nil { if err != nil {
return err return err
@@ -374,31 +327,73 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
return nil return nil
} }
func readRuleItemString(reader varbin.Reader) ([]string, error) { func readRuleItemString(reader io.Reader) ([]string, error) {
return varbin.ReadValue[[]string](reader, binary.BigEndian) 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 writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) error { func writeRuleItemString(writer io.Writer, itemType uint8, value []string) error {
err := writer.WriteByte(itemType) err := binary.Write(writer, binary.BigEndian, itemType)
if err != nil { if err != nil {
return err return err
} }
return varbin.Write(writer, binary.BigEndian, value) err = rw.WriteUVariant(writer, uint64(len(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 { if err != nil {
return err return err
} }
return varbin.Write(writer, binary.BigEndian, value) for _, item := range value {
err = rw.WriteVString(writer, item)
if err != nil {
return err
}
}
return nil
} }
func writeRuleItemCIDR(writer varbin.Writer, itemType uint8, value []string) error { 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 {
var builder netipx.IPSetBuilder var builder netipx.IPSetBuilder
for i, prefixString := range value { for i, prefixString := range value {
prefix, err := netip.ParsePrefix(prefixString) prefix, err := netip.ParsePrefix(prefixString)
@@ -424,8 +419,9 @@ func writeRuleItemCIDR(writer varbin.Writer, itemType uint8, value []string) err
return writeIPSet(writer, ipSet) return writeIPSet(writer, ipSet)
} }
func readLogicalRule(reader varbin.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) { func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) {
mode, err := reader.ReadByte() var mode uint8
err = binary.Read(reader, binary.BigEndian, &mode)
if err != nil { if err != nil {
return return
} }
@@ -438,7 +434,7 @@ func readLogicalRule(reader varbin.Reader, recovery bool) (logicalRule option.Lo
err = E.New("unknown logical mode: ", mode) err = E.New("unknown logical mode: ", mode)
return return
} }
length, err := binary.ReadUvarint(reader) length, err := rw.ReadUVariant(reader)
if err != nil { if err != nil {
return return
} }
@@ -457,7 +453,7 @@ func readLogicalRule(reader varbin.Reader, recovery bool) (logicalRule option.Lo
return return
} }
func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRule, generateUnstable bool) error { func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error {
err := binary.Write(writer, binary.BigEndian, uint8(1)) err := binary.Write(writer, binary.BigEndian, uint8(1))
if err != nil { if err != nil {
return err return err
@@ -473,12 +469,12 @@ func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRu
if err != nil { if err != nil {
return err return err
} }
_, err = varbin.WriteUvarint(writer, uint64(len(logicalRule.Rules))) err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules)))
if err != nil { if err != nil {
return err return err
} }
for _, rule := range logicalRule.Rules { for _, rule := range logicalRule.Rules {
err = writeRule(writer, rule, generateUnstable) err = writeRule(writer, rule)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -2,13 +2,11 @@ package srs
import ( import (
"encoding/binary" "encoding/binary"
"io"
"net/netip" "net/netip"
"os"
"unsafe" "unsafe"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common/rw"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/varbin"
"go4.org/netipx" "go4.org/netipx"
) )
@@ -22,57 +20,94 @@ type myIPRange struct {
to netip.Addr to netip.Addr
} }
type myIPRangeData struct { func readIPSet(reader io.Reader) (*netipx.IPSet, error) {
From []byte var version uint8
To []byte err := binary.Read(reader, binary.BigEndian, &version)
}
func readIPSet(reader varbin.Reader) (*netipx.IPSet, error) {
version, err := reader.ReadByte()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if version != 1 {
return nil, os.ErrInvalid
}
// WTF why using uint64 here
var length uint64 var length uint64
err = binary.Read(reader, binary.BigEndian, &length) err = binary.Read(reader, binary.BigEndian, &length)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ranges := make([]myIPRangeData, length)
err = varbin.Read(reader, binary.BigEndian, &ranges)
if err != nil {
return nil, err
}
mySet := &myIPSet{ mySet := &myIPSet{
rr: make([]myIPRange, len(ranges)), rr: make([]myIPRange, length),
} }
for i, rangeData := range ranges { for i := uint64(0); i < length; i++ {
mySet.rr[i].from = M.AddrFromIP(rangeData.From) var (
mySet.rr[i].to = M.AddrFromIP(rangeData.To) 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}
} }
return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil
} }
func writeIPSet(writer varbin.Writer, set *netipx.IPSet) error { func writeIPSet(writer io.Writer, set *netipx.IPSet) error {
err := writer.WriteByte(1) err := binary.Write(writer, binary.BigEndian, uint8(1))
if err != nil { if err != nil {
return err return err
} }
dataList := common.Map((*myIPSet)(unsafe.Pointer(set)).rr, func(rr myIPRange) myIPRangeData { mySet := (*myIPSet)(unsafe.Pointer(set))
return myIPRangeData{ err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr)))
From: rr.from.AsSlice(), if err != nil {
To: rr.to.AsSlice(), 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)))
err = binary.Write(writer, binary.BigEndian, uint64(len(dataList))) if err != nil {
if err != nil { return err
return err }
} _, err = writer.Write(fromBinary)
for _, data := range dataList { if err != nil {
err = varbin.Write(writer, binary.BigEndian, data) 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)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -11,11 +11,12 @@ import (
"strings" "strings"
cftls "github.com/sagernet/cloudflare-tls" cftls "github.com/sagernet/cloudflare-tls"
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
"github.com/fsnotify/fsnotify"
) )
type echServerConfig struct { type echServerConfig struct {
@@ -25,8 +26,9 @@ type echServerConfig struct {
key []byte key []byte
certificatePath string certificatePath string
keyPath string keyPath string
watcher *fsnotify.Watcher
echKeyPath string echKeyPath string
watcher *fswatch.Watcher echWatcher *fsnotify.Watcher
} }
func (c *echServerConfig) ServerName() string { func (c *echServerConfig) ServerName() string {
@@ -64,84 +66,146 @@ func (c *echServerConfig) Clone() Config {
} }
func (c *echServerConfig) Start() error { func (c *echServerConfig) Start() error {
err := c.startWatcher() if c.certificatePath != "" && c.keyPath != "" {
if err != nil { err := c.startWatcher()
c.logger.Warn("create credentials watcher: ", err) 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)
}
} }
return nil return nil
} }
func (c *echServerConfig) startWatcher() error { func (c *echServerConfig) startWatcher() error {
var watchPath []string watcher, err := fsnotify.NewWatcher()
if c.certificatePath != "" {
watchPath = append(watchPath, c.certificatePath)
}
if c.keyPath != "" {
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 { if err != nil {
return err return err
} }
c.watcher = watcher if c.certificatePath != "" {
return nil err = watcher.Add(c.certificatePath)
}
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 {
return err
}
c.certificate = certificate
} else {
key, err := os.ReadFile(c.keyPath)
if err != nil {
return err
}
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 { if err != nil {
return err 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")
} }
if c.keyPath != "" {
err = watcher.Add(c.keyPath)
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()
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 *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()
if err != nil {
c.logger.Error(E.Cause(err, "reload ECH key"))
}
case err, ok := <-c.echWatcher.Errors:
if !ok {
return
}
c.logger.Error(E.Cause(err, "fsnotify error"))
}
}
}
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 return nil
} }
@@ -149,7 +213,12 @@ func (c *echServerConfig) Close() error {
var err error var err error
if c.watcher != nil { if c.watcher != nil {
err = E.Append(err, c.watcher.Close(), func(err error) error { err = E.Append(err, c.watcher.Close(), func(err error) error {
return E.Cause(err, "close credentials watcher") 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 err return err

View File

@@ -7,13 +7,14 @@ import (
"os" "os"
"strings" "strings"
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
"github.com/fsnotify/fsnotify"
) )
var errInsecureUnused = E.New("tls: insecure unused") var errInsecureUnused = E.New("tls: insecure unused")
@@ -26,7 +27,7 @@ type STDServerConfig struct {
key []byte key []byte
certificatePath string certificatePath string
keyPath string keyPath string
watcher *fswatch.Watcher watcher *fsnotify.Watcher
} }
func (c *STDServerConfig) ServerName() string { func (c *STDServerConfig) ServerName() string {
@@ -87,37 +88,59 @@ func (c *STDServerConfig) Start() error {
} }
func (c *STDServerConfig) startWatcher() error { func (c *STDServerConfig) startWatcher() error {
var watchPath []string watcher, err := fsnotify.NewWatcher()
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 { if err != nil {
return err 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 c.watcher = watcher
go c.loopUpdate()
return nil return nil
} }
func (c *STDServerConfig) certificateUpdated(path string) error { func (c *STDServerConfig) loopUpdate() {
if path == c.certificatePath { 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 != "" {
certificate, err := os.ReadFile(c.certificatePath) certificate, err := os.ReadFile(c.certificatePath)
if err != nil { if err != nil {
return E.Cause(err, "reload certificate from ", c.certificatePath) return E.Cause(err, "reload certificate from ", c.certificatePath)
} }
c.certificate = certificate c.certificate = certificate
} else if path == c.keyPath { }
if c.keyPath != "" {
key, err := os.ReadFile(c.keyPath) key, err := os.ReadFile(c.keyPath)
if err != nil { if err != nil {
return E.Cause(err, "reload key from ", c.keyPath) return E.Cause(err, "reload key from ", c.keyPath)

View File

@@ -217,10 +217,18 @@ func init() {
func uTLSClientHelloID(name string) (utls.ClientHelloID, error) { func uTLSClientHelloID(name string) (utls.ClientHelloID, error) {
switch name { switch name {
case "chrome_psk", "chrome_psk_shuffle", "chrome_padding_psk_shuffle", "chrome_pq":
fallthrough
case "chrome", "": case "chrome", "":
return utls.HelloChrome_Auto, nil return utls.HelloChrome_Auto, nil
case "chrome_psk":
return utls.HelloChrome_100_PSK, nil
case "chrome_psk_shuffle":
return utls.HelloChrome_112_PSK_Shuf, nil
case "chrome_padding_psk_shuffle":
return utls.HelloChrome_114_Padding_PSK_Shuf, nil
case "chrome_pq":
return utls.HelloChrome_115_PQ, nil
case "chrome_pq_psk":
return utls.HelloChrome_115_PQ_PSK, nil
case "firefox": case "firefox":
return utls.HelloFirefox_Auto, nil return utls.HelloFirefox_Auto, nil
case "edge": case "edge":

View File

@@ -13,14 +13,14 @@ import (
"github.com/sagernet/sing/common/uot" "github.com/sagernet/sing/common/uot"
) )
var _ adapter.ConnectionRouterEx = (*Router)(nil) var _ adapter.ConnectionRouter = (*Router)(nil)
type Router struct { type Router struct {
router adapter.ConnectionRouterEx router adapter.ConnectionRouter
logger logger.ContextLogger logger logger.ContextLogger
} }
func NewRouter(router adapter.ConnectionRouterEx, logger logger.ContextLogger) *Router { func NewRouter(router adapter.ConnectionRouter, logger logger.ContextLogger) *Router {
return &Router{router, logger} return &Router{router, logger}
} }
@@ -51,36 +51,3 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
return r.router.RoutePacketConnection(ctx, conn, metadata) return r.router.RoutePacketConnection(ctx, conn, metadata)
} }
func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
switch metadata.Destination.Fqdn {
case uot.MagicAddress:
request, err := uot.ReadRequest(conn)
if err != nil {
err = E.Cause(err, "UoT read request")
r.logger.ErrorContext(ctx, "process connection from ", metadata.Source, ": ", err)
N.CloseOnHandshakeFailure(conn, onClose, err)
return
}
if request.IsConnect {
r.logger.InfoContext(ctx, "inbound UoT connect connection to ", request.Destination)
} else {
r.logger.InfoContext(ctx, "inbound UoT connection to ", request.Destination)
}
metadata.Domain = metadata.Destination.Fqdn
metadata.Destination = request.Destination
r.router.RoutePacketConnectionEx(ctx, uot.NewConn(conn, *request), metadata, onClose)
return
case uot.LegacyMagicAddress:
r.logger.InfoContext(ctx, "inbound legacy UoT connection")
metadata.Domain = metadata.Destination.Fqdn
metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()}
r.RoutePacketConnectionEx(ctx, uot.NewConn(conn, uot.Request{}), metadata, onClose)
return
}
r.router.RouteConnectionEx(ctx, conn, metadata, onClose)
}
func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
r.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
}

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