mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-14 04:38:28 +10:00
Compare commits
1 Commits
v1.8.0-bet
...
dev-sentry
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05b4d98852 |
14
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
14
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -44,7 +44,12 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Version
|
label: Version
|
||||||
description: If you are using the original command line program, please provide the output of the `sing-box version` command.
|
description: If you are using the original command line program, please provide the output of the `sing-box version` command.
|
||||||
render: shell
|
value: |-
|
||||||
|
<details>
|
||||||
|
```console
|
||||||
|
# Replace this line with the output
|
||||||
|
```
|
||||||
|
</details>
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Description
|
label: Description
|
||||||
@@ -64,4 +69,9 @@ body:
|
|||||||
If you encounter a crash with the graphical client, please provide crash logs.
|
If you encounter a crash with the graphical client, please provide crash logs.
|
||||||
For Apple platform clients, please check `Settings - View Service Log` for crash logs.
|
For Apple platform clients, please check `Settings - View Service Log` for crash logs.
|
||||||
For the Android client, please check the `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` file for crash logs.
|
For the Android client, please check the `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` file for crash logs.
|
||||||
render: shell
|
value: |-
|
||||||
|
<details>
|
||||||
|
```console
|
||||||
|
# Replace this line with logs
|
||||||
|
```
|
||||||
|
</details>
|
||||||
28
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
28
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
@@ -44,7 +44,12 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: 版本
|
label: 版本
|
||||||
description: 如果您使用原始命令行程序,请提供 `sing-box version` 命令的输出。
|
description: 如果您使用原始命令行程序,请提供 `sing-box version` 命令的输出。
|
||||||
render: shell
|
value: |-
|
||||||
|
<details>
|
||||||
|
```console
|
||||||
|
# 使用输出内容覆盖此行
|
||||||
|
```
|
||||||
|
</details>
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: 描述
|
label: 描述
|
||||||
@@ -64,18 +69,9 @@ body:
|
|||||||
如果您遭遇图形界面应用程序崩溃,请提供崩溃日志。
|
如果您遭遇图形界面应用程序崩溃,请提供崩溃日志。
|
||||||
对于 Apple 平台图形客户端程序,请检查 `Settings - View Service Log` 以导出崩溃日志。
|
对于 Apple 平台图形客户端程序,请检查 `Settings - View Service Log` 以导出崩溃日志。
|
||||||
对于 Android 图形客户端程序,请检查 `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` 文件以导出崩溃日志。
|
对于 Android 图形客户端程序,请检查 `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` 文件以导出崩溃日志。
|
||||||
render: shell
|
value: |-
|
||||||
- type: checkboxes
|
<details>
|
||||||
attributes:
|
```console
|
||||||
label: 完整性要求
|
# 使用日志内容覆盖此行
|
||||||
description: 我保证我提供了完整的可以在本地重现该问题的服务器、客户端配置文件与流程,而不是一个脱敏的复杂客户端配置文件,否则该 issue 将被关闭。
|
```
|
||||||
options:
|
</details>
|
||||||
- label: 我保证
|
|
||||||
required: true
|
|
||||||
- type: checkboxes
|
|
||||||
attributes:
|
|
||||||
label: 负责性要求
|
|
||||||
description: 我保证我阅读了文档,了解所有我编写的配置文件项的含义,而不是大量堆砌看似有用的选项或默认值,否则该 issue 将被关闭。
|
|
||||||
options:
|
|
||||||
- label: 我保证
|
|
||||||
required: true
|
|
||||||
8
.github/workflows/debug.yml
vendored
8
.github/workflows/debug.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Get latest go version
|
- name: Get latest go version
|
||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
@@ -201,7 +201,7 @@ jobs:
|
|||||||
TAGS: with_clash_api,with_quic
|
TAGS: with_clash_api,with_quic
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Get latest go version
|
- name: Get latest go version
|
||||||
|
|||||||
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Setup QEMU for Docker Buildx
|
- name: Setup QEMU for Docker Buildx
|
||||||
@@ -39,8 +39,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
platforms: linux/386,linux/amd64,linux/arm64,linux/s390x
|
platforms: linux/386,linux/amd64,linux/arm64,linux/s390x
|
||||||
target: dist
|
target: dist
|
||||||
build-args: |
|
|
||||||
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
|
||||||
tags: |
|
tags: |
|
||||||
${{ steps.tag.outputs.latest }}
|
${{ steps.tag.outputs.latest }}
|
||||||
${{ steps.tag.outputs.versioned }}
|
${{ steps.tag.outputs.versioned }}
|
||||||
|
|||||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Get latest go version
|
- name: Get latest go version
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,7 +1,6 @@
|
|||||||
/.idea/
|
/.idea/
|
||||||
/vendor/
|
/vendor/
|
||||||
/*.json
|
/*.json
|
||||||
/*.srs
|
|
||||||
/*.db
|
/*.db
|
||||||
/site/
|
/site/
|
||||||
/bin/
|
/bin/
|
||||||
|
|||||||
@@ -19,12 +19,10 @@ builds:
|
|||||||
- with_ech
|
- with_ech
|
||||||
- with_utls
|
- with_utls
|
||||||
- with_reality_server
|
- with_reality_server
|
||||||
- with_acme
|
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
targets:
|
targets:
|
||||||
- linux_386
|
|
||||||
- linux_amd64_v1
|
- linux_amd64_v1
|
||||||
- linux_amd64_v3
|
- linux_amd64_v3
|
||||||
- linux_arm64
|
- linux_arm64
|
||||||
@@ -57,12 +55,11 @@ builds:
|
|||||||
- with_ech
|
- with_ech
|
||||||
- with_utls
|
- with_utls
|
||||||
- with_reality_server
|
- with_reality_server
|
||||||
- with_acme
|
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
- GOROOT=/nix/store/kg6i737jjqs923jcijnm003h68c1dghj-go-1.20.11/share/go
|
- GOROOT=/nix/store/5h8gjl89zx8qxgc572wa3k81zplv8v4z-go-1.20.10/share/go
|
||||||
gobinary: /nix/store/kg6i737jjqs923jcijnm003h68c1dghj-go-1.20.11/bin/go
|
gobinary: /nix/store/5h8gjl89zx8qxgc572wa3k81zplv8v4z-go-1.20.10/bin/go
|
||||||
targets:
|
targets:
|
||||||
- windows_amd64_v1
|
- windows_amd64_v1
|
||||||
- windows_386
|
- windows_386
|
||||||
@@ -86,8 +83,6 @@ builds:
|
|||||||
- with_wireguard
|
- with_wireguard
|
||||||
- with_ech
|
- with_ech
|
||||||
- with_utls
|
- with_utls
|
||||||
- with_reality_server
|
|
||||||
- with_acme
|
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=1
|
- CGO_ENABLED=1
|
||||||
@@ -158,7 +153,6 @@ nfpms:
|
|||||||
formats:
|
formats:
|
||||||
- deb
|
- deb
|
||||||
- rpm
|
- rpm
|
||||||
- archlinux
|
|
||||||
priority: extra
|
priority: extra
|
||||||
contents:
|
contents:
|
||||||
- src: release/config/config.json
|
- src: release/config/config.json
|
||||||
|
|||||||
10
Dockerfile
10
Dockerfile
@@ -1,23 +1,19 @@
|
|||||||
FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS builder
|
FROM golang:1.21-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
|
||||||
ARG TARGETOS TARGETARCH
|
|
||||||
ARG GOPROXY=""
|
ARG GOPROXY=""
|
||||||
ENV GOPROXY ${GOPROXY}
|
ENV GOPROXY ${GOPROXY}
|
||||||
ENV CGO_ENABLED=0
|
ENV CGO_ENABLED=0
|
||||||
ENV GOOS=$TARGETOS
|
|
||||||
ENV GOARCH=$TARGETARCH
|
|
||||||
RUN set -ex \
|
RUN set -ex \
|
||||||
&& apk add git build-base \
|
&& apk add git build-base \
|
||||||
&& export COMMIT=$(git rev-parse --short HEAD) \
|
&& export COMMIT=$(git rev-parse --short HEAD) \
|
||||||
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
||||||
&& go build -v -trimpath -tags \
|
&& go build -v -trimpath -tags with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_clash_api,with_acme \
|
||||||
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \
|
|
||||||
-o /go/bin/sing-box \
|
-o /go/bin/sing-box \
|
||||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
FROM --platform=$TARGETPLATFORM alpine AS dist
|
FROM 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 \
|
||||||
|
|||||||
34
Makefile
34
Makefile
@@ -1,9 +1,9 @@
|
|||||||
NAME = sing-box
|
NAME = sing-box
|
||||||
COMMIT = $(shell git rev-parse --short HEAD)
|
COMMIT = $(shell git rev-parse --short HEAD)
|
||||||
TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api
|
TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_utls,with_reality_server,with_clash_api
|
||||||
TAGS_GO120 = with_quic,with_ech,with_utls
|
TAGS_GO120 = with_quic,with_ech
|
||||||
TAGS ?= $(TAGS_GO118),$(TAGS_GO120)
|
TAGS ?= $(TAGS_GO118),$(TAGS_GO120)
|
||||||
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,with_shadowsocksr
|
||||||
|
|
||||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||||
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
||||||
@@ -14,7 +14,7 @@ MAIN_PARAMS = $(PARAMS) -tags $(TAGS)
|
|||||||
MAIN = ./cmd/sing-box
|
MAIN = ./cmd/sing-box
|
||||||
PREFIX ?= $(shell go env GOPATH)
|
PREFIX ?= $(shell go env GOPATH)
|
||||||
|
|
||||||
.PHONY: test release docs
|
.PHONY: test release
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go build $(MAIN_PARAMS) $(MAIN)
|
go build $(MAIN_PARAMS) $(MAIN)
|
||||||
@@ -61,7 +61,7 @@ proto_install:
|
|||||||
release:
|
release:
|
||||||
go run ./cmd/internal/build goreleaser release --clean --skip-publish || exit 1
|
go run ./cmd/internal/build goreleaser release --clean --skip-publish || exit 1
|
||||||
mkdir dist/release
|
mkdir dist/release
|
||||||
mv dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/*.pkg.tar.zst dist/release
|
mv dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/release
|
||||||
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release
|
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release
|
||||||
rm -r dist/release
|
rm -r dist/release
|
||||||
|
|
||||||
@@ -73,21 +73,18 @@ update_android_version:
|
|||||||
go run ./cmd/internal/update_android_version
|
go run ./cmd/internal/update_android_version
|
||||||
|
|
||||||
build_android:
|
build_android:
|
||||||
cd ../sing-box-for-android && ./gradlew :app:assemblePlayRelease && ./gradlew --stop
|
cd ../sing-box-for-android && ./gradlew :app:assembleRelease && ./gradlew --stop
|
||||||
|
|
||||||
upload_android:
|
upload_android:
|
||||||
mkdir -p dist/release_android
|
mkdir -p dist/release_android
|
||||||
cp ../sing-box-for-android/app/build/outputs/apk/play/release/*.apk dist/release_android
|
cp ../sing-box-for-android/app/build/outputs/apk/release/*.apk dist/release_android
|
||||||
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release_android
|
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release_android
|
||||||
rm -rf dist/release_android
|
rm -rf dist/release_android
|
||||||
|
|
||||||
release_android: lib_android update_android_version build_android upload_android
|
release_android: lib_android update_android_version build_android upload_android
|
||||||
|
|
||||||
publish_android:
|
publish_android:
|
||||||
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle
|
cd ../sing-box-for-android && ./gradlew :app:appCenterAssembleAndUploadRelease
|
||||||
|
|
||||||
publish_android_appcenter:
|
|
||||||
cd ../sing-box-for-android && ./gradlew :app:appCenterAssembleAndUploadPlayRelease
|
|
||||||
|
|
||||||
build_ios:
|
build_ios:
|
||||||
cd ../sing-box-for-apple && \
|
cd ../sing-box-for-apple && \
|
||||||
@@ -152,8 +149,10 @@ 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_independent
|
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_independent
|
||||||
|
rm -rf dist
|
||||||
|
|
||||||
release_apple_beta: update_apple_version release_ios release_macos release_tvos
|
release_apple_beta: update_apple_version release_ios release_macos release_tvos
|
||||||
|
rm -rf dist
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@go test -v ./... && \
|
@go test -v ./... && \
|
||||||
@@ -178,17 +177,10 @@ 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.1
|
go get -v -d
|
||||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.1
|
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20230915142329-c6740b6d2950
|
||||||
|
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20230915142329-c6740b6d2950
|
||||||
|
|
||||||
docs:
|
|
||||||
mkdocs serve
|
|
||||||
|
|
||||||
publish_docs:
|
|
||||||
mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
|
|
||||||
|
|
||||||
docs_install:
|
|
||||||
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
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,11 @@
|
|||||||
package adapter
|
package adapter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/urltest"
|
"github.com/sagernet/sing-box/common/urltest"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/common/rw"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClashServer interface {
|
type ClashServer interface {
|
||||||
@@ -18,83 +13,22 @@ type ClashServer interface {
|
|||||||
PreStarter
|
PreStarter
|
||||||
Mode() string
|
Mode() string
|
||||||
ModeList() []string
|
ModeList() []string
|
||||||
|
StoreSelected() bool
|
||||||
|
StoreFakeIP() bool
|
||||||
|
CacheFile() ClashCacheFile
|
||||||
HistoryStorage() *urltest.HistoryStorage
|
HistoryStorage() *urltest.HistoryStorage
|
||||||
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
|
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
|
||||||
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker)
|
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker)
|
||||||
}
|
}
|
||||||
|
|
||||||
type CacheFile interface {
|
type ClashCacheFile interface {
|
||||||
Service
|
|
||||||
PreStarter
|
|
||||||
|
|
||||||
StoreFakeIP() bool
|
|
||||||
FakeIPStorage
|
|
||||||
|
|
||||||
LoadMode() string
|
LoadMode() string
|
||||||
StoreMode(mode string) error
|
StoreMode(mode string) error
|
||||||
LoadSelected(group string) string
|
LoadSelected(group string) string
|
||||||
StoreSelected(group string, selected string) error
|
StoreSelected(group string, selected string) error
|
||||||
LoadGroupExpand(group string) (isExpand bool, loaded bool)
|
LoadGroupExpand(group string) (isExpand bool, loaded bool)
|
||||||
StoreGroupExpand(group string, expand bool) error
|
StoreGroupExpand(group string, expand bool) error
|
||||||
LoadRuleSet(tag string) *SavedRuleSet
|
FakeIPStorage
|
||||||
SaveRuleSet(tag string, set *SavedRuleSet) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type SavedRuleSet struct {
|
|
||||||
Content []byte
|
|
||||||
LastUpdated time.Time
|
|
||||||
LastEtag string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = rw.WriteUVariant(&buffer, uint64(len(s.Content)))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
buffer.Write(s.Content)
|
|
||||||
err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = rw.WriteVString(&buffer, s.LastEtag)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return buffer.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
|
|
||||||
reader := bytes.NewReader(data)
|
|
||||||
var version uint8
|
|
||||||
err := binary.Read(reader, binary.BigEndian, &version)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
contentLen, err := rw.ReadUVariant(reader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.Content = make([]byte, contentLen)
|
|
||||||
_, err = io.ReadFull(reader, s.Content)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var lastUpdated int64
|
|
||||||
err = binary.Read(reader, binary.BigEndian, &lastUpdated)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.LastUpdated = time.Unix(lastUpdated, 0)
|
|
||||||
s.LastEtag, err = rw.ReadVString(reader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tracker interface {
|
type Tracker interface {
|
||||||
@@ -109,7 +43,7 @@ type OutboundGroup interface {
|
|||||||
|
|
||||||
type URLTestGroup interface {
|
type URLTestGroup interface {
|
||||||
OutboundGroup
|
OutboundGroup
|
||||||
URLTest(ctx context.Context) (map[string]uint16, error)
|
URLTest(ctx context.Context, url string) (map[string]uint16, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func OutboundTag(detour Outbound) string {
|
func OutboundTag(detour Outbound) string {
|
||||||
|
|||||||
@@ -46,24 +46,11 @@ type InboundContext struct {
|
|||||||
SourceGeoIPCode string
|
SourceGeoIPCode string
|
||||||
GeoIPCode string
|
GeoIPCode string
|
||||||
ProcessInfo *process.Info
|
ProcessInfo *process.Info
|
||||||
QueryType uint16
|
|
||||||
FakeIP bool
|
FakeIP bool
|
||||||
|
|
||||||
// rule cache
|
// dns cache
|
||||||
|
|
||||||
IPCIDRMatchSource bool
|
QueryType uint16
|
||||||
SourceAddressMatch bool
|
|
||||||
SourcePortMatch bool
|
|
||||||
DestinationAddressMatch bool
|
|
||||||
DestinationPortMatch bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *InboundContext) ResetRuleCache() {
|
|
||||||
c.IPCIDRMatchSource = false
|
|
||||||
c.SourceAddressMatch = false
|
|
||||||
c.SourcePortMatch = false
|
|
||||||
c.DestinationAddressMatch = false
|
|
||||||
c.DestinationPortMatch = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type inboundContextKey struct{}
|
type inboundContextKey struct{}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/geoip"
|
"github.com/sagernet/sing-box/common/geoip"
|
||||||
@@ -17,21 +17,19 @@ import (
|
|||||||
|
|
||||||
type Router interface {
|
type Router interface {
|
||||||
Service
|
Service
|
||||||
PostStarter
|
|
||||||
|
|
||||||
Outbounds() []Outbound
|
Outbounds() []Outbound
|
||||||
Outbound(tag string) (Outbound, bool)
|
Outbound(tag string) (Outbound, bool)
|
||||||
DefaultOutbound(network string) (Outbound, error)
|
DefaultOutbound(network string) Outbound
|
||||||
|
|
||||||
FakeIPStore() FakeIPStore
|
FakeIPStore() FakeIPStore
|
||||||
|
|
||||||
ConnectionRouter
|
RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||||
|
RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||||
|
|
||||||
GeoIPReader() *geoip.Reader
|
GeoIPReader() *geoip.Reader
|
||||||
LoadGeosite(code string) (Rule, error)
|
LoadGeosite(code string) (Rule, error)
|
||||||
|
|
||||||
RuleSet(tag string) (RuleSet, bool)
|
|
||||||
|
|
||||||
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
|
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
|
||||||
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
|
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
|
||||||
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
|
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
|
||||||
@@ -46,7 +44,6 @@ type Router interface {
|
|||||||
NetworkMonitor() tun.NetworkUpdateMonitor
|
NetworkMonitor() tun.NetworkUpdateMonitor
|
||||||
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
||||||
PackageManager() tun.PackageManager
|
PackageManager() tun.PackageManager
|
||||||
WIFIState() WIFIState
|
|
||||||
Rules() []Rule
|
Rules() []Rule
|
||||||
|
|
||||||
ClashServer() ClashServer
|
ClashServer() ClashServer
|
||||||
@@ -66,15 +63,11 @@ func RouterFromContext(ctx context.Context) Router {
|
|||||||
return service.FromContext[Router](ctx)
|
return service.FromContext[Router](ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
type HeadlessRule interface {
|
|
||||||
Match(metadata *InboundContext) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type Rule interface {
|
type Rule interface {
|
||||||
HeadlessRule
|
|
||||||
Service
|
Service
|
||||||
Type() string
|
Type() string
|
||||||
UpdateGeosite() error
|
UpdateGeosite() error
|
||||||
|
Match(metadata *InboundContext) bool
|
||||||
Outbound() string
|
Outbound() string
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
@@ -85,29 +78,6 @@ type DNSRule interface {
|
|||||||
RewriteTTL() *uint32
|
RewriteTTL() *uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuleSet interface {
|
|
||||||
StartContext(ctx context.Context, startContext RuleSetStartContext) error
|
|
||||||
PostStart() error
|
|
||||||
Metadata() RuleSetMetadata
|
|
||||||
Close() error
|
|
||||||
HeadlessRule
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuleSetMetadata struct {
|
|
||||||
ContainsProcessRule bool
|
|
||||||
ContainsWIFIRule bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuleSetStartContext interface {
|
|
||||||
HTTPClient(detour string, dialer N.Dialer) *http.Client
|
|
||||||
Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
type InterfaceUpdateListener interface {
|
type InterfaceUpdateListener interface {
|
||||||
InterfaceUpdated()
|
InterfaceUpdated()
|
||||||
}
|
}
|
||||||
|
|
||||||
type WIFIState struct {
|
|
||||||
SSID string
|
|
||||||
BSSID string
|
|
||||||
}
|
|
||||||
|
|||||||
114
box.go
114
box.go
@@ -9,10 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"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/libbox/platform"
|
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||||
"github.com/sagernet/sing-box/inbound"
|
"github.com/sagernet/sing-box/inbound"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
@@ -35,8 +32,7 @@ type Box struct {
|
|||||||
outbounds []adapter.Outbound
|
outbounds []adapter.Outbound
|
||||||
logFactory log.Factory
|
logFactory log.Factory
|
||||||
logger log.ContextLogger
|
logger log.ContextLogger
|
||||||
preServices1 map[string]adapter.Service
|
preServices map[string]adapter.Service
|
||||||
preServices2 map[string]adapter.Service
|
|
||||||
postServices map[string]adapter.Service
|
postServices map[string]adapter.Service
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
}
|
||||||
@@ -45,26 +41,21 @@ type Options struct {
|
|||||||
option.Options
|
option.Options
|
||||||
Context context.Context
|
Context context.Context
|
||||||
PlatformInterface platform.Interface
|
PlatformInterface platform.Interface
|
||||||
PlatformLogWriter log.PlatformWriter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(options Options) (*Box, error) {
|
func New(options Options) (*Box, error) {
|
||||||
createdAt := time.Now()
|
|
||||||
ctx := options.Context
|
ctx := options.Context
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
ctx = service.ContextWithDefaultRegistry(ctx)
|
ctx = service.ContextWithDefaultRegistry(ctx)
|
||||||
ctx = pause.ContextWithDefaultManager(ctx)
|
ctx = pause.ContextWithDefaultManager(ctx)
|
||||||
|
createdAt := time.Now()
|
||||||
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
||||||
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
||||||
var needCacheFile bool
|
|
||||||
var needClashAPI bool
|
var needClashAPI bool
|
||||||
var needV2RayAPI bool
|
var needV2RayAPI bool
|
||||||
if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil {
|
if experimentalOptions.ClashAPI != nil || options.PlatformInterface != nil {
|
||||||
needCacheFile = true
|
|
||||||
}
|
|
||||||
if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil {
|
|
||||||
needClashAPI = true
|
needClashAPI = true
|
||||||
}
|
}
|
||||||
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
||||||
@@ -80,7 +71,7 @@ func New(options Options) (*Box, error) {
|
|||||||
Observable: needClashAPI,
|
Observable: needClashAPI,
|
||||||
DefaultWriter: defaultLogWriter,
|
DefaultWriter: defaultLogWriter,
|
||||||
BaseTime: createdAt,
|
BaseTime: createdAt,
|
||||||
PlatformWriter: options.PlatformLogWriter,
|
PlatformWriter: options.PlatformInterface,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "create log factory")
|
return nil, E.Cause(err, "create log factory")
|
||||||
@@ -153,14 +144,8 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "initialize platform interface")
|
return nil, E.Cause(err, "initialize platform interface")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
preServices1 := make(map[string]adapter.Service)
|
preServices := make(map[string]adapter.Service)
|
||||||
preServices2 := make(map[string]adapter.Service)
|
|
||||||
postServices := make(map[string]adapter.Service)
|
postServices := make(map[string]adapter.Service)
|
||||||
if needCacheFile {
|
|
||||||
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
|
||||||
preServices1["cache file"] = cacheFile
|
|
||||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
|
||||||
}
|
|
||||||
if needClashAPI {
|
if needClashAPI {
|
||||||
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
||||||
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
|
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
|
||||||
@@ -169,7 +154,7 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "create clash api server")
|
return nil, E.Cause(err, "create clash api server")
|
||||||
}
|
}
|
||||||
router.SetClashServer(clashServer)
|
router.SetClashServer(clashServer)
|
||||||
preServices2["clash api"] = clashServer
|
preServices["clash api"] = clashServer
|
||||||
}
|
}
|
||||||
if needV2RayAPI {
|
if needV2RayAPI {
|
||||||
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
|
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
|
||||||
@@ -177,7 +162,7 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "create v2ray api server")
|
return nil, E.Cause(err, "create v2ray api server")
|
||||||
}
|
}
|
||||||
router.SetV2RayServer(v2rayServer)
|
router.SetV2RayServer(v2rayServer)
|
||||||
preServices2["v2ray api"] = v2rayServer
|
preServices["v2ray api"] = v2rayServer
|
||||||
}
|
}
|
||||||
return &Box{
|
return &Box{
|
||||||
router: router,
|
router: router,
|
||||||
@@ -186,8 +171,7 @@ func New(options Options) (*Box, error) {
|
|||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
logFactory: logFactory,
|
logFactory: logFactory,
|
||||||
logger: logFactory.Logger(),
|
logger: logFactory.Logger(),
|
||||||
preServices1: preServices1,
|
preServices: preServices,
|
||||||
preServices2: preServices2,
|
|
||||||
postServices: postServices,
|
postServices: postServices,
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
}, nil
|
}, nil
|
||||||
@@ -232,34 +216,16 @@ func (s *Box) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Box) preStart() error {
|
func (s *Box) preStart() error {
|
||||||
monitor := taskmonitor.New(s.logger, C.DefaultStartTimeout)
|
for serviceName, service := range s.preServices {
|
||||||
monitor.Start("start logger")
|
|
||||||
err := s.logFactory.Start()
|
|
||||||
monitor.Finish()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "start logger")
|
|
||||||
}
|
|
||||||
for serviceName, service := range s.preServices1 {
|
|
||||||
if preService, isPreService := service.(adapter.PreStarter); isPreService {
|
if preService, isPreService := service.(adapter.PreStarter); isPreService {
|
||||||
monitor.Start("pre-start ", serviceName)
|
s.logger.Trace("pre-start ", serviceName)
|
||||||
err := preService.PreStart()
|
err := preService.PreStart()
|
||||||
monitor.Finish()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "pre-start ", serviceName)
|
return E.Cause(err, "pre-starting ", serviceName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for serviceName, service := range s.preServices2 {
|
err := s.startOutbounds()
|
||||||
if preService, isPreService := service.(adapter.PreStarter); isPreService {
|
|
||||||
monitor.Start("pre-start ", serviceName)
|
|
||||||
err := preService.PreStart()
|
|
||||||
monitor.Finish()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "pre-start ", serviceName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = s.startOutbounds()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -271,13 +237,8 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for serviceName, service := range s.preServices1 {
|
for serviceName, service := range s.preServices {
|
||||||
err = service.Start()
|
s.logger.Trace("starting ", serviceName)
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "start ", serviceName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for serviceName, service := range s.preServices2 {
|
|
||||||
err = service.Start()
|
err = service.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "start ", serviceName)
|
return E.Cause(err, "start ", serviceName)
|
||||||
@@ -290,34 +251,33 @@ func (s *Box) start() error {
|
|||||||
} else {
|
} else {
|
||||||
tag = in.Tag()
|
tag = in.Tag()
|
||||||
}
|
}
|
||||||
|
s.logger.Trace("initializing inbound/", in.Type(), "[", tag, "]")
|
||||||
err = in.Start()
|
err = in.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
|
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return s.postStart()
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Box) postStart() error {
|
func (s *Box) postStart() error {
|
||||||
for serviceName, service := range s.postServices {
|
for serviceName, service := range s.postServices {
|
||||||
|
s.logger.Trace("starting ", service)
|
||||||
err := service.Start()
|
err := service.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "start ", serviceName)
|
return E.Cause(err, "start ", serviceName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, outbound := range s.outbounds {
|
for serviceName, service := range s.outbounds {
|
||||||
if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound {
|
if lateService, isLateService := service.(adapter.PostStarter); isLateService {
|
||||||
err := lateOutbound.PostStart()
|
s.logger.Trace("post-starting ", service)
|
||||||
|
err := lateService.PostStart()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "post-start outbound/", outbound.Tag())
|
return E.Cause(err, "post-start ", serviceName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err := s.router.PostStart()
|
return nil
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "post-start router")
|
|
||||||
}
|
|
||||||
return s.router.PostStart()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Box) Close() error {
|
func (s *Box) Close() error {
|
||||||
@@ -327,53 +287,41 @@ func (s *Box) Close() error {
|
|||||||
default:
|
default:
|
||||||
close(s.done)
|
close(s.done)
|
||||||
}
|
}
|
||||||
monitor := taskmonitor.New(s.logger, C.DefaultStopTimeout)
|
|
||||||
var errors error
|
var errors error
|
||||||
for serviceName, service := range s.postServices {
|
for serviceName, service := range s.postServices {
|
||||||
monitor.Start("close ", serviceName)
|
s.logger.Trace("closing ", serviceName)
|
||||||
errors = E.Append(errors, service.Close(), func(err error) error {
|
errors = E.Append(errors, service.Close(), func(err error) error {
|
||||||
return E.Cause(err, "close ", serviceName)
|
return E.Cause(err, "close ", serviceName)
|
||||||
})
|
})
|
||||||
monitor.Finish()
|
|
||||||
}
|
}
|
||||||
for i, in := range s.inbounds {
|
for i, in := range s.inbounds {
|
||||||
monitor.Start("close inbound/", in.Type(), "[", i, "]")
|
s.logger.Trace("closing inbound/", in.Type(), "[", i, "]")
|
||||||
errors = E.Append(errors, in.Close(), func(err error) error {
|
errors = E.Append(errors, in.Close(), func(err error) error {
|
||||||
return E.Cause(err, "close inbound/", in.Type(), "[", i, "]")
|
return E.Cause(err, "close inbound/", in.Type(), "[", i, "]")
|
||||||
})
|
})
|
||||||
monitor.Finish()
|
|
||||||
}
|
}
|
||||||
for i, out := range s.outbounds {
|
for i, out := range s.outbounds {
|
||||||
monitor.Start("close outbound/", out.Type(), "[", i, "]")
|
s.logger.Trace("closing outbound/", out.Type(), "[", i, "]")
|
||||||
errors = E.Append(errors, common.Close(out), func(err error) error {
|
errors = E.Append(errors, common.Close(out), func(err error) error {
|
||||||
return E.Cause(err, "close outbound/", out.Type(), "[", i, "]")
|
return E.Cause(err, "close outbound/", out.Type(), "[", i, "]")
|
||||||
})
|
})
|
||||||
monitor.Finish()
|
|
||||||
}
|
}
|
||||||
monitor.Start("close router")
|
s.logger.Trace("closing router")
|
||||||
if err := common.Close(s.router); err != nil {
|
if err := common.Close(s.router); err != nil {
|
||||||
errors = E.Append(errors, err, func(err error) error {
|
errors = E.Append(errors, err, func(err error) error {
|
||||||
return E.Cause(err, "close router")
|
return E.Cause(err, "close router")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
monitor.Finish()
|
for serviceName, service := range s.preServices {
|
||||||
for serviceName, service := range s.preServices1 {
|
s.logger.Trace("closing ", serviceName)
|
||||||
monitor.Start("close ", serviceName)
|
|
||||||
errors = E.Append(errors, service.Close(), func(err error) error {
|
errors = E.Append(errors, service.Close(), func(err error) error {
|
||||||
return E.Cause(err, "close ", serviceName)
|
return E.Cause(err, "close ", serviceName)
|
||||||
})
|
})
|
||||||
monitor.Finish()
|
|
||||||
}
|
|
||||||
for serviceName, service := range s.preServices2 {
|
|
||||||
monitor.Start("close ", serviceName)
|
|
||||||
errors = E.Append(errors, service.Close(), func(err error) error {
|
|
||||||
return E.Cause(err, "close ", serviceName)
|
|
||||||
})
|
|
||||||
monitor.Finish()
|
|
||||||
}
|
}
|
||||||
|
s.logger.Trace("closing log factory")
|
||||||
if err := common.Close(s.logFactory); err != nil {
|
if err := common.Close(s.logFactory); err != nil {
|
||||||
errors = E.Append(errors, err, func(err error) error {
|
errors = E.Append(errors, err, func(err error) error {
|
||||||
return E.Cause(err, "close logger")
|
return E.Cause(err, "close log factory")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return errors
|
return errors
|
||||||
|
|||||||
@@ -4,15 +4,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Box) startOutbounds() error {
|
func (s *Box) startOutbounds() error {
|
||||||
monitor := taskmonitor.New(s.logger, C.DefaultStartTimeout)
|
|
||||||
outboundTags := make(map[adapter.Outbound]string)
|
outboundTags := make(map[adapter.Outbound]string)
|
||||||
outbounds := make(map[string]adapter.Outbound)
|
outbounds := make(map[string]adapter.Outbound)
|
||||||
for i, outboundToStart := range s.outbounds {
|
for i, outboundToStart := range s.outbounds {
|
||||||
@@ -46,9 +43,8 @@ func (s *Box) startOutbounds() error {
|
|||||||
started[outboundTag] = true
|
started[outboundTag] = true
|
||||||
canContinue = true
|
canContinue = true
|
||||||
if starter, isStarter := outboundToStart.(common.Starter); isStarter {
|
if starter, isStarter := outboundToStart.(common.Starter); isStarter {
|
||||||
monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
s.logger.Trace("initializing outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||||
err := starter.Start()
|
err := starter.Start()
|
||||||
monitor.Finish()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
return E.Cause(err, "initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
build_shared.FindSDK()
|
build_shared.FindSDK()
|
||||||
|
|
||||||
if os.Getenv("GOPATH") == "" {
|
if os.Getenv("build.Default.GOPATH") == "" {
|
||||||
os.Setenv("GOPATH", build.Default.GOPATH)
|
os.Setenv("GOPATH", build.Default.GOPATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
_ "github.com/sagernet/gomobile"
|
_ "github.com/sagernet/gomobile/event/key"
|
||||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing/common/rw"
|
"github.com/sagernet/sing/common/rw"
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ func ReadTag() (string, error) {
|
|||||||
}
|
}
|
||||||
shortCommit, _ := shell.Exec("git", "rev-parse", "--short", "HEAD").ReadOutput()
|
shortCommit, _ := shell.Exec("git", "rev-parse", "--short", "HEAD").ReadOutput()
|
||||||
version := badversion.Parse(currentTagRev[1:])
|
version := badversion.Parse(currentTagRev[1:])
|
||||||
|
if version.PreReleaseIdentifier == "" {
|
||||||
|
version.Patch++
|
||||||
|
}
|
||||||
return version.String() + "-" + shortCommit, nil
|
return version.String() + "-" + shortCommit, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/json"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"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/json"
|
|
||||||
"github.com/sagernet/sing/common/json/badjson"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -38,10 +38,6 @@ func format() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, optionsEntry := range optionsList {
|
for _, optionsEntry := range optionsList {
|
||||||
optionsEntry.options, err = badjson.Omitempty(optionsEntry.options)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
encoder := json.NewEncoder(buffer)
|
encoder := json.NewEncoder(buffer)
|
||||||
encoder.SetIndent("", " ")
|
encoder.SetIndent("", " ")
|
||||||
@@ -73,3 +69,41 @@ func format() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formatOne(configPath string) error {
|
||||||
|
configContent, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "read config")
|
||||||
|
}
|
||||||
|
var options option.Options
|
||||||
|
err = options.UnmarshalJSON(configContent)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "decode config")
|
||||||
|
}
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
encoder := json.NewEncoder(buffer)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
err = encoder.Encode(options)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "encode config")
|
||||||
|
}
|
||||||
|
if !commandFormatFlagWrite {
|
||||||
|
os.Stdout.WriteString(buffer.String() + "\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if bytes.Equal(configContent, buffer.Bytes()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
output, err := os.Create(configPath)
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
outputPath, _ := filepath.Abs(configPath)
|
||||||
|
os.Stderr.WriteString(outputPath + "\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
|
|
||||||
"github.com/oschwald/maxminddb-golang"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
geoipReader *maxminddb.Reader
|
|
||||||
commandGeoIPFlagFile string
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandGeoip = &cobra.Command{
|
|
||||||
Use: "geoip",
|
|
||||||
Short: "GeoIP tools",
|
|
||||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := geoipPreRun()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandGeoip.PersistentFlags().StringVarP(&commandGeoIPFlagFile, "file", "f", "geoip.db", "geoip file")
|
|
||||||
mainCommand.AddCommand(commandGeoip)
|
|
||||||
}
|
|
||||||
|
|
||||||
func geoipPreRun() error {
|
|
||||||
reader, err := maxminddb.Open(commandGeoIPFlagFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if reader.Metadata.DatabaseType != "sing-geoip" {
|
|
||||||
reader.Close()
|
|
||||||
return E.New("incorrect database type, expected sing-geoip, got ", reader.Metadata.DatabaseType)
|
|
||||||
}
|
|
||||||
geoipReader = reader
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
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/oschwald/maxminddb-golang"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var flagGeoipExportOutput string
|
|
||||||
|
|
||||||
const flagGeoipExportDefaultOutput = "geoip-<country>.srs"
|
|
||||||
|
|
||||||
var commandGeoipExport = &cobra.Command{
|
|
||||||
Use: "export <country>",
|
|
||||||
Short: "Export geoip country as rule-set",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := geoipExport(args[0])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandGeoipExport.Flags().StringVarP(&flagGeoipExportOutput, "output", "o", flagGeoipExportDefaultOutput, "Output path")
|
|
||||||
commandGeoip.AddCommand(commandGeoipExport)
|
|
||||||
}
|
|
||||||
|
|
||||||
func geoipExport(countryCode string) error {
|
|
||||||
networks := geoipReader.Networks(maxminddb.SkipAliasedNetworks)
|
|
||||||
countryMap := make(map[string][]*net.IPNet)
|
|
||||||
var (
|
|
||||||
ipNet *net.IPNet
|
|
||||||
nextCountryCode string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
for networks.Next() {
|
|
||||||
ipNet, err = networks.Network(&nextCountryCode)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
countryMap[nextCountryCode] = append(countryMap[nextCountryCode], ipNet)
|
|
||||||
}
|
|
||||||
ipNets := countryMap[strings.ToLower(countryCode)]
|
|
||||||
if len(ipNets) == 0 {
|
|
||||||
return E.New("country code not found: ", countryCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
outputFile *os.File
|
|
||||||
outputWriter io.Writer
|
|
||||||
)
|
|
||||||
if flagGeoipExportOutput == "stdout" {
|
|
||||||
outputWriter = os.Stdout
|
|
||||||
} else if flagGeoipExportOutput == flagGeoipExportDefaultOutput {
|
|
||||||
outputFile, err = os.Create("geoip-" + countryCode + ".json")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer outputFile.Close()
|
|
||||||
outputWriter = outputFile
|
|
||||||
} else {
|
|
||||||
outputFile, err = os.Create(flagGeoipExportOutput)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer outputFile.Close()
|
|
||||||
outputWriter = outputFile
|
|
||||||
}
|
|
||||||
|
|
||||||
encoder := json.NewEncoder(outputWriter)
|
|
||||||
encoder.SetIndent("", " ")
|
|
||||||
var headlessRule option.DefaultHeadlessRule
|
|
||||||
headlessRule.IPCIDR = make([]string, 0, len(ipNets))
|
|
||||||
for _, cidr := range ipNets {
|
|
||||||
headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String())
|
|
||||||
}
|
|
||||||
var plainRuleSet option.PlainRuleSetCompat
|
|
||||||
plainRuleSet.Version = C.RuleSetVersion1
|
|
||||||
plainRuleSet.Options.Rules = []option.HeadlessRule{
|
|
||||||
{
|
|
||||||
Type: C.RuleTypeDefault,
|
|
||||||
DefaultOptions: headlessRule,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return encoder.Encode(plainRuleSet)
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandGeoipList = &cobra.Command{
|
|
||||||
Use: "list",
|
|
||||||
Short: "List geoip country codes",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := listGeoip()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandGeoip.AddCommand(commandGeoipList)
|
|
||||||
}
|
|
||||||
|
|
||||||
func listGeoip() error {
|
|
||||||
for _, code := range geoipReader.Metadata.Languages {
|
|
||||||
os.Stdout.WriteString(code + "\n")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/netip"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandGeoipLookup = &cobra.Command{
|
|
||||||
Use: "lookup <address>",
|
|
||||||
Short: "Lookup if an IP address is contained in the GeoIP database",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := geoipLookup(args[0])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandGeoip.AddCommand(commandGeoipLookup)
|
|
||||||
}
|
|
||||||
|
|
||||||
func geoipLookup(address string) error {
|
|
||||||
addr, err := netip.ParseAddr(address)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "parse address")
|
|
||||||
}
|
|
||||||
if !N.IsPublicAddr(addr) {
|
|
||||||
os.Stdout.WriteString("private\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var code string
|
|
||||||
_ = geoipReader.Lookup(addr.AsSlice(), &code)
|
|
||||||
if code != "" {
|
|
||||||
os.Stdout.WriteString(code + "\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
os.Stdout.WriteString("unknown\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sagernet/sing-box/common/geosite"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
commandGeoSiteFlagFile string
|
|
||||||
geositeReader *geosite.Reader
|
|
||||||
geositeCodeList []string
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandGeoSite = &cobra.Command{
|
|
||||||
Use: "geosite",
|
|
||||||
Short: "Geosite tools",
|
|
||||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := geositePreRun()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandGeoSite.PersistentFlags().StringVarP(&commandGeoSiteFlagFile, "file", "f", "geosite.db", "geosite file")
|
|
||||||
mainCommand.AddCommand(commandGeoSite)
|
|
||||||
}
|
|
||||||
|
|
||||||
func geositePreRun() error {
|
|
||||||
reader, codeList, err := geosite.Open(commandGeoSiteFlagFile)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "open geosite file")
|
|
||||||
}
|
|
||||||
geositeReader = reader
|
|
||||||
geositeCodeList = codeList
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/geosite"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandGeositeExportOutput string
|
|
||||||
|
|
||||||
const commandGeositeExportDefaultOutput = "geosite-<category>.json"
|
|
||||||
|
|
||||||
var commandGeositeExport = &cobra.Command{
|
|
||||||
Use: "export <category>",
|
|
||||||
Short: "Export geosite category as rule-set",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := geositeExport(args[0])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandGeositeExport.Flags().StringVarP(&commandGeositeExportOutput, "output", "o", commandGeositeExportDefaultOutput, "Output path")
|
|
||||||
commandGeoSite.AddCommand(commandGeositeExport)
|
|
||||||
}
|
|
||||||
|
|
||||||
func geositeExport(category string) error {
|
|
||||||
sourceSet, err := geositeReader.Read(category)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
outputFile *os.File
|
|
||||||
outputWriter io.Writer
|
|
||||||
)
|
|
||||||
if commandGeositeExportOutput == "stdout" {
|
|
||||||
outputWriter = os.Stdout
|
|
||||||
} else if commandGeositeExportOutput == commandGeositeExportDefaultOutput {
|
|
||||||
outputFile, err = os.Create("geosite-" + category + ".json")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer outputFile.Close()
|
|
||||||
outputWriter = outputFile
|
|
||||||
} else {
|
|
||||||
outputFile, err = os.Create(commandGeositeExportOutput)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer outputFile.Close()
|
|
||||||
outputWriter = outputFile
|
|
||||||
}
|
|
||||||
|
|
||||||
encoder := json.NewEncoder(outputWriter)
|
|
||||||
encoder.SetIndent("", " ")
|
|
||||||
var headlessRule option.DefaultHeadlessRule
|
|
||||||
defaultRule := geosite.Compile(sourceSet)
|
|
||||||
headlessRule.Domain = defaultRule.Domain
|
|
||||||
headlessRule.DomainSuffix = defaultRule.DomainSuffix
|
|
||||||
headlessRule.DomainKeyword = defaultRule.DomainKeyword
|
|
||||||
headlessRule.DomainRegex = defaultRule.DomainRegex
|
|
||||||
var plainRuleSet option.PlainRuleSetCompat
|
|
||||||
plainRuleSet.Version = C.RuleSetVersion1
|
|
||||||
plainRuleSet.Options.Rules = []option.HeadlessRule{
|
|
||||||
{
|
|
||||||
Type: C.RuleTypeDefault,
|
|
||||||
DefaultOptions: headlessRule,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return encoder.Encode(plainRuleSet)
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandGeositeList = &cobra.Command{
|
|
||||||
Use: "list <category>",
|
|
||||||
Short: "List geosite categories",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := geositeList()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandGeoSite.AddCommand(commandGeositeList)
|
|
||||||
}
|
|
||||||
|
|
||||||
func geositeList() error {
|
|
||||||
var geositeEntry []struct {
|
|
||||||
category string
|
|
||||||
items int
|
|
||||||
}
|
|
||||||
for _, category := range geositeCodeList {
|
|
||||||
sourceSet, err := geositeReader.Read(category)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
geositeEntry = append(geositeEntry, struct {
|
|
||||||
category string
|
|
||||||
items int
|
|
||||||
}{category, len(sourceSet)})
|
|
||||||
}
|
|
||||||
sort.SliceStable(geositeEntry, func(i, j int) bool {
|
|
||||||
return geositeEntry[i].items < geositeEntry[j].items
|
|
||||||
})
|
|
||||||
for _, entry := range geositeEntry {
|
|
||||||
os.Stdout.WriteString(F.ToString(entry.category, " (", entry.items, ")\n"))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandGeositeLookup = &cobra.Command{
|
|
||||||
Use: "lookup [category] <domain>",
|
|
||||||
Short: "Check if a domain is in the geosite",
|
|
||||||
Args: cobra.RangeArgs(1, 2),
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
var (
|
|
||||||
source string
|
|
||||||
target string
|
|
||||||
)
|
|
||||||
switch len(args) {
|
|
||||||
case 1:
|
|
||||||
target = args[0]
|
|
||||||
case 2:
|
|
||||||
source = args[0]
|
|
||||||
target = args[1]
|
|
||||||
}
|
|
||||||
err := geositeLookup(source, target)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandGeoSite.AddCommand(commandGeositeLookup)
|
|
||||||
}
|
|
||||||
|
|
||||||
func geositeLookup(source string, target string) error {
|
|
||||||
var sourceMatcherList []struct {
|
|
||||||
code string
|
|
||||||
matcher *searchGeositeMatcher
|
|
||||||
}
|
|
||||||
if source != "" {
|
|
||||||
sourceSet, err := geositeReader.Read(source)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sourceMatcher, err := newSearchGeositeMatcher(sourceSet)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "compile code: "+source)
|
|
||||||
}
|
|
||||||
sourceMatcherList = []struct {
|
|
||||||
code string
|
|
||||||
matcher *searchGeositeMatcher
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
code: source,
|
|
||||||
matcher: sourceMatcher,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
for _, code := range geositeCodeList {
|
|
||||||
sourceSet, err := geositeReader.Read(code)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sourceMatcher, err := newSearchGeositeMatcher(sourceSet)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "compile code: "+code)
|
|
||||||
}
|
|
||||||
sourceMatcherList = append(sourceMatcherList, struct {
|
|
||||||
code string
|
|
||||||
matcher *searchGeositeMatcher
|
|
||||||
}{
|
|
||||||
code: code,
|
|
||||||
matcher: sourceMatcher,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.SliceStable(sourceMatcherList, func(i, j int) bool {
|
|
||||||
return sourceMatcherList[i].code < sourceMatcherList[j].code
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, matcherItem := range sourceMatcherList {
|
|
||||||
if matchRule := matcherItem.matcher.Match(target); matchRule != "" {
|
|
||||||
os.Stdout.WriteString("Match code (")
|
|
||||||
os.Stdout.WriteString(matcherItem.code)
|
|
||||||
os.Stdout.WriteString(") ")
|
|
||||||
os.Stdout.WriteString(matchRule)
|
|
||||||
os.Stdout.WriteString("\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/geosite"
|
|
||||||
)
|
|
||||||
|
|
||||||
type searchGeositeMatcher struct {
|
|
||||||
domainMap map[string]bool
|
|
||||||
suffixList []string
|
|
||||||
keywordList []string
|
|
||||||
regexList []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSearchGeositeMatcher(items []geosite.Item) (*searchGeositeMatcher, error) {
|
|
||||||
options := geosite.Compile(items)
|
|
||||||
domainMap := make(map[string]bool)
|
|
||||||
for _, domain := range options.Domain {
|
|
||||||
domainMap[domain] = true
|
|
||||||
}
|
|
||||||
rule := &searchGeositeMatcher{
|
|
||||||
domainMap: domainMap,
|
|
||||||
suffixList: options.DomainSuffix,
|
|
||||||
keywordList: options.DomainKeyword,
|
|
||||||
regexList: options.DomainRegex,
|
|
||||||
}
|
|
||||||
return rule, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *searchGeositeMatcher) Match(domain string) string {
|
|
||||||
if r.domainMap[domain] {
|
|
||||||
return "domain=" + domain
|
|
||||||
}
|
|
||||||
for _, suffix := range r.suffixList {
|
|
||||||
if strings.HasSuffix(domain, suffix) {
|
|
||||||
return "domain_suffix=" + suffix
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, keyword := range r.keywordList {
|
|
||||||
if strings.Contains(domain, keyword) {
|
|
||||||
return "domain_keyword=" + keyword
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, regexStr := range r.regexList {
|
|
||||||
regex, err := regexp.Compile(regexStr)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if regex.MatchString(domain) {
|
|
||||||
return "domain_regex=" + regexStr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
@@ -6,19 +6,19 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/json"
|
||||||
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/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/json"
|
|
||||||
"github.com/sagernet/sing/common/rw"
|
"github.com/sagernet/sing/common/rw"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var commandMerge = &cobra.Command{
|
var commandMerge = &cobra.Command{
|
||||||
Use: "merge <output>",
|
Use: "merge [output]",
|
||||||
Short: "Merge configurations",
|
Short: "Merge configurations",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
err := merge(args[0])
|
err := merge(args[0])
|
||||||
@@ -65,26 +65,50 @@ func merge(outputPath string) error {
|
|||||||
|
|
||||||
func mergePathResources(options *option.Options) error {
|
func mergePathResources(options *option.Options) error {
|
||||||
for index, inbound := range options.Inbounds {
|
for index, inbound := range options.Inbounds {
|
||||||
rawOptions, err := inbound.RawOptions()
|
switch inbound.Type {
|
||||||
if err != nil {
|
case C.TypeHTTP:
|
||||||
return err
|
inbound.HTTPOptions.TLS = mergeTLSInboundOptions(inbound.HTTPOptions.TLS)
|
||||||
}
|
case C.TypeMixed:
|
||||||
if tlsOptions, containsTLSOptions := rawOptions.(option.InboundTLSOptionsWrapper); containsTLSOptions {
|
inbound.MixedOptions.TLS = mergeTLSInboundOptions(inbound.MixedOptions.TLS)
|
||||||
tlsOptions.ReplaceInboundTLSOptions(mergeTLSInboundOptions(tlsOptions.TakeInboundTLSOptions()))
|
case C.TypeVMess:
|
||||||
|
inbound.VMessOptions.TLS = mergeTLSInboundOptions(inbound.VMessOptions.TLS)
|
||||||
|
case C.TypeTrojan:
|
||||||
|
inbound.TrojanOptions.TLS = mergeTLSInboundOptions(inbound.TrojanOptions.TLS)
|
||||||
|
case C.TypeNaive:
|
||||||
|
inbound.NaiveOptions.TLS = mergeTLSInboundOptions(inbound.NaiveOptions.TLS)
|
||||||
|
case C.TypeHysteria:
|
||||||
|
inbound.HysteriaOptions.TLS = mergeTLSInboundOptions(inbound.HysteriaOptions.TLS)
|
||||||
|
case C.TypeVLESS:
|
||||||
|
inbound.VLESSOptions.TLS = mergeTLSInboundOptions(inbound.VLESSOptions.TLS)
|
||||||
|
case C.TypeTUIC:
|
||||||
|
inbound.TUICOptions.TLS = mergeTLSInboundOptions(inbound.TUICOptions.TLS)
|
||||||
|
case C.TypeHysteria2:
|
||||||
|
inbound.Hysteria2Options.TLS = mergeTLSInboundOptions(inbound.Hysteria2Options.TLS)
|
||||||
|
default:
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
options.Inbounds[index] = inbound
|
options.Inbounds[index] = inbound
|
||||||
}
|
}
|
||||||
for index, 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.TypeHTTP:
|
||||||
|
outbound.HTTPOptions.TLS = mergeTLSOutboundOptions(outbound.HTTPOptions.TLS)
|
||||||
|
case C.TypeVMess:
|
||||||
|
outbound.VMessOptions.TLS = mergeTLSOutboundOptions(outbound.VMessOptions.TLS)
|
||||||
|
case C.TypeTrojan:
|
||||||
|
outbound.TrojanOptions.TLS = mergeTLSOutboundOptions(outbound.TrojanOptions.TLS)
|
||||||
|
case C.TypeHysteria:
|
||||||
|
outbound.HysteriaOptions.TLS = mergeTLSOutboundOptions(outbound.HysteriaOptions.TLS)
|
||||||
case C.TypeSSH:
|
case C.TypeSSH:
|
||||||
outbound.SSHOptions = mergeSSHOutboundOptions(outbound.SSHOptions)
|
outbound.SSHOptions = mergeSSHOutboundOptions(outbound.SSHOptions)
|
||||||
}
|
case C.TypeVLESS:
|
||||||
if tlsOptions, containsTLSOptions := rawOptions.(option.OutboundTLSOptionsWrapper); containsTLSOptions {
|
outbound.VLESSOptions.TLS = mergeTLSOutboundOptions(outbound.VLESSOptions.TLS)
|
||||||
tlsOptions.ReplaceOutboundTLSOptions(mergeTLSOutboundOptions(tlsOptions.TakeOutboundTLSOptions()))
|
case C.TypeTUIC:
|
||||||
|
outbound.TUICOptions.TLS = mergeTLSOutboundOptions(outbound.TUICOptions.TLS)
|
||||||
|
case C.TypeHysteria2:
|
||||||
|
outbound.Hysteria2Options.TLS = mergeTLSOutboundOptions(outbound.Hysteria2Options.TLS)
|
||||||
|
default:
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
options.Outbounds[index] = outbound
|
options.Outbounds[index] = outbound
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandRuleSet = &cobra.Command{
|
|
||||||
Use: "rule-set",
|
|
||||||
Short: "Manage rule sets",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
mainCommand.AddCommand(commandRuleSet)
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/srs"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common/json"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var flagRuleSetCompileOutput string
|
|
||||||
|
|
||||||
const flagRuleSetCompileDefaultOutput = "<file_name>.srs"
|
|
||||||
|
|
||||||
var commandRuleSetCompile = &cobra.Command{
|
|
||||||
Use: "compile [source-path]",
|
|
||||||
Short: "Compile rule-set json to binary",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := compileRuleSet(args[0])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandRuleSet.AddCommand(commandRuleSetCompile)
|
|
||||||
commandRuleSetCompile.Flags().StringVarP(&flagRuleSetCompileOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file")
|
|
||||||
}
|
|
||||||
|
|
||||||
func compileRuleSet(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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
decoder := json.NewDecoder(json.NewCommentFilter(reader))
|
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
var plainRuleSet option.PlainRuleSetCompat
|
|
||||||
err = decoder.Decode(&plainRuleSet)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ruleSet := plainRuleSet.Upgrade()
|
|
||||||
var outputPath string
|
|
||||||
if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput {
|
|
||||||
if strings.HasSuffix(sourcePath, ".json") {
|
|
||||||
outputPath = sourcePath[:len(sourcePath)-5] + ".srs"
|
|
||||||
} else {
|
|
||||||
outputPath = sourcePath + ".srs"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
outputPath = flagRuleSetCompileOutput
|
|
||||||
}
|
|
||||||
outputFile, err := os.Create(outputPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = srs.Write(outputFile, ruleSet)
|
|
||||||
if err != nil {
|
|
||||||
outputFile.Close()
|
|
||||||
os.Remove(outputPath)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
outputFile.Close()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"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 commandRuleSetFormatFlagWrite bool
|
|
||||||
|
|
||||||
var commandRuleSetFormat = &cobra.Command{
|
|
||||||
Use: "format <source-path>",
|
|
||||||
Short: "Format rule-set json",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := formatRuleSet(args[0])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandRuleSetFormat.Flags().BoolVarP(&commandRuleSetFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout")
|
|
||||||
commandRuleSet.AddCommand(commandRuleSetFormat)
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatRuleSet(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
|
|
||||||
}
|
|
||||||
decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content)))
|
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
var plainRuleSet option.PlainRuleSetCompat
|
|
||||||
err = decoder.Decode(&plainRuleSet)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ruleSet := plainRuleSet.Upgrade()
|
|
||||||
buffer := new(bytes.Buffer)
|
|
||||||
encoder := json.NewEncoder(buffer)
|
|
||||||
encoder.SetIndent("", " ")
|
|
||||||
err = encoder.Encode(ruleSet)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "encode config")
|
|
||||||
}
|
|
||||||
outputPath, _ := filepath.Abs(sourcePath)
|
|
||||||
if !commandRuleSetFormatFlagWrite || 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
|
|
||||||
}
|
|
||||||
@@ -13,12 +13,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box"
|
"github.com/sagernet/sing-box"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
"github.com/sagernet/sing-box/common/badjsonmerge"
|
||||||
"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/json"
|
|
||||||
"github.com/sagernet/sing/common/json/badjson"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -57,7 +55,8 @@ 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.UnmarshalExtended[option.Options](configContent)
|
var options option.Options
|
||||||
|
err = options.UnmarshalJSON(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)
|
||||||
}
|
}
|
||||||
@@ -107,18 +106,13 @@ func readConfigAndMerge() (option.Options, error) {
|
|||||||
if len(optionsList) == 1 {
|
if len(optionsList) == 1 {
|
||||||
return optionsList[0].options, nil
|
return optionsList[0].options, nil
|
||||||
}
|
}
|
||||||
var mergedMessage json.RawMessage
|
var mergedOptions option.Options
|
||||||
for _, options := range optionsList {
|
for _, options := range optionsList {
|
||||||
mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage)
|
mergedOptions, err = badjsonmerge.MergeOptions(options.options, mergedOptions)
|
||||||
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
|
|
||||||
err = mergedOptions.UnmarshalJSON(mergedMessage)
|
|
||||||
if err != nil {
|
|
||||||
return option.Options{}, E.Cause(err, "unmarshal merged config")
|
|
||||||
}
|
|
||||||
return mergedOptions, nil
|
return mergedOptions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +193,7 @@ func run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func closeMonitor(ctx context.Context) {
|
func closeMonitor(ctx context.Context) {
|
||||||
time.Sleep(C.DefaultStopFatalTimeout)
|
time.Sleep(3 * time.Second)
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ func createPreStartedClient() (*box.Box, error) {
|
|||||||
|
|
||||||
func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) {
|
func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) {
|
||||||
if outboundTag == "" {
|
if outboundTag == "" {
|
||||||
return instance.Router().DefaultOutbound(N.NetworkName(network))
|
outbound := instance.Router().DefaultOutbound(N.NetworkName(network))
|
||||||
|
if outbound == nil {
|
||||||
|
return nil, E.New("missing default outbound")
|
||||||
|
}
|
||||||
|
return outbound, nil
|
||||||
} else {
|
} else {
|
||||||
outbound, loaded := instance.Router().Outbound(outboundTag)
|
outbound, loaded := instance.Router().Outbound(outboundTag)
|
||||||
if !loaded {
|
if !loaded {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
var commandConnectFlagNetwork string
|
var commandConnectFlagNetwork string
|
||||||
|
|
||||||
var commandConnect = &cobra.Command{
|
var commandConnect = &cobra.Command{
|
||||||
Use: "connect <address>",
|
Use: "connect [address]",
|
||||||
Short: "Connect to an address",
|
Short: "Connect to an address",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"github.com/getsentry/sentry-go"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ var (
|
|||||||
configDirectories []string
|
configDirectories []string
|
||||||
workingDir string
|
workingDir string
|
||||||
disableColor bool
|
disableColor bool
|
||||||
|
enableDebug bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var mainCommand = &cobra.Command{
|
var mainCommand = &cobra.Command{
|
||||||
@@ -28,17 +29,30 @@ func init() {
|
|||||||
mainCommand.PersistentFlags().StringArrayVarP(&configDirectories, "config-directory", "C", nil, "set configuration directory path")
|
mainCommand.PersistentFlags().StringArrayVarP(&configDirectories, "config-directory", "C", nil, "set configuration directory path")
|
||||||
mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
|
mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
|
||||||
mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
|
mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
|
||||||
|
mainCommand.PersistentFlags().BoolVarP(&enableDebug, "debug", "", false, "enable sentry debug mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := mainCommand.Execute(); err != nil {
|
if err := mainCommand.Execute(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if enableDebug {
|
||||||
|
err := sentry.Init(sentry.ClientOptions{
|
||||||
|
Dsn: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("sentry.Init: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer sentry.Flush(2 * time.Second)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func preRun(cmd *cobra.Command, args []string) {
|
func preRun(cmd *cobra.Command, args []string) {
|
||||||
if disableColor {
|
if disableColor {
|
||||||
log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger())
|
log.SetStdLogger(log.NewFactory(log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, nil).Logger())
|
||||||
}
|
}
|
||||||
if workingDir != "" {
|
if workingDir != "" {
|
||||||
_, err := os.Stat(workingDir)
|
_, err := os.Stat(workingDir)
|
||||||
|
|||||||
46
common/badjson/array.go
Normal file
46
common/badjson/array.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package badjson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/json"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JSONArray []any
|
||||||
|
|
||||||
|
func (a JSONArray) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal([]any(a))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *JSONArray) UnmarshalJSON(content []byte) error {
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(content))
|
||||||
|
arrayStart, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if arrayStart != json.Delim('[') {
|
||||||
|
return E.New("excepted array start, but got ", arrayStart)
|
||||||
|
}
|
||||||
|
err = a.decodeJSON(decoder)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
arrayEnd, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if arrayEnd != json.Delim(']') {
|
||||||
|
return E.New("excepted array end, but got ", arrayEnd)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *JSONArray) decodeJSON(decoder *json.Decoder) error {
|
||||||
|
for decoder.More() {
|
||||||
|
item, err := decodeJSON(decoder)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*a = append(*a, item)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
54
common/badjson/json.go
Normal file
54
common/badjson/json.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package badjson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/json"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Decode(content []byte) (any, error) {
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(content))
|
||||||
|
return decodeJSON(decoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeJSON(decoder *json.Decoder) (any, error) {
|
||||||
|
rawToken, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch token := rawToken.(type) {
|
||||||
|
case json.Delim:
|
||||||
|
switch token {
|
||||||
|
case '{':
|
||||||
|
var object JSONObject
|
||||||
|
err = object.decodeJSON(decoder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rawToken, err = decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if rawToken != json.Delim('}') {
|
||||||
|
return nil, E.New("excepted object end, but got ", rawToken)
|
||||||
|
}
|
||||||
|
return &object, nil
|
||||||
|
case '[':
|
||||||
|
var array JSONArray
|
||||||
|
err = array.decodeJSON(decoder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rawToken, err = decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if rawToken != json.Delim(']') {
|
||||||
|
return nil, E.New("excepted array end, but got ", rawToken)
|
||||||
|
}
|
||||||
|
return array, nil
|
||||||
|
default:
|
||||||
|
return nil, E.New("excepted object or array end: ", token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rawToken, nil
|
||||||
|
}
|
||||||
79
common/badjson/object.go
Normal file
79
common/badjson/object.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package badjson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/json"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/x/linkedhashmap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JSONObject struct {
|
||||||
|
linkedhashmap.Map[string, any]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m JSONObject) MarshalJSON() ([]byte, error) {
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
buffer.WriteString("{")
|
||||||
|
items := m.Entries()
|
||||||
|
iLen := len(items)
|
||||||
|
for i, entry := range items {
|
||||||
|
keyContent, err := json.Marshal(entry.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
buffer.WriteString(strings.TrimSpace(string(keyContent)))
|
||||||
|
buffer.WriteString(": ")
|
||||||
|
valueContent, err := json.Marshal(entry.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
buffer.WriteString(strings.TrimSpace(string(valueContent)))
|
||||||
|
if i < iLen-1 {
|
||||||
|
buffer.WriteString(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer.WriteString("}")
|
||||||
|
return buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *JSONObject) UnmarshalJSON(content []byte) error {
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(content))
|
||||||
|
m.Clear()
|
||||||
|
objectStart, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if objectStart != json.Delim('{') {
|
||||||
|
return E.New("expected json object start, but starts with ", objectStart)
|
||||||
|
}
|
||||||
|
err = m.decodeJSON(decoder)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "decode json object content")
|
||||||
|
}
|
||||||
|
objectEnd, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if objectEnd != json.Delim('}') {
|
||||||
|
return E.New("expected json object end, but ends with ", objectEnd)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *JSONObject) decodeJSON(decoder *json.Decoder) error {
|
||||||
|
for decoder.More() {
|
||||||
|
var entryKey string
|
||||||
|
keyToken, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
entryKey = keyToken.(string)
|
||||||
|
var entryValue any
|
||||||
|
entryValue, err = decodeJSON(decoder)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "decode value for ", entryKey)
|
||||||
|
}
|
||||||
|
m.Put(entryKey, entryValue)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
80
common/badjsonmerge/merge.go
Normal file
80
common/badjsonmerge/merge.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package badjsonmerge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/badjson"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MergeOptions(source option.Options, destination option.Options) (option.Options, error) {
|
||||||
|
rawSource, err := json.Marshal(source)
|
||||||
|
if err != nil {
|
||||||
|
return option.Options{}, E.Cause(err, "marshal source")
|
||||||
|
}
|
||||||
|
rawDestination, err := json.Marshal(destination)
|
||||||
|
if err != nil {
|
||||||
|
return option.Options{}, E.Cause(err, "marshal destination")
|
||||||
|
}
|
||||||
|
rawMerged, err := MergeJSON(rawSource, rawDestination)
|
||||||
|
if err != nil {
|
||||||
|
return option.Options{}, E.Cause(err, "merge options")
|
||||||
|
}
|
||||||
|
var merged option.Options
|
||||||
|
err = json.Unmarshal(rawMerged, &merged)
|
||||||
|
if err != nil {
|
||||||
|
return option.Options{}, E.Cause(err, "unmarshal merged options")
|
||||||
|
}
|
||||||
|
return merged, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MergeJSON(rawSource json.RawMessage, rawDestination json.RawMessage) (json.RawMessage, error) {
|
||||||
|
source, err := badjson.Decode(rawSource)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "decode source")
|
||||||
|
}
|
||||||
|
destination, err := badjson.Decode(rawDestination)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "decode destination")
|
||||||
|
}
|
||||||
|
merged, err := mergeJSON(source, destination)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return json.Marshal(merged)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeJSON(anySource any, anyDestination any) (any, error) {
|
||||||
|
switch destination := anyDestination.(type) {
|
||||||
|
case badjson.JSONArray:
|
||||||
|
switch source := anySource.(type) {
|
||||||
|
case badjson.JSONArray:
|
||||||
|
destination = append(destination, source...)
|
||||||
|
default:
|
||||||
|
destination = append(destination, source)
|
||||||
|
}
|
||||||
|
return destination, nil
|
||||||
|
case *badjson.JSONObject:
|
||||||
|
switch source := anySource.(type) {
|
||||||
|
case *badjson.JSONObject:
|
||||||
|
for _, entry := range source.Entries() {
|
||||||
|
oldValue, loaded := destination.Get(entry.Key)
|
||||||
|
if loaded {
|
||||||
|
var err error
|
||||||
|
entry.Value, err = mergeJSON(entry.Value, oldValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "merge object item ", entry.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
destination.Put(entry.Key, entry.Value)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, E.New("cannot merge json object into ", reflect.TypeOf(destination))
|
||||||
|
}
|
||||||
|
return destination, nil
|
||||||
|
default:
|
||||||
|
return destination, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
59
common/badjsonmerge/merge_test.go
Normal file
59
common/badjsonmerge/merge_test.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package badjsonmerge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMergeJSON(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
options := option.Options{
|
||||||
|
Log: &option.LogOptions{
|
||||||
|
Level: "info",
|
||||||
|
},
|
||||||
|
Route: &option.RouteOptions{
|
||||||
|
Rules: []option.Rule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
Network: []string{N.NetworkTCP},
|
||||||
|
Outbound: "direct",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
anotherOptions := option.Options{
|
||||||
|
Outbounds: []option.Outbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeDirect,
|
||||||
|
Tag: "direct",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
thirdOptions := option.Options{
|
||||||
|
Route: &option.RouteOptions{
|
||||||
|
Rules: []option.Rule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
Network: []string{N.NetworkUDP},
|
||||||
|
Outbound: "direct",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mergeOptions, err := MergeOptions(options, anotherOptions)
|
||||||
|
require.NoError(t, err)
|
||||||
|
mergeOptions, err = MergeOptions(thirdOptions, mergeOptions)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "info", mergeOptions.Log.Level)
|
||||||
|
require.Equal(t, 2, len(mergeOptions.Route.Rules))
|
||||||
|
require.Equal(t, C.TypeDirect, mergeOptions.Outbounds[0].Type)
|
||||||
|
}
|
||||||
233
common/badtls/badtls.go
Normal file
233
common/badtls/badtls.go
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
//go:build go1.20 && !go1.21
|
||||||
|
|
||||||
|
package badtls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Conn struct {
|
||||||
|
*tls.Conn
|
||||||
|
writer N.ExtendedWriter
|
||||||
|
isHandshakeComplete *atomic.Bool
|
||||||
|
activeCall *atomic.Int32
|
||||||
|
closeNotifySent *bool
|
||||||
|
version *uint16
|
||||||
|
rand io.Reader
|
||||||
|
halfAccess *sync.Mutex
|
||||||
|
halfError *error
|
||||||
|
cipher cipher.AEAD
|
||||||
|
explicitNonceLen int
|
||||||
|
halfPtr uintptr
|
||||||
|
halfSeq []byte
|
||||||
|
halfScratchBuf []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func TryCreate(conn aTLS.Conn) aTLS.Conn {
|
||||||
|
tlsConn, ok := conn.(*tls.Conn)
|
||||||
|
if !ok {
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
badConn, err := Create(tlsConn)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("initialize badtls: ", err)
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
return badConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func Create(conn *tls.Conn) (aTLS.Conn, error) {
|
||||||
|
rawConn := reflect.Indirect(reflect.ValueOf(conn))
|
||||||
|
rawIsHandshakeComplete := rawConn.FieldByName("isHandshakeComplete")
|
||||||
|
if !rawIsHandshakeComplete.IsValid() || rawIsHandshakeComplete.Kind() != reflect.Struct {
|
||||||
|
return nil, E.New("badtls: invalid isHandshakeComplete")
|
||||||
|
}
|
||||||
|
isHandshakeComplete := (*atomic.Bool)(unsafe.Pointer(rawIsHandshakeComplete.UnsafeAddr()))
|
||||||
|
if !isHandshakeComplete.Load() {
|
||||||
|
return nil, E.New("handshake not finished")
|
||||||
|
}
|
||||||
|
rawActiveCall := rawConn.FieldByName("activeCall")
|
||||||
|
if !rawActiveCall.IsValid() || rawActiveCall.Kind() != reflect.Struct {
|
||||||
|
return nil, E.New("badtls: invalid active call")
|
||||||
|
}
|
||||||
|
activeCall := (*atomic.Int32)(unsafe.Pointer(rawActiveCall.UnsafeAddr()))
|
||||||
|
rawHalfConn := rawConn.FieldByName("out")
|
||||||
|
if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct {
|
||||||
|
return nil, E.New("badtls: invalid half conn")
|
||||||
|
}
|
||||||
|
rawVersion := rawConn.FieldByName("vers")
|
||||||
|
if !rawVersion.IsValid() || rawVersion.Kind() != reflect.Uint16 {
|
||||||
|
return nil, E.New("badtls: invalid version")
|
||||||
|
}
|
||||||
|
version := (*uint16)(unsafe.Pointer(rawVersion.UnsafeAddr()))
|
||||||
|
rawCloseNotifySent := rawConn.FieldByName("closeNotifySent")
|
||||||
|
if !rawCloseNotifySent.IsValid() || rawCloseNotifySent.Kind() != reflect.Bool {
|
||||||
|
return nil, E.New("badtls: invalid notify")
|
||||||
|
}
|
||||||
|
closeNotifySent := (*bool)(unsafe.Pointer(rawCloseNotifySent.UnsafeAddr()))
|
||||||
|
rawConfig := reflect.Indirect(rawConn.FieldByName("config"))
|
||||||
|
if !rawConfig.IsValid() || rawConfig.Kind() != reflect.Struct {
|
||||||
|
return nil, E.New("badtls: bad config")
|
||||||
|
}
|
||||||
|
config := (*tls.Config)(unsafe.Pointer(rawConfig.UnsafeAddr()))
|
||||||
|
randReader := config.Rand
|
||||||
|
if randReader == nil {
|
||||||
|
randReader = rand.Reader
|
||||||
|
}
|
||||||
|
rawHalfMutex := rawHalfConn.FieldByName("Mutex")
|
||||||
|
if !rawHalfMutex.IsValid() || rawHalfMutex.Kind() != reflect.Struct {
|
||||||
|
return nil, E.New("badtls: invalid half mutex")
|
||||||
|
}
|
||||||
|
halfAccess := (*sync.Mutex)(unsafe.Pointer(rawHalfMutex.UnsafeAddr()))
|
||||||
|
rawHalfError := rawHalfConn.FieldByName("err")
|
||||||
|
if !rawHalfError.IsValid() || rawHalfError.Kind() != reflect.Interface {
|
||||||
|
return nil, E.New("badtls: invalid half error")
|
||||||
|
}
|
||||||
|
halfError := (*error)(unsafe.Pointer(rawHalfError.UnsafeAddr()))
|
||||||
|
rawHalfCipherInterface := rawHalfConn.FieldByName("cipher")
|
||||||
|
if !rawHalfCipherInterface.IsValid() || rawHalfCipherInterface.Kind() != reflect.Interface {
|
||||||
|
return nil, E.New("badtls: invalid cipher interface")
|
||||||
|
}
|
||||||
|
rawHalfCipher := rawHalfCipherInterface.Elem()
|
||||||
|
aeadCipher, loaded := valueInterface(rawHalfCipher, false).(cipher.AEAD)
|
||||||
|
if !loaded {
|
||||||
|
return nil, E.New("badtls: invalid AEAD cipher")
|
||||||
|
}
|
||||||
|
var explicitNonceLen int
|
||||||
|
switch cipherName := reflect.Indirect(rawHalfCipher).Type().String(); cipherName {
|
||||||
|
case "tls.prefixNonceAEAD":
|
||||||
|
explicitNonceLen = aeadCipher.NonceSize()
|
||||||
|
case "tls.xorNonceAEAD":
|
||||||
|
default:
|
||||||
|
return nil, E.New("badtls: unknown cipher type: ", cipherName)
|
||||||
|
}
|
||||||
|
rawHalfSeq := rawHalfConn.FieldByName("seq")
|
||||||
|
if !rawHalfSeq.IsValid() || rawHalfSeq.Kind() != reflect.Array {
|
||||||
|
return nil, E.New("badtls: invalid seq")
|
||||||
|
}
|
||||||
|
halfSeq := rawHalfSeq.Bytes()
|
||||||
|
rawHalfScratchBuf := rawHalfConn.FieldByName("scratchBuf")
|
||||||
|
if !rawHalfScratchBuf.IsValid() || rawHalfScratchBuf.Kind() != reflect.Array {
|
||||||
|
return nil, E.New("badtls: invalid scratchBuf")
|
||||||
|
}
|
||||||
|
halfScratchBuf := rawHalfScratchBuf.Bytes()
|
||||||
|
return &Conn{
|
||||||
|
Conn: conn,
|
||||||
|
writer: bufio.NewExtendedWriter(conn.NetConn()),
|
||||||
|
isHandshakeComplete: isHandshakeComplete,
|
||||||
|
activeCall: activeCall,
|
||||||
|
closeNotifySent: closeNotifySent,
|
||||||
|
version: version,
|
||||||
|
halfAccess: halfAccess,
|
||||||
|
halfError: halfError,
|
||||||
|
cipher: aeadCipher,
|
||||||
|
explicitNonceLen: explicitNonceLen,
|
||||||
|
rand: randReader,
|
||||||
|
halfPtr: rawHalfConn.UnsafeAddr(),
|
||||||
|
halfSeq: halfSeq,
|
||||||
|
halfScratchBuf: halfScratchBuf,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) WriteBuffer(buffer *buf.Buffer) error {
|
||||||
|
if buffer.Len() > maxPlaintext {
|
||||||
|
defer buffer.Release()
|
||||||
|
return common.Error(c.Write(buffer.Bytes()))
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
x := c.activeCall.Load()
|
||||||
|
if x&1 != 0 {
|
||||||
|
return net.ErrClosed
|
||||||
|
}
|
||||||
|
if c.activeCall.CompareAndSwap(x, x+2) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer c.activeCall.Add(-2)
|
||||||
|
c.halfAccess.Lock()
|
||||||
|
defer c.halfAccess.Unlock()
|
||||||
|
if err := *c.halfError; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if *c.closeNotifySent {
|
||||||
|
return errShutdown
|
||||||
|
}
|
||||||
|
dataLen := buffer.Len()
|
||||||
|
dataBytes := buffer.Bytes()
|
||||||
|
outBuf := buffer.ExtendHeader(recordHeaderLen + c.explicitNonceLen)
|
||||||
|
outBuf[0] = 23
|
||||||
|
version := *c.version
|
||||||
|
if version == 0 {
|
||||||
|
version = tls.VersionTLS10
|
||||||
|
} else if version == tls.VersionTLS13 {
|
||||||
|
version = tls.VersionTLS12
|
||||||
|
}
|
||||||
|
binary.BigEndian.PutUint16(outBuf[1:], version)
|
||||||
|
var nonce []byte
|
||||||
|
if c.explicitNonceLen > 0 {
|
||||||
|
nonce = outBuf[5 : 5+c.explicitNonceLen]
|
||||||
|
if c.explicitNonceLen < 16 {
|
||||||
|
copy(nonce, c.halfSeq)
|
||||||
|
} else {
|
||||||
|
if _, err := io.ReadFull(c.rand, nonce); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(nonce) == 0 {
|
||||||
|
nonce = c.halfSeq
|
||||||
|
}
|
||||||
|
if *c.version == tls.VersionTLS13 {
|
||||||
|
buffer.FreeBytes()[0] = 23
|
||||||
|
binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen+1+c.cipher.Overhead()))
|
||||||
|
c.cipher.Seal(outBuf, nonce, outBuf[recordHeaderLen:recordHeaderLen+c.explicitNonceLen+dataLen+1], outBuf[:recordHeaderLen])
|
||||||
|
buffer.Extend(1 + c.cipher.Overhead())
|
||||||
|
} else {
|
||||||
|
binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen))
|
||||||
|
additionalData := append(c.halfScratchBuf[:0], c.halfSeq...)
|
||||||
|
additionalData = append(additionalData, outBuf[:recordHeaderLen]...)
|
||||||
|
c.cipher.Seal(outBuf, nonce, dataBytes, additionalData)
|
||||||
|
buffer.Extend(c.cipher.Overhead())
|
||||||
|
binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen+c.explicitNonceLen+c.cipher.Overhead()))
|
||||||
|
}
|
||||||
|
incSeq(c.halfPtr)
|
||||||
|
log.Trace("badtls write ", buffer.Len())
|
||||||
|
return c.writer.WriteBuffer(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) FrontHeadroom() int {
|
||||||
|
return recordHeaderLen + c.explicitNonceLen
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) RearHeadroom() int {
|
||||||
|
return 1 + c.cipher.Overhead()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) WriterMTU() int {
|
||||||
|
return maxPlaintext
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Upstream() any {
|
||||||
|
return c.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) UpstreamWriter() any {
|
||||||
|
return c.NetConn()
|
||||||
|
}
|
||||||
14
common/badtls/badtls_stub.go
Normal file
14
common/badtls/badtls_stub.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//go:build !go1.19 || go1.21
|
||||||
|
|
||||||
|
package badtls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Create(conn *tls.Conn) (aTLS.Conn, error) {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
22
common/badtls/link.go
Normal file
22
common/badtls/link.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
//go:build go1.20 && !go.1.21
|
||||||
|
|
||||||
|
package badtls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
_ "unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxPlaintext = 16384 // maximum plaintext payload length
|
||||||
|
recordHeaderLen = 5 // record header length
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:linkname errShutdown crypto/tls.errShutdown
|
||||||
|
var errShutdown error
|
||||||
|
|
||||||
|
//go:linkname incSeq crypto/tls.(*halfConn).incSeq
|
||||||
|
func incSeq(conn uintptr)
|
||||||
|
|
||||||
|
//go:linkname valueInterface reflect.valueInterface
|
||||||
|
func valueInterface(v reflect.Value, safe bool) any
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
//go:build go1.21 && !without_badtls
|
|
||||||
|
|
||||||
package badtls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"sync"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
"github.com/sagernet/sing/common/tls"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ N.ReadWaiter = (*ReadWaitConn)(nil)
|
|
||||||
|
|
||||||
type ReadWaitConn struct {
|
|
||||||
*tls.STDConn
|
|
||||||
halfAccess *sync.Mutex
|
|
||||||
rawInput *bytes.Buffer
|
|
||||||
input *bytes.Reader
|
|
||||||
hand *bytes.Buffer
|
|
||||||
readWaitOptions N.ReadWaitOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) {
|
|
||||||
stdConn, isSTDConn := conn.(*tls.STDConn)
|
|
||||||
if !isSTDConn {
|
|
||||||
return nil, os.ErrInvalid
|
|
||||||
}
|
|
||||||
rawConn := reflect.Indirect(reflect.ValueOf(stdConn))
|
|
||||||
rawHalfConn := rawConn.FieldByName("in")
|
|
||||||
if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct {
|
|
||||||
return nil, E.New("badtls: invalid half conn")
|
|
||||||
}
|
|
||||||
rawHalfMutex := rawHalfConn.FieldByName("Mutex")
|
|
||||||
if !rawHalfMutex.IsValid() || rawHalfMutex.Kind() != reflect.Struct {
|
|
||||||
return nil, E.New("badtls: invalid half mutex")
|
|
||||||
}
|
|
||||||
halfAccess := (*sync.Mutex)(unsafe.Pointer(rawHalfMutex.UnsafeAddr()))
|
|
||||||
rawRawInput := rawConn.FieldByName("rawInput")
|
|
||||||
if !rawRawInput.IsValid() || rawRawInput.Kind() != reflect.Struct {
|
|
||||||
return nil, E.New("badtls: invalid raw input")
|
|
||||||
}
|
|
||||||
rawInput := (*bytes.Buffer)(unsafe.Pointer(rawRawInput.UnsafeAddr()))
|
|
||||||
rawInput0 := rawConn.FieldByName("input")
|
|
||||||
if !rawInput0.IsValid() || rawInput0.Kind() != reflect.Struct {
|
|
||||||
return nil, E.New("badtls: invalid input")
|
|
||||||
}
|
|
||||||
input := (*bytes.Reader)(unsafe.Pointer(rawInput0.UnsafeAddr()))
|
|
||||||
rawHand := rawConn.FieldByName("hand")
|
|
||||||
if !rawHand.IsValid() || rawHand.Kind() != reflect.Struct {
|
|
||||||
return nil, E.New("badtls: invalid hand")
|
|
||||||
}
|
|
||||||
hand := (*bytes.Buffer)(unsafe.Pointer(rawHand.UnsafeAddr()))
|
|
||||||
return &ReadWaitConn{
|
|
||||||
STDConn: stdConn,
|
|
||||||
halfAccess: halfAccess,
|
|
||||||
rawInput: rawInput,
|
|
||||||
input: input,
|
|
||||||
hand: hand,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ReadWaitConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) {
|
|
||||||
c.readWaitOptions = options
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
|
|
||||||
err = c.Handshake()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.halfAccess.Lock()
|
|
||||||
defer c.halfAccess.Unlock()
|
|
||||||
for c.input.Len() == 0 {
|
|
||||||
err = tlsReadRecord(c.STDConn)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for c.hand.Len() > 0 {
|
|
||||||
err = tlsHandlePostHandshakeMessage(c.STDConn)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buffer = c.readWaitOptions.NewBuffer()
|
|
||||||
n, err := c.input.Read(buffer.FreeBytes())
|
|
||||||
if err != nil {
|
|
||||||
buffer.Release()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
buffer.Truncate(n)
|
|
||||||
|
|
||||||
if n != 0 && c.input.Len() == 0 && c.rawInput.Len() > 0 &&
|
|
||||||
// recordType(c.rawInput.Bytes()[0]) == recordTypeAlert {
|
|
||||||
c.rawInput.Bytes()[0] == 21 {
|
|
||||||
_ = tlsReadRecord(c.STDConn)
|
|
||||||
// return n, err // will be io.EOF on closeNotify
|
|
||||||
}
|
|
||||||
|
|
||||||
c.readWaitOptions.PostReturn(buffer)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:linkname tlsReadRecord crypto/tls.(*Conn).readRecord
|
|
||||||
func tlsReadRecord(c *tls.STDConn) error
|
|
||||||
|
|
||||||
//go:linkname tlsHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage
|
|
||||||
func tlsHandlePostHandshakeMessage(c *tls.STDConn) error
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
//go:build !go1.21 || without_badtls
|
|
||||||
|
|
||||||
package badtls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/tls"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) {
|
|
||||||
return nil, os.ErrInvalid
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package badversion
|
package badversion
|
||||||
|
|
||||||
import "github.com/sagernet/sing/common/json"
|
import "github.com/sagernet/sing-box/common/json"
|
||||||
|
|
||||||
func (v Version) MarshalJSON() ([]byte, error) {
|
func (v Version) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(v.String())
|
return json.Marshal(v.String())
|
||||||
|
|||||||
@@ -18,19 +18,11 @@ func NewRouter(router adapter.Router) N.Dialer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *RouterDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
func (d *RouterDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
dialer, err := d.router.DefaultOutbound(network)
|
return d.router.DefaultOutbound(network).DialContext(ctx, network, destination)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return dialer.DialContext(ctx, network, destination)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *RouterDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
func (d *RouterDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
dialer, err := d.router.DefaultOutbound(N.NetworkUDP)
|
return d.router.DefaultOutbound(N.NetworkUDP).ListenPacket(ctx, destination)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return dialer.ListenPacket(ctx, destination)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *RouterDialer) Upstream() any {
|
func (d *RouterDialer) Upstream() any {
|
||||||
|
|||||||
128
common/json/comment.go
Normal file
128
common/json/comment.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package json
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// kanged from v2ray
|
||||||
|
|
||||||
|
type commentFilterState = byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
commentFilterStateContent commentFilterState = iota
|
||||||
|
commentFilterStateEscape
|
||||||
|
commentFilterStateDoubleQuote
|
||||||
|
commentFilterStateDoubleQuoteEscape
|
||||||
|
commentFilterStateSingleQuote
|
||||||
|
commentFilterStateSingleQuoteEscape
|
||||||
|
commentFilterStateComment
|
||||||
|
commentFilterStateSlash
|
||||||
|
commentFilterStateMultilineComment
|
||||||
|
commentFilterStateMultilineCommentStar
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommentFilter struct {
|
||||||
|
br *bufio.Reader
|
||||||
|
state commentFilterState
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommentFilter(reader io.Reader) io.Reader {
|
||||||
|
return &CommentFilter{br: bufio.NewReader(reader)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *CommentFilter) Read(b []byte) (int, error) {
|
||||||
|
p := b[:0]
|
||||||
|
for len(p) < len(b)-2 {
|
||||||
|
x, err := v.br.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
switch v.state {
|
||||||
|
case commentFilterStateContent:
|
||||||
|
switch x {
|
||||||
|
case '"':
|
||||||
|
v.state = commentFilterStateDoubleQuote
|
||||||
|
p = append(p, x)
|
||||||
|
case '\'':
|
||||||
|
v.state = commentFilterStateSingleQuote
|
||||||
|
p = append(p, x)
|
||||||
|
case '\\':
|
||||||
|
v.state = commentFilterStateEscape
|
||||||
|
case '#':
|
||||||
|
v.state = commentFilterStateComment
|
||||||
|
case '/':
|
||||||
|
v.state = commentFilterStateSlash
|
||||||
|
default:
|
||||||
|
p = append(p, x)
|
||||||
|
}
|
||||||
|
case commentFilterStateEscape:
|
||||||
|
p = append(p, '\\', x)
|
||||||
|
v.state = commentFilterStateContent
|
||||||
|
case commentFilterStateDoubleQuote:
|
||||||
|
switch x {
|
||||||
|
case '"':
|
||||||
|
v.state = commentFilterStateContent
|
||||||
|
p = append(p, x)
|
||||||
|
case '\\':
|
||||||
|
v.state = commentFilterStateDoubleQuoteEscape
|
||||||
|
default:
|
||||||
|
p = append(p, x)
|
||||||
|
}
|
||||||
|
case commentFilterStateDoubleQuoteEscape:
|
||||||
|
p = append(p, '\\', x)
|
||||||
|
v.state = commentFilterStateDoubleQuote
|
||||||
|
case commentFilterStateSingleQuote:
|
||||||
|
switch x {
|
||||||
|
case '\'':
|
||||||
|
v.state = commentFilterStateContent
|
||||||
|
p = append(p, x)
|
||||||
|
case '\\':
|
||||||
|
v.state = commentFilterStateSingleQuoteEscape
|
||||||
|
default:
|
||||||
|
p = append(p, x)
|
||||||
|
}
|
||||||
|
case commentFilterStateSingleQuoteEscape:
|
||||||
|
p = append(p, '\\', x)
|
||||||
|
v.state = commentFilterStateSingleQuote
|
||||||
|
case commentFilterStateComment:
|
||||||
|
if x == '\n' {
|
||||||
|
v.state = commentFilterStateContent
|
||||||
|
p = append(p, '\n')
|
||||||
|
}
|
||||||
|
case commentFilterStateSlash:
|
||||||
|
switch x {
|
||||||
|
case '/':
|
||||||
|
v.state = commentFilterStateComment
|
||||||
|
case '*':
|
||||||
|
v.state = commentFilterStateMultilineComment
|
||||||
|
default:
|
||||||
|
p = append(p, '/', x)
|
||||||
|
}
|
||||||
|
case commentFilterStateMultilineComment:
|
||||||
|
switch x {
|
||||||
|
case '*':
|
||||||
|
v.state = commentFilterStateMultilineCommentStar
|
||||||
|
case '\n':
|
||||||
|
p = append(p, '\n')
|
||||||
|
}
|
||||||
|
case commentFilterStateMultilineCommentStar:
|
||||||
|
switch x {
|
||||||
|
case '/':
|
||||||
|
v.state = commentFilterStateContent
|
||||||
|
case '*':
|
||||||
|
// Stay
|
||||||
|
case '\n':
|
||||||
|
p = append(p, '\n')
|
||||||
|
default:
|
||||||
|
v.state = commentFilterStateMultilineComment
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("Unknown state.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
18
common/json/std.go
Normal file
18
common/json/std.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package json
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
var (
|
||||||
|
Marshal = json.Marshal
|
||||||
|
Unmarshal = json.Unmarshal
|
||||||
|
NewEncoder = json.NewEncoder
|
||||||
|
NewDecoder = json.NewDecoder
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Encoder = json.Encoder
|
||||||
|
Decoder = json.Decoder
|
||||||
|
Token = json.Token
|
||||||
|
Delim = json.Delim
|
||||||
|
SyntaxError = json.SyntaxError
|
||||||
|
)
|
||||||
@@ -1,42 +1,21 @@
|
|||||||
package mux
|
package mux
|
||||||
|
|
||||||
import (
|
import (
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-mux"
|
"github.com/sagernet/sing-mux"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client = mux.Client
|
func NewClientWithOptions(dialer N.Dialer, options option.MultiplexOptions) (*Client, error) {
|
||||||
|
|
||||||
func NewClientWithOptions(dialer N.Dialer, logger logger.Logger, options option.OutboundMultiplexOptions) (*Client, error) {
|
|
||||||
if !options.Enabled {
|
if !options.Enabled {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
var brutalOptions mux.BrutalOptions
|
|
||||||
if options.Brutal != nil && options.Brutal.Enabled {
|
|
||||||
brutalOptions = mux.BrutalOptions{
|
|
||||||
Enabled: true,
|
|
||||||
SendBPS: uint64(options.Brutal.UpMbps * C.MbpsToBps),
|
|
||||||
ReceiveBPS: uint64(options.Brutal.DownMbps * C.MbpsToBps),
|
|
||||||
}
|
|
||||||
if brutalOptions.SendBPS < mux.BrutalMinSpeedBPS {
|
|
||||||
return nil, E.New("brutal: invalid upload speed")
|
|
||||||
}
|
|
||||||
if brutalOptions.ReceiveBPS < mux.BrutalMinSpeedBPS {
|
|
||||||
return nil, E.New("brutal: invalid download speed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mux.NewClient(mux.Options{
|
return mux.NewClient(mux.Options{
|
||||||
Dialer: dialer,
|
Dialer: dialer,
|
||||||
Logger: logger,
|
|
||||||
Protocol: options.Protocol,
|
Protocol: options.Protocol,
|
||||||
MaxConnections: options.MaxConnections,
|
MaxConnections: options.MaxConnections,
|
||||||
MinStreams: options.MinStreams,
|
MinStreams: options.MinStreams,
|
||||||
MaxStreams: options.MaxStreams,
|
MaxStreams: options.MaxStreams,
|
||||||
Padding: options.Padding,
|
Padding: options.Padding,
|
||||||
Brutal: brutalOptions,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
14
common/mux/protocol.go
Normal file
14
common/mux/protocol.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package mux
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing-mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Client = mux.Client
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Destination = mux.Destination
|
||||||
|
HandleConnection = mux.HandleConnection
|
||||||
|
)
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package mux
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing-mux"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Router struct {
|
|
||||||
router adapter.ConnectionRouter
|
|
||||||
service *mux.Service
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRouterWithOptions(router adapter.ConnectionRouter, logger logger.ContextLogger, options option.InboundMultiplexOptions) (adapter.ConnectionRouter, error) {
|
|
||||||
if !options.Enabled {
|
|
||||||
return router, nil
|
|
||||||
}
|
|
||||||
var brutalOptions mux.BrutalOptions
|
|
||||||
if options.Brutal != nil && options.Brutal.Enabled {
|
|
||||||
brutalOptions = mux.BrutalOptions{
|
|
||||||
Enabled: true,
|
|
||||||
SendBPS: uint64(options.Brutal.UpMbps * C.MbpsToBps),
|
|
||||||
ReceiveBPS: uint64(options.Brutal.DownMbps * C.MbpsToBps),
|
|
||||||
}
|
|
||||||
if brutalOptions.SendBPS < mux.BrutalMinSpeedBPS {
|
|
||||||
return nil, E.New("brutal: invalid upload speed")
|
|
||||||
}
|
|
||||||
if brutalOptions.ReceiveBPS < mux.BrutalMinSpeedBPS {
|
|
||||||
return nil, E.New("brutal: invalid download speed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
service, err := mux.NewService(mux.ServiceOptions{
|
|
||||||
NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context {
|
|
||||||
return log.ContextWithNewID(ctx)
|
|
||||||
},
|
|
||||||
Logger: logger,
|
|
||||||
Handler: adapter.NewRouteContextHandler(router, logger),
|
|
||||||
Padding: options.Padding,
|
|
||||||
Brutal: brutalOptions,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Router{router, service}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
|
||||||
if metadata.Destination == mux.Destination {
|
|
||||||
return r.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, adapter.UpstreamMetadata(metadata))
|
|
||||||
} else {
|
|
||||||
return r.router.RouteConnection(ctx, conn, metadata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
|
||||||
return r.router.RoutePacketConnection(ctx, conn, metadata)
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -182,52 +182,11 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
switch frameType {
|
switch frameType {
|
||||||
case 0x00: // PADDING
|
case 0x0:
|
||||||
continue
|
continue
|
||||||
case 0x01: // PING
|
case 0x1:
|
||||||
continue
|
continue
|
||||||
case 0x02, 0x03: // ACK
|
case 0x6:
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // Largest Acknowledged
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ACK Delay
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ackRangeCount, err := qtls.ReadUvarint(decryptedReader) // ACK Range Count
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // First ACK Range
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for i := 0; i < int(ackRangeCount); i++ {
|
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // Gap
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ACK Range Length
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if frameType == 0x03 {
|
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ECT0 Count
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ECT1 Count
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ECN-CE Count
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 {
|
||||||
@@ -249,26 +208,8 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
case 0x1c: // CONNECTION_CLOSE
|
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // Error Code
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // Frame Type
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var length uint64
|
|
||||||
length, err = qtls.ReadUvarint(decryptedReader) // Reason Phrase Length
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent) // Reason Phrase
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return nil, os.ErrInvalid
|
// ignore unknown frame type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tlsHdr := make([]byte, 5)
|
tlsHdr := make([]byte, 5)
|
||||||
|
|||||||
@@ -1,487 +0,0 @@
|
|||||||
package srs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"compress/zlib"
|
|
||||||
"encoding/binary"
|
|
||||||
"io"
|
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing/common/domain"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/rw"
|
|
||||||
|
|
||||||
"go4.org/netipx"
|
|
||||||
)
|
|
||||||
|
|
||||||
var MagicBytes = [3]byte{0x53, 0x52, 0x53} // SRS
|
|
||||||
|
|
||||||
const (
|
|
||||||
ruleItemQueryType uint8 = iota
|
|
||||||
ruleItemNetwork
|
|
||||||
ruleItemDomain
|
|
||||||
ruleItemDomainKeyword
|
|
||||||
ruleItemDomainRegex
|
|
||||||
ruleItemSourceIPCIDR
|
|
||||||
ruleItemIPCIDR
|
|
||||||
ruleItemSourcePort
|
|
||||||
ruleItemSourcePortRange
|
|
||||||
ruleItemPort
|
|
||||||
ruleItemPortRange
|
|
||||||
ruleItemProcessName
|
|
||||||
ruleItemProcessPath
|
|
||||||
ruleItemPackageName
|
|
||||||
ruleItemWIFISSID
|
|
||||||
ruleItemWIFIBSSID
|
|
||||||
ruleItemFinal uint8 = 0xFF
|
|
||||||
)
|
|
||||||
|
|
||||||
func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) {
|
|
||||||
var magicBytes [3]byte
|
|
||||||
_, err = io.ReadFull(reader, magicBytes[:])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if magicBytes != MagicBytes {
|
|
||||||
err = E.New("invalid sing-box rule set file")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var version uint8
|
|
||||||
err = binary.Read(reader, binary.BigEndian, &version)
|
|
||||||
if err != nil {
|
|
||||||
return ruleSet, err
|
|
||||||
}
|
|
||||||
if version != 1 {
|
|
||||||
return ruleSet, E.New("unsupported version: ", version)
|
|
||||||
}
|
|
||||||
zReader, err := zlib.NewReader(reader)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
length, err := rw.ReadUVariant(zReader)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ruleSet.Rules = make([]option.HeadlessRule, length)
|
|
||||||
for i := uint64(0); i < length; i++ {
|
|
||||||
ruleSet.Rules[i], err = readRule(zReader, recovery)
|
|
||||||
if err != nil {
|
|
||||||
err = E.Cause(err, "read rule[", i, "]")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func Write(writer io.Writer, ruleSet option.PlainRuleSet) error {
|
|
||||||
_, err := writer.Write(MagicBytes[:])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = binary.Write(writer, binary.BigEndian, uint8(1))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
zWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, rule := range ruleSet.Rules {
|
|
||||||
err = writeRule(zWriter, rule)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return zWriter.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) {
|
|
||||||
var ruleType uint8
|
|
||||||
err = binary.Read(reader, binary.BigEndian, &ruleType)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch ruleType {
|
|
||||||
case 0:
|
|
||||||
rule.Type = C.RuleTypeDefault
|
|
||||||
rule.DefaultOptions, err = readDefaultRule(reader, recovery)
|
|
||||||
case 1:
|
|
||||||
rule.Type = C.RuleTypeLogical
|
|
||||||
rule.LogicalOptions, err = readLogicalRule(reader, recovery)
|
|
||||||
default:
|
|
||||||
err = E.New("unknown rule type: ", ruleType)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeRule(writer io.Writer, rule option.HeadlessRule) error {
|
|
||||||
switch rule.Type {
|
|
||||||
case C.RuleTypeDefault:
|
|
||||||
return writeDefaultRule(writer, rule.DefaultOptions)
|
|
||||||
case C.RuleTypeLogical:
|
|
||||||
return writeLogicalRule(writer, rule.LogicalOptions)
|
|
||||||
default:
|
|
||||||
panic("unknown rule type: " + rule.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) {
|
|
||||||
var lastItemType uint8
|
|
||||||
for {
|
|
||||||
var itemType uint8
|
|
||||||
err = binary.Read(reader, binary.BigEndian, &itemType)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch itemType {
|
|
||||||
case ruleItemQueryType:
|
|
||||||
var rawQueryType []uint16
|
|
||||||
rawQueryType, err = readRuleItemUint16(reader)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rule.QueryType = common.Map(rawQueryType, func(it uint16) option.DNSQueryType {
|
|
||||||
return option.DNSQueryType(it)
|
|
||||||
})
|
|
||||||
case ruleItemNetwork:
|
|
||||||
rule.Network, err = readRuleItemString(reader)
|
|
||||||
case ruleItemDomain:
|
|
||||||
var matcher *domain.Matcher
|
|
||||||
matcher, err = domain.ReadMatcher(reader)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rule.DomainMatcher = matcher
|
|
||||||
case ruleItemDomainKeyword:
|
|
||||||
rule.DomainKeyword, err = readRuleItemString(reader)
|
|
||||||
case ruleItemDomainRegex:
|
|
||||||
rule.DomainRegex, err = readRuleItemString(reader)
|
|
||||||
case ruleItemSourceIPCIDR:
|
|
||||||
rule.SourceIPSet, err = readIPSet(reader)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if recovery {
|
|
||||||
rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String)
|
|
||||||
}
|
|
||||||
case ruleItemIPCIDR:
|
|
||||||
rule.IPSet, err = readIPSet(reader)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if recovery {
|
|
||||||
rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String)
|
|
||||||
}
|
|
||||||
case ruleItemSourcePort:
|
|
||||||
rule.SourcePort, err = readRuleItemUint16(reader)
|
|
||||||
case ruleItemSourcePortRange:
|
|
||||||
rule.SourcePortRange, err = readRuleItemString(reader)
|
|
||||||
case ruleItemPort:
|
|
||||||
rule.Port, err = readRuleItemUint16(reader)
|
|
||||||
case ruleItemPortRange:
|
|
||||||
rule.PortRange, err = readRuleItemString(reader)
|
|
||||||
case ruleItemProcessName:
|
|
||||||
rule.ProcessName, err = readRuleItemString(reader)
|
|
||||||
case ruleItemProcessPath:
|
|
||||||
rule.ProcessPath, err = readRuleItemString(reader)
|
|
||||||
case ruleItemPackageName:
|
|
||||||
rule.PackageName, err = readRuleItemString(reader)
|
|
||||||
case ruleItemWIFISSID:
|
|
||||||
rule.WIFISSID, err = readRuleItemString(reader)
|
|
||||||
case ruleItemWIFIBSSID:
|
|
||||||
rule.WIFIBSSID, err = readRuleItemString(reader)
|
|
||||||
case ruleItemFinal:
|
|
||||||
err = binary.Read(reader, binary.BigEndian, &rule.Invert)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
err = E.New("unknown rule item type: ", itemType, ", last type: ", lastItemType)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lastItemType = itemType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error {
|
|
||||||
err := binary.Write(writer, binary.BigEndian, uint8(0))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(rule.QueryType) > 0 {
|
|
||||||
err = writeRuleItemUint16(writer, ruleItemQueryType, common.Map(rule.QueryType, func(it option.DNSQueryType) uint16 {
|
|
||||||
return uint16(it)
|
|
||||||
}))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(rule.Network) > 0 {
|
|
||||||
err = writeRuleItemString(writer, ruleItemNetwork, rule.Network)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(rule.Domain) > 0 || len(rule.DomainSuffix) > 0 {
|
|
||||||
err = binary.Write(writer, binary.BigEndian, ruleItemDomain)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = domain.NewMatcher(rule.Domain, rule.DomainSuffix).Write(writer)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(rule.DomainKeyword) > 0 {
|
|
||||||
err = writeRuleItemString(writer, ruleItemDomainKeyword, rule.DomainKeyword)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(rule.DomainRegex) > 0 {
|
|
||||||
err = writeRuleItemString(writer, ruleItemDomainRegex, rule.DomainRegex)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(rule.SourceIPCIDR) > 0 {
|
|
||||||
err = writeRuleItemCIDR(writer, ruleItemSourceIPCIDR, rule.SourceIPCIDR)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "source_ipcidr")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(rule.IPCIDR) > 0 {
|
|
||||||
err = writeRuleItemCIDR(writer, ruleItemIPCIDR, rule.IPCIDR)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "ipcidr")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(rule.SourcePort) > 0 {
|
|
||||||
err = writeRuleItemUint16(writer, ruleItemSourcePort, rule.SourcePort)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(rule.SourcePortRange) > 0 {
|
|
||||||
err = writeRuleItemString(writer, ruleItemSourcePortRange, rule.SourcePortRange)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(rule.Port) > 0 {
|
|
||||||
err = writeRuleItemUint16(writer, ruleItemPort, rule.Port)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(rule.PortRange) > 0 {
|
|
||||||
err = writeRuleItemString(writer, ruleItemPortRange, rule.PortRange)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(rule.ProcessName) > 0 {
|
|
||||||
err = writeRuleItemString(writer, ruleItemProcessName, rule.ProcessName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(rule.ProcessPath) > 0 {
|
|
||||||
err = writeRuleItemString(writer, ruleItemProcessPath, rule.ProcessPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(rule.PackageName) > 0 {
|
|
||||||
err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(rule.WIFISSID) > 0 {
|
|
||||||
err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(rule.WIFIBSSID) > 0 {
|
|
||||||
err = writeRuleItemString(writer, ruleItemWIFIBSSID, rule.WIFIBSSID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = binary.Write(writer, binary.BigEndian, ruleItemFinal)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = binary.Write(writer, binary.BigEndian, rule.Invert)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readRuleItemString(reader io.Reader) ([]string, error) {
|
|
||||||
length, err := rw.ReadUVariant(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
value := make([]string, length)
|
|
||||||
for i := uint64(0); i < length; i++ {
|
|
||||||
value[i], err = rw.ReadVString(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeRuleItemString(writer io.Writer, itemType uint8, value []string) 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 = rw.WriteVString(writer, item)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
for i, prefixString := range value {
|
|
||||||
prefix, err := netip.ParsePrefix(prefixString)
|
|
||||||
if err == nil {
|
|
||||||
builder.AddPrefix(prefix)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
addr, addrErr := netip.ParseAddr(prefixString)
|
|
||||||
if addrErr == nil {
|
|
||||||
builder.Add(addr)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return E.Cause(err, "parse [", i, "]")
|
|
||||||
}
|
|
||||||
ipSet, err := builder.IPSet()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = binary.Write(writer, binary.BigEndian, itemType)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return writeIPSet(writer, ipSet)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) {
|
|
||||||
var mode uint8
|
|
||||||
err = binary.Read(reader, binary.BigEndian, &mode)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch mode {
|
|
||||||
case 0:
|
|
||||||
logicalRule.Mode = C.LogicalTypeAnd
|
|
||||||
case 1:
|
|
||||||
logicalRule.Mode = C.LogicalTypeOr
|
|
||||||
default:
|
|
||||||
err = E.New("unknown logical mode: ", mode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
length, err := rw.ReadUVariant(reader)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logicalRule.Rules = make([]option.HeadlessRule, length)
|
|
||||||
for i := uint64(0); i < length; i++ {
|
|
||||||
logicalRule.Rules[i], err = readRule(reader, recovery)
|
|
||||||
if err != nil {
|
|
||||||
err = E.Cause(err, "read logical rule [", i, "]")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = binary.Read(reader, binary.BigEndian, &logicalRule.Invert)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error {
|
|
||||||
err := binary.Write(writer, binary.BigEndian, uint8(1))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
switch logicalRule.Mode {
|
|
||||||
case C.LogicalTypeAnd:
|
|
||||||
err = binary.Write(writer, binary.BigEndian, uint8(0))
|
|
||||||
case C.LogicalTypeOr:
|
|
||||||
err = binary.Write(writer, binary.BigEndian, uint8(1))
|
|
||||||
default:
|
|
||||||
panic("unknown logical mode: " + logicalRule.Mode)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, rule := range logicalRule.Rules {
|
|
||||||
err = writeRule(writer, rule)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = binary.Write(writer, binary.BigEndian, logicalRule.Invert)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
package srs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"io"
|
|
||||||
"net/netip"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/rw"
|
|
||||||
|
|
||||||
"go4.org/netipx"
|
|
||||||
)
|
|
||||||
|
|
||||||
type myIPSet struct {
|
|
||||||
rr []myIPRange
|
|
||||||
}
|
|
||||||
|
|
||||||
type myIPRange struct {
|
|
||||||
from netip.Addr
|
|
||||||
to netip.Addr
|
|
||||||
}
|
|
||||||
|
|
||||||
func readIPSet(reader io.Reader) (*netipx.IPSet, error) {
|
|
||||||
var version uint8
|
|
||||||
err := binary.Read(reader, binary.BigEndian, &version)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var length uint64
|
|
||||||
err = binary.Read(reader, binary.BigEndian, &length)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mySet := &myIPSet{
|
|
||||||
rr: make([]myIPRange, length),
|
|
||||||
}
|
|
||||||
for i := uint64(0); i < length; i++ {
|
|
||||||
var (
|
|
||||||
fromLen uint64
|
|
||||||
toLen uint64
|
|
||||||
fromAddr netip.Addr
|
|
||||||
toAddr netip.Addr
|
|
||||||
)
|
|
||||||
fromLen, err = rw.ReadUVariant(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
fromBytes := make([]byte, fromLen)
|
|
||||||
_, err = io.ReadFull(reader, fromBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = fromAddr.UnmarshalBinary(fromBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
toLen, err = rw.ReadUVariant(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
toBytes := make([]byte, toLen)
|
|
||||||
_, err = io.ReadFull(reader, toBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = toAddr.UnmarshalBinary(toBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mySet.rr[i] = myIPRange{fromAddr, toAddr}
|
|
||||||
}
|
|
||||||
return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeIPSet(writer io.Writer, set *netipx.IPSet) error {
|
|
||||||
err := binary.Write(writer, binary.BigEndian, uint8(1))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
mySet := (*myIPSet)(unsafe.Pointer(set))
|
|
||||||
err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, rr := range mySet.rr {
|
|
||||||
var (
|
|
||||||
fromBinary []byte
|
|
||||||
toBinary []byte
|
|
||||||
)
|
|
||||||
fromBinary, err = rr.from.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = rw.WriteUVariant(writer, uint64(len(fromBinary)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = writer.Write(fromBinary)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
toBinary, err = rr.to.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = rw.WriteUVariant(writer, uint64(len(toBinary)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = writer.Write(toBinary)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package taskmonitor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Monitor struct {
|
|
||||||
logger logger.Logger
|
|
||||||
timeout time.Duration
|
|
||||||
timer *time.Timer
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(logger logger.Logger, timeout time.Duration) *Monitor {
|
|
||||||
return &Monitor{
|
|
||||||
logger: logger,
|
|
||||||
timeout: timeout,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Monitor) Start(taskName ...any) {
|
|
||||||
m.timer = time.AfterFunc(m.timeout, func() {
|
|
||||||
m.logger.Warn(F.ToString(taskName...), " take too much time to finish!")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Monitor) Finish() {
|
|
||||||
m.timer.Stop()
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/badtls"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"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"
|
||||||
@@ -43,17 +42,7 @@ func NewClient(ctx context.Context, serverAddress string, options option.Outboun
|
|||||||
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
|
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
tlsConn, err := aTLS.ClientHandshake(ctx, conn, config)
|
return aTLS.ClientHandshake(ctx, conn, config)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
readWaitConn, err := badtls.NewReadWaitConn(tlsConn)
|
|
||||||
if err == nil {
|
|
||||||
return readWaitConn, nil
|
|
||||||
} else if err != os.ErrInvalid {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return tlsConn, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Dialer struct {
|
type Dialer struct {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/ecdh"
|
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
@@ -138,21 +137,12 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn
|
|||||||
hello.SessionId[2] = 1
|
hello.SessionId[2] = 1
|
||||||
binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix()))
|
binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix()))
|
||||||
copy(hello.SessionId[8:], e.shortID[:])
|
copy(hello.SessionId[8:], e.shortID[:])
|
||||||
|
|
||||||
if debug.Enabled {
|
if debug.Enabled {
|
||||||
fmt.Printf("REALITY hello.sessionId[:16]: %v\n", hello.SessionId[:16])
|
fmt.Printf("REALITY hello.sessionId[:16]: %v\n", hello.SessionId[:16])
|
||||||
}
|
}
|
||||||
publicKey, err := ecdh.X25519().NewPublicKey(e.publicKey)
|
|
||||||
if err != nil {
|
authKey := uConn.HandshakeState.State13.EcdheParams.SharedKey(e.publicKey)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ecdheKey := uConn.HandshakeState.State13.EcdheKey
|
|
||||||
if ecdheKey == nil {
|
|
||||||
return nil, E.New("nil ecdhe_key")
|
|
||||||
}
|
|
||||||
authKey, err := ecdheKey.ECDH(publicKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if authKey == nil {
|
if authKey == nil {
|
||||||
return nil, E.New("nil auth_key")
|
return nil, E.New("nil auth_key")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ package tls
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/badtls"
|
|
||||||
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"
|
||||||
@@ -28,15 +26,5 @@ func NewServer(ctx context.Context, logger log.Logger, options option.InboundTLS
|
|||||||
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
|
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
tlsConn, err := aTLS.ServerHandshake(ctx, conn, config)
|
return aTLS.ServerHandshake(ctx, conn, config)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
readWaitConn, err := badtls.NewReadWaitConn(tlsConn)
|
|
||||||
if err == nil {
|
|
||||||
return readWaitConn, nil
|
|
||||||
} else if err != os.ErrInvalid {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return tlsConn, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,16 +219,6 @@ func uTLSClientHelloID(name string) (utls.ClientHelloID, error) {
|
|||||||
switch name {
|
switch name {
|
||||||
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":
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
package uot
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
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"
|
|
||||||
"github.com/sagernet/sing/common/uot"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ adapter.ConnectionRouter = (*Router)(nil)
|
|
||||||
|
|
||||||
type Router struct {
|
|
||||||
router adapter.ConnectionRouter
|
|
||||||
logger logger.ContextLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRouter(router adapter.ConnectionRouter, logger logger.ContextLogger) *Router {
|
|
||||||
return &Router{router, logger}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
|
||||||
switch metadata.Destination.Fqdn {
|
|
||||||
case uot.MagicAddress:
|
|
||||||
request, err := uot.ReadRequest(conn)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "read UoT request")
|
|
||||||
}
|
|
||||||
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
|
|
||||||
return r.router.RoutePacketConnection(ctx, uot.NewConn(conn, *request), metadata)
|
|
||||||
case uot.LegacyMagicAddress:
|
|
||||||
r.logger.InfoContext(ctx, "inbound legacy UoT connection")
|
|
||||||
metadata.Domain = metadata.Destination.Fqdn
|
|
||||||
metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()}
|
|
||||||
return r.RoutePacketConnection(ctx, uot.NewConn(conn, uot.Request{}), metadata)
|
|
||||||
}
|
|
||||||
return r.router.RouteConnection(ctx, conn, metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
|
||||||
return r.router.RoutePacketConnection(ctx, conn, metadata)
|
|
||||||
}
|
|
||||||
@@ -9,11 +9,3 @@ const (
|
|||||||
LogicalTypeAnd = "and"
|
LogicalTypeAnd = "and"
|
||||||
LogicalTypeOr = "or"
|
LogicalTypeOr = "or"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
RuleSetTypeLocal = "local"
|
|
||||||
RuleSetTypeRemote = "remote"
|
|
||||||
RuleSetVersion1 = 1
|
|
||||||
RuleSetFormatSource = "source"
|
|
||||||
RuleSetFormatBinary = "binary"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
package constant
|
|
||||||
|
|
||||||
const MbpsToBps = 125000
|
|
||||||
@@ -9,9 +9,5 @@ const (
|
|||||||
QUICTimeout = 30 * time.Second
|
QUICTimeout = 30 * time.Second
|
||||||
STUNTimeout = 15 * time.Second
|
STUNTimeout = 15 * time.Second
|
||||||
UDPTimeout = 5 * time.Minute
|
UDPTimeout = 5 * time.Minute
|
||||||
DefaultURLTestInterval = 3 * time.Minute
|
DefaultURLTestInterval = 1 * time.Minute
|
||||||
DefaultURLTestIdleTimeout = 30 * time.Minute
|
|
||||||
DefaultStartTimeout = 10 * time.Second
|
|
||||||
DefaultStopTimeout = 5 * time.Second
|
|
||||||
DefaultStopFatalTimeout = 10 * time.Second
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,5 +5,4 @@ const (
|
|||||||
V2RayTransportTypeWebsocket = "ws"
|
V2RayTransportTypeWebsocket = "ws"
|
||||||
V2RayTransportTypeQUIC = "quic"
|
V2RayTransportTypeQUIC = "quic"
|
||||||
V2RayTransportTypeGRPC = "grpc"
|
V2RayTransportTypeGRPC = "grpc"
|
||||||
V2RayTransportTypeHTTPUpgrade = "httpupgrade"
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/badjson"
|
||||||
"github.com/sagernet/sing-box/common/humanize"
|
"github.com/sagernet/sing-box/common/humanize"
|
||||||
|
"github.com/sagernet/sing-box/common/json"
|
||||||
"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/json"
|
|
||||||
"github.com/sagernet/sing/common/json/badjson"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,335 +1,3 @@
|
|||||||
---
|
|
||||||
icon: material/alert-decagram
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 1.8.0-beta.3
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.8.0-beta.2
|
|
||||||
|
|
||||||
* Fix GSO support
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.7.5
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.8.0-alpha.17
|
|
||||||
|
|
||||||
* Add GSO support for TUN and WireGuard system interface **1**
|
|
||||||
* Update uTLS to 1.5.4 **2**
|
|
||||||
* Update dependencies **3**
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
See [TUN](/configuration/inbound/tun) inbound and [WireGuard](/configuration/outbound/wireguard) outbound.
|
|
||||||
|
|
||||||
**2**:
|
|
||||||
|
|
||||||
Added some new [fingerprints](/configuration/shared/tls#utls).
|
|
||||||
Also, starting with this release, uTLS requires at least Go 1.20.
|
|
||||||
|
|
||||||
**3**:
|
|
||||||
|
|
||||||
Updated `cloudflare-tls`, `gomobile`, `smux`, `tfo-go` and `wireguard-go` to latest, and `gvisor` to `20231204.0`
|
|
||||||
|
|
||||||
This may break something, good luck!
|
|
||||||
|
|
||||||
#### 1.7.4
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
_Due to the long waiting time, this version is no longer waiting for approval
|
|
||||||
by the Apple App Store, so updates to Apple Platforms will be delayed._
|
|
||||||
|
|
||||||
#### 1.8.0-alpha.16
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.8.0-alpha.15
|
|
||||||
|
|
||||||
* Some chaotic changes **1**
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
Designed to optimize memory usage of idle connections, may take effect on the following protocols:
|
|
||||||
|
|
||||||
| Protocol | TCP | UDP |
|
|
||||||
|------------------------------------------------------|------------------|------------------|
|
|
||||||
| HTTP proxy server | :material-check: | / |
|
|
||||||
| SOCKS5 | :material-close: | :material-check: |
|
|
||||||
| Shadowsocks none/AEAD/AEAD2022 | :material-check: | :material-check: |
|
|
||||||
| Trojan | / | :material-check: |
|
|
||||||
| TUIC/Hysteria/Hysteria2 | :material-close: | :material-check: |
|
|
||||||
| Multiplex | :material-close: | :material-check: |
|
|
||||||
| Plain TLS (Trojan/VLESS without extra sub-protocols) | :material-check: | / |
|
|
||||||
| Other protocols | :material-close: | :material-close: |
|
|
||||||
|
|
||||||
At the same time, everything existing may be broken, please actively report problems with this version.
|
|
||||||
|
|
||||||
#### 1.8.0-alpha.13
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.8.0-alpha.10
|
|
||||||
|
|
||||||
* Add `idle_timeout` for URLTest outbound **1**
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
When URLTest is idle for a certain period of time, the scheduled delay test will be paused.
|
|
||||||
|
|
||||||
#### 1.7.2
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.8.0-alpha.8
|
|
||||||
|
|
||||||
* Add context to JSON decode error message **1**
|
|
||||||
* Reject internal fake-ip queries **2**
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
JSON parse errors will now include the current key path.
|
|
||||||
Only takes effect when compiled with Go 1.21+.
|
|
||||||
|
|
||||||
**2**:
|
|
||||||
|
|
||||||
All internal DNS queries now skip DNS rules with `server` type `fakeip`,
|
|
||||||
and the default DNS server can no longer be `fakeip`.
|
|
||||||
|
|
||||||
This change is intended to break incorrect usage and essentially requires no action.
|
|
||||||
|
|
||||||
#### 1.8.0-alpha.7
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.7.1
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.8.0-alpha.6
|
|
||||||
|
|
||||||
* Fix rule-set matching logic **1**
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
Now the rules in the `rule_set` rule item can be logically considered to be merged into the rule using rule sets,
|
|
||||||
rather than completely following the AND logic.
|
|
||||||
|
|
||||||
#### 1.8.0-alpha.5
|
|
||||||
|
|
||||||
* Parallel rule-set initialization
|
|
||||||
* Independent `source_ip_is_private` and `ip_is_private` rules **1**
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
The `private` GeoIP country never existed and was actually implemented inside V2Ray.
|
|
||||||
Since GeoIP was deprecated, we made this rule independent, see [Migration](/migration/#migrate-geoip-to-rule-sets).
|
|
||||||
|
|
||||||
#### 1.8.0-alpha.1
|
|
||||||
|
|
||||||
* Migrate cache file from Clash API to independent options **1**
|
|
||||||
* Introducing [Rule Set](/configuration/rule-set) **2**
|
|
||||||
* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3**
|
|
||||||
* Allow nested logical rules **4**
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
See [Cache File](/configuration/experimental/cache-file) and
|
|
||||||
[Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options).
|
|
||||||
|
|
||||||
**2**:
|
|
||||||
|
|
||||||
Rule set is independent collections of rules that can be compiled into binaries to improve performance.
|
|
||||||
Compared to legacy GeoIP and Geosite resources,
|
|
||||||
it can include more types of rules, load faster,
|
|
||||||
use less memory, and update automatically.
|
|
||||||
|
|
||||||
See [Route#rule_set](/configuration/route/#rule_set),
|
|
||||||
[Route Rule](/configuration/route/rule),
|
|
||||||
[DNS Rule](/configuration/dns/rule),
|
|
||||||
[Rule Set](/configuration/rule-set),
|
|
||||||
[Source Format](/configuration/rule-set/source-format) and
|
|
||||||
[Headless Rule](/configuration/rule-set/headless-rule).
|
|
||||||
|
|
||||||
For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and
|
|
||||||
[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets).
|
|
||||||
|
|
||||||
**3**:
|
|
||||||
|
|
||||||
New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets.
|
|
||||||
|
|
||||||
**4**:
|
|
||||||
|
|
||||||
Logical rules in route rules, DNS rules, and the new headless rule now allow nesting of logical rules.
|
|
||||||
|
|
||||||
#### 1.7.0
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
Important changes since 1.6:
|
|
||||||
|
|
||||||
* Add [exclude route support](/configuration/inbound/tun) for TUN inbound
|
|
||||||
* Add `udp_disable_domain_unmapping` [inbound listen option](/configuration/shared/listen) **1**
|
|
||||||
* Add [HTTPUpgrade V2Ray transport](/configuration/shared/v2ray-transport#HTTPUpgrade) support **2**
|
|
||||||
* Migrate multiplex and UoT server to inbound **3**
|
|
||||||
* Add TCP Brutal support for multiplex **4**
|
|
||||||
* Add `wifi_ssid` and `wifi_bssid` route and DNS rules **5**
|
|
||||||
* Update quic-go to v0.40.0
|
|
||||||
* Update gVisor to 20231113.0
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
If enabled, for UDP proxy requests addressed to a domain,
|
|
||||||
the original packet address will be sent in the response instead of the mapped domain.
|
|
||||||
|
|
||||||
This option is used for compatibility with clients that
|
|
||||||
do not support receiving UDP packets with domain addresses, such as Surge.
|
|
||||||
|
|
||||||
**2**:
|
|
||||||
|
|
||||||
Introduced in V2Ray 5.10.0.
|
|
||||||
|
|
||||||
The new HTTPUpgrade transport has better performance than WebSocket and is better suited for CDN abuse.
|
|
||||||
|
|
||||||
**3**:
|
|
||||||
|
|
||||||
Starting in 1.7.0, multiplexing support is no longer enabled by default
|
|
||||||
and needs to be turned on explicitly in inbound
|
|
||||||
options.
|
|
||||||
|
|
||||||
**4**
|
|
||||||
|
|
||||||
Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server,
|
|
||||||
see [TCP Brutal](/configuration/shared/tcp-brutal) for details.
|
|
||||||
|
|
||||||
**5**:
|
|
||||||
|
|
||||||
Only supported in graphical clients on Android and iOS.
|
|
||||||
|
|
||||||
#### 1.7.0-rc.3
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.6.7
|
|
||||||
|
|
||||||
* macOS: Add button for uninstall SystemExtension in the standalone graphical client
|
|
||||||
* Fix missing UDP user context on TUIC/Hysteria2 inbounds
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.7.0-rc.2
|
|
||||||
|
|
||||||
* Fix missing UDP user context on TUIC/Hysteria2 inbounds
|
|
||||||
* macOS: Add button for uninstall SystemExtension in the standalone graphical client
|
|
||||||
|
|
||||||
#### 1.6.6
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.7.0-rc.1
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.7.0-beta.5
|
|
||||||
|
|
||||||
* Update gVisor to 20231113.0
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.7.0-beta.4
|
|
||||||
|
|
||||||
* Add `wifi_ssid` and `wifi_bssid` route and DNS rules **1**
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
Only supported in graphical clients on Android and iOS.
|
|
||||||
|
|
||||||
#### 1.7.0-beta.3
|
|
||||||
|
|
||||||
* Fix zero TTL was incorrectly reset
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.6.5
|
|
||||||
|
|
||||||
* Fix crash if TUIC inbound authentication failed
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.7.0-beta.2
|
|
||||||
|
|
||||||
* Fix crash if TUIC inbound authentication failed
|
|
||||||
* Update quic-go to v0.40.0
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.6.4
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.7.0-beta.1
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.6.3
|
|
||||||
|
|
||||||
* iOS/Android: Fix profile auto update
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.7.0-alpha.11
|
|
||||||
|
|
||||||
* iOS/Android: Fix profile auto update
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.7.0-alpha.10
|
|
||||||
|
|
||||||
* Fix tcp-brutal not working with TLS
|
|
||||||
* Fix Android client not closing in some cases
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.6.2
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.6.1
|
|
||||||
|
|
||||||
* Our [Android client](/installation/clients/sfa) is now available in the Google Play Store ▶️
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.7.0-alpha.6
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.7.0-alpha.4
|
|
||||||
|
|
||||||
* Migrate multiplex and UoT server to inbound **1**
|
|
||||||
* Add TCP Brutal support for multiplex **2**
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
Starting in 1.7.0, multiplexing support is no longer enabled by default and needs to be turned on explicitly in inbound
|
|
||||||
options.
|
|
||||||
|
|
||||||
**2**
|
|
||||||
|
|
||||||
Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server,
|
|
||||||
see [TCP Brutal](/configuration/shared/tcp-brutal) for details.
|
|
||||||
|
|
||||||
#### 1.7.0-alpha.3
|
|
||||||
|
|
||||||
* Add [HTTPUpgrade V2Ray transport](/configuration/shared/v2ray-transport#HTTPUpgrade) support **1**
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
Introduced in V2Ray 5.10.0.
|
|
||||||
|
|
||||||
The new HTTPUpgrade transport has better performance than WebSocket and is better suited for CDN abuse.
|
|
||||||
|
|
||||||
#### 1.6.0
|
#### 1.6.0
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
@@ -354,23 +22,6 @@ This update is intended to address the multi-send defects of the old implementat
|
|||||||
Based on discussions with the original author, the brutal CC and QUIC protocol parameters of
|
Based on discussions with the original author, the brutal CC and QUIC protocol parameters of
|
||||||
the old protocol (Hysteria 1) have been updated to be consistent with Hysteria 2
|
the old protocol (Hysteria 1) have been updated to be consistent with Hysteria 2
|
||||||
|
|
||||||
#### 1.7.0-alpha.2
|
|
||||||
|
|
||||||
* Fix bugs introduced in 1.7.0-alpha.1
|
|
||||||
|
|
||||||
#### 1.7.0-alpha.1
|
|
||||||
|
|
||||||
* Add [exclude route support](/configuration/inbound/tun) for TUN inbound
|
|
||||||
* Add `udp_disable_domain_unmapping` [inbound listen option](/configuration/shared/listen) **1**
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
If enabled, for UDP proxy requests addressed to a domain,
|
|
||||||
the original packet address will be sent in the response instead of the mapped domain.
|
|
||||||
|
|
||||||
This option is used for compatibility with clients that
|
|
||||||
do not support receiving UDP packets with domain addresses, such as Surge.
|
|
||||||
|
|
||||||
#### 1.5.5
|
#### 1.5.5
|
||||||
|
|
||||||
@@ -384,9 +35,8 @@ When `auto_route` is enabled and `strict_route` is disabled, the device can now
|
|||||||
|
|
||||||
**2**:
|
**2**:
|
||||||
|
|
||||||
Built using Go 1.20, the last version that will run on
|
Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave.
|
||||||
Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High
|
|
||||||
Sierra, 10.14 Mojave.
|
|
||||||
|
|
||||||
#### 1.6.0-rc.4
|
#### 1.6.0-rc.4
|
||||||
|
|
||||||
@@ -399,9 +49,7 @@ Sierra, 10.14 Mojave.
|
|||||||
|
|
||||||
**1**:
|
**1**:
|
||||||
|
|
||||||
Built using Go 1.20, the last version that will run on
|
Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave.
|
||||||
Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High
|
|
||||||
Sierra, 10.14 Mojave.
|
|
||||||
|
|
||||||
#### 1.6.0-beta.4
|
#### 1.6.0-beta.4
|
||||||
|
|
||||||
@@ -435,24 +83,6 @@ the old protocol (Hysteria 1) have been updated to be consistent with Hysteria 2
|
|||||||
* Update golang.org/x/net to v0.17.0
|
* Update golang.org/x/net to v0.17.0
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
#### 1.6.0-beta.3
|
|
||||||
|
|
||||||
* Update the legacy Hysteria protocol **1**
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
**1**
|
|
||||||
|
|
||||||
Based on discussions with the original author, the brutal CC and QUIC protocol parameters of
|
|
||||||
the old protocol (Hysteria 1) have been updated to be consistent with Hysteria 2
|
|
||||||
|
|
||||||
#### 1.6.0-beta.2
|
|
||||||
|
|
||||||
* Add TLS self sign key pair generate command
|
|
||||||
* Update brutal congestion control for Hysteria2
|
|
||||||
* Fix Clash cache crash on arm32 devices
|
|
||||||
* Update golang.org/x/net to v0.17.0
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.5.3
|
#### 1.5.3
|
||||||
|
|
||||||
* Fix compatibility with Android 14
|
* Fix compatibility with Android 14
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
# :material-decagram: Features
|
|
||||||
|
|
||||||
#### UI options
|
|
||||||
|
|
||||||
* Display realtime network speed in the notification
|
|
||||||
|
|
||||||
#### Service
|
|
||||||
|
|
||||||
SFA allows you to run sing-box through ForegroundService or VpnService (when TUN is required).
|
|
||||||
|
|
||||||
#### TUN
|
|
||||||
|
|
||||||
SFA provides an unprivileged TUN implementation through Android VpnService.
|
|
||||||
|
|
||||||
| TUN inbound option | Available | Note |
|
|
||||||
|-------------------------------|------------------|--------------------|
|
|
||||||
| `interface_name` | :material-close: | Managed by Android |
|
|
||||||
| `inet4_address` | :material-check: | / |
|
|
||||||
| `inet6_address` | :material-check: | / |
|
|
||||||
| `mtu` | :material-check: | / |
|
|
||||||
| `gso` | :material-close: | No permission |
|
|
||||||
| `gso_max_size` | :material-close: | No permission |
|
|
||||||
| `auto_route` | :material-check: | / |
|
|
||||||
| `strict_route` | :material-close: | Not implemented |
|
|
||||||
| `inet4_route_address` | :material-check: | / |
|
|
||||||
| `inet6_route_address` | :material-check: | / |
|
|
||||||
| `inet4_route_exclude_address` | :material-check: | / |
|
|
||||||
| `inet6_route_exclude_address` | :material-check: | / |
|
|
||||||
| `endpoint_independent_nat` | :material-check: | / |
|
|
||||||
| `stack` | :material-check: | / |
|
|
||||||
| `include_interface` | :material-close: | No permission |
|
|
||||||
| `exclude_interface` | :material-close: | No permission |
|
|
||||||
| `include_uid` | :material-close: | No permission |
|
|
||||||
| `exclude_uid` | :material-close: | No permission |
|
|
||||||
| `include_android_user` | :material-close: | No permission |
|
|
||||||
| `include_package` | :material-check: | / |
|
|
||||||
| `exclude_package` | :material-check: | / |
|
|
||||||
| `platform` | :material-check: | / |
|
|
||||||
|
|
||||||
| Route/DNS rule option | Available | Note |
|
|
||||||
|-----------------------|------------------|-----------------------------------|
|
|
||||||
| `process_name` | :material-close: | No permission |
|
|
||||||
| `process_path` | :material-close: | No permission |
|
|
||||||
| `package_name` | :material-check: | / |
|
|
||||||
| `user` | :material-close: | Use `package_name` instead |
|
|
||||||
| `user_id` | :material-close: | Use `package_name` instead |
|
|
||||||
| `wifi_ssid` | :material-check: | Fine location permission required |
|
|
||||||
| `wifi_bssid` | :material-check: | Fine location permission required |
|
|
||||||
|
|
||||||
### Override
|
|
||||||
|
|
||||||
Overrides profile configuration items with platform-specific values.
|
|
||||||
|
|
||||||
#### Per-app proxy
|
|
||||||
|
|
||||||
SFA allows you to select a list of Android apps that require proxying or bypassing in the graphical interface to
|
|
||||||
override the `include_package` and `exclude_package` configuration items.
|
|
||||||
|
|
||||||
In particular, the selector also provides the “China apps” scanning feature, providing Chinese users with an excellent
|
|
||||||
experience to bypass apps that do not require a proxy. Specifically, by scanning China application or SDK
|
|
||||||
characteristics through dex class path and other means, there will be almost no missed reports.
|
|
||||||
|
|
||||||
### Chore
|
|
||||||
|
|
||||||
* The working directory is located at `/sdcard/Android/data/io.nekohasekai.sfa/files` (External files directory)
|
|
||||||
* Crash logs is located in `$working_directory/stderr.log`
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
icon: material/android
|
|
||||||
---
|
|
||||||
|
|
||||||
# sing-box for Android
|
|
||||||
|
|
||||||
SFA allows users to manage and run local or remote sing-box configuration files, and provides
|
|
||||||
platform-specific function implementation, such as TUN transparent proxy implementation.
|
|
||||||
|
|
||||||
## :material-graph: Requirements
|
|
||||||
|
|
||||||
* Android 5.0+
|
|
||||||
|
|
||||||
## :material-download: Download
|
|
||||||
|
|
||||||
* [Play Store](https://play.google.com/store/apps/details?id=io.nekohasekai.sfa)
|
|
||||||
* [Play Store (Beta)](https://play.google.com/apps/testing/io.nekohasekai.sfa)
|
|
||||||
* [GitHub Releases](https://github.com/SagerNet/sing-box/releases)
|
|
||||||
|
|
||||||
## :material-source-repository: Source code
|
|
||||||
|
|
||||||
* [GitHub](https://github.com/SagerNet/sing-box-for-android)
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
# :material-decagram: Features
|
|
||||||
|
|
||||||
#### UI options
|
|
||||||
|
|
||||||
* Always On
|
|
||||||
* Include All Networks (Proxy traffic for LAN and cellular services)
|
|
||||||
* (Apple tvOS) Import profile from iPhone/iPad
|
|
||||||
|
|
||||||
#### Service
|
|
||||||
|
|
||||||
SFI/SFM/SFT allows you to run sing-box through NetworkExtension with Application Extension or System Extension.
|
|
||||||
|
|
||||||
#### TUN
|
|
||||||
|
|
||||||
SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension.
|
|
||||||
|
|
||||||
| TUN inbound option | Available | Note |
|
|
||||||
|-------------------------------|-------------------|-------------------|
|
|
||||||
| `interface_name` | :material-close:️ | Managed by Darwin |
|
|
||||||
| `inet4_address` | :material-check: | / |
|
|
||||||
| `inet6_address` | :material-check: | / |
|
|
||||||
| `mtu` | :material-check: | / |
|
|
||||||
| `gso` | :material-close: | Not implemented |
|
|
||||||
| `gso_max_size` | :material-close: | Not implemented |
|
|
||||||
| `auto_route` | :material-check: | / |
|
|
||||||
| `strict_route` | :material-close:️ | Not implemented |
|
|
||||||
| `inet4_route_address` | :material-check: | / |
|
|
||||||
| `inet6_route_address` | :material-check: | / |
|
|
||||||
| `inet4_route_exclude_address` | :material-check: | / |
|
|
||||||
| `inet6_route_exclude_address` | :material-check: | / |
|
|
||||||
| `endpoint_independent_nat` | :material-check: | / |
|
|
||||||
| `stack` | :material-check: | / |
|
|
||||||
| `include_interface` | :material-close:️ | Not implemented |
|
|
||||||
| `exclude_interface` | :material-close:️ | Not implemented |
|
|
||||||
| `include_uid` | :material-close:️ | Not implemented |
|
|
||||||
| `exclude_uid` | :material-close:️ | Not implemented |
|
|
||||||
| `include_android_user` | :material-close:️ | Not implemented |
|
|
||||||
| `include_package` | :material-close:️ | Not implemented |
|
|
||||||
| `exclude_package` | :material-close:️ | Not implemented |
|
|
||||||
| `platform` | :material-check: | / |
|
|
||||||
|
|
||||||
| Route/DNS rule option | Available | Note |
|
|
||||||
|-----------------------|------------------|-----------------------|
|
|
||||||
| `process_name` | :material-close: | No permission |
|
|
||||||
| `process_path` | :material-close: | No permission |
|
|
||||||
| `package_name` | :material-close: | / |
|
|
||||||
| `user` | :material-close: | No permission |
|
|
||||||
| `user_id` | :material-close: | No permission |
|
|
||||||
| `wifi_ssid` | :material-alert: | Only supported on iOS |
|
|
||||||
| `wifi_bssid` | :material-alert: | Only supported on iOS |
|
|
||||||
|
|
||||||
### Chore
|
|
||||||
|
|
||||||
* Crash logs is located in `Settings` -> `View Service Log`
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
---
|
|
||||||
icon: material/apple
|
|
||||||
---
|
|
||||||
|
|
||||||
# sing-box for Apple platforms
|
|
||||||
|
|
||||||
SFI/SFM/SFT allows users to manage and run local or remote sing-box configuration files, and provides
|
|
||||||
platform-specific function implementation, such as TUN transparent proxy implementation.
|
|
||||||
|
|
||||||
## :material-graph: Requirements
|
|
||||||
|
|
||||||
* iOS 15.0+ / macOS 13.0+ / Apple tvOS 17.0+
|
|
||||||
* An Apple account outside of mainland China
|
|
||||||
|
|
||||||
## :material-download: Download
|
|
||||||
|
|
||||||
* [App Store](https://apps.apple.com/us/app/sing-box/id6451272673)
|
|
||||||
* [TestFlight (Beta)](https://testflight.apple.com/join/AcqO44FH)
|
|
||||||
|
|
||||||
## :material-file-download: Download (macOS standalone version)
|
|
||||||
|
|
||||||
* [Homebrew Cask](https://formulae.brew.sh/cask/sfm)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew install sfm
|
|
||||||
```
|
|
||||||
|
|
||||||
* [GitHub Releases](https://github.com/SagerNet/sing-box/releases)
|
|
||||||
|
|
||||||
## :material-source-repository: Source code
|
|
||||||
|
|
||||||
* [GitHub](https://github.com/SagerNet/sing-box-for-apple)
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
---
|
|
||||||
icon: material/pencil-ruler
|
|
||||||
---
|
|
||||||
|
|
||||||
# General
|
|
||||||
|
|
||||||
Describes and explains the functions implemented uniformly by sing-box graphical clients.
|
|
||||||
|
|
||||||
### Profile
|
|
||||||
|
|
||||||
Profile describes a sing-box configuration file and its state.
|
|
||||||
|
|
||||||
#### Local
|
|
||||||
|
|
||||||
* Local Profile represents a local sing-box configuration with minimal state
|
|
||||||
* The graphical client must provide an editor to modify configuration content
|
|
||||||
|
|
||||||
#### iCloud (on iOS and macOS)
|
|
||||||
|
|
||||||
* iCloud Profile represents a remote sing-box configuration with iCloud as the update source
|
|
||||||
* The configuration file is stored in the sing-box folder under iCloud
|
|
||||||
* The graphical client must provide an editor to modify configuration content
|
|
||||||
|
|
||||||
#### Remote
|
|
||||||
|
|
||||||
* Remote Profile represents a remote sing-box configuration with a URL as the update source.
|
|
||||||
* The graphical client should provide a configuration content viewer
|
|
||||||
* The graphical client must implement automatic profile update (default interval is 60 minutes) and HTTP Basic
|
|
||||||
authorization.
|
|
||||||
|
|
||||||
At the same time, the graphical client must provide support for importing remote profiles
|
|
||||||
through a specific URL Scheme. The URL is defined as follows:
|
|
||||||
|
|
||||||
```
|
|
||||||
sing-box://import-remote-profile?url=urlEncodedURL#urlEncodedName
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dashboard
|
|
||||||
|
|
||||||
While the sing-box service is running, the graphical client should provide a Dashboard interface to manage the service.
|
|
||||||
|
|
||||||
#### Status
|
|
||||||
|
|
||||||
Dashboard should display status information such as memory, connection, and traffic.
|
|
||||||
|
|
||||||
#### Mode
|
|
||||||
|
|
||||||
Dashboard should provide a Mode selector for switching when the configuration uses at least two `clash_mode` values.
|
|
||||||
|
|
||||||
#### Groups
|
|
||||||
|
|
||||||
When the configuration includes group outbounds (specifically, Selector or URLTest),
|
|
||||||
the dashboard should provide a Group selector for status display or switching.
|
|
||||||
|
|
||||||
### Chore
|
|
||||||
|
|
||||||
#### Core
|
|
||||||
|
|
||||||
Graphical clients should provide a Core region:
|
|
||||||
|
|
||||||
* Display the current sing-box version
|
|
||||||
* Provides a button to clean the working directory
|
|
||||||
* Provides a memory limiter switch
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# :material-cellphone-link: Graphical Clients
|
|
||||||
|
|
||||||
Maintained by Project S to provide a unified experience and platform-specific functionality.
|
|
||||||
|
|
||||||
| Platform | Client |
|
|
||||||
|---------------------------------------|-----------------------------------------|
|
|
||||||
| :material-android: Android | [sing-box for Android](./android) |
|
|
||||||
| :material-apple: iOS/macOS/Apple tvOS | [sing-box for Apple platforms](./apple) |
|
|
||||||
| :material-laptop: Desktop | Working in progress |
|
|
||||||
|
|
||||||
Some third-party projects that claim to use sing-box or use sing-box as a selling point are not listed here. The core
|
|
||||||
motivation of the maintainers of such projects is to acquire more users, and even though they provide friendly VPN
|
|
||||||
client features, the code is usually of poor quality and contains ads.
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# :material-cellphone-link: 图形界面客户端
|
|
||||||
|
|
||||||
由 Project S 维护,提供统一的体验与平台特定的功能。
|
|
||||||
|
|
||||||
| 平台 | 客户端 |
|
|
||||||
|---------------------------------------|-----------------------------------------|
|
|
||||||
| :material-android: Android | [sing-box for Android](./android) |
|
|
||||||
| :material-apple: iOS/macOS/Apple tvOS | [sing-box for Apple platforms](./apple) |
|
|
||||||
| :material-laptop: Desktop | 施工中 |
|
|
||||||
|
|
||||||
此处没有列出一些声称使用或以 sing-box 为卖点的第三方项目。此类项目维护者的动机是获得更多用户,即使它们提供友好的商业
|
|
||||||
VPN 客户端功能, 但代码质量很差且包含广告。
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
icon: material/security
|
|
||||||
---
|
|
||||||
|
|
||||||
# Privacy policy
|
|
||||||
|
|
||||||
sing-box and official graphics clients do not collect or share personal data,
|
|
||||||
and the data generated by the software is always on your device.
|
|
||||||
@@ -1,14 +1,3 @@
|
|||||||
---
|
|
||||||
icon: material/alert-decagram
|
|
||||||
---
|
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.8.0"
|
|
||||||
|
|
||||||
:material-plus: [rule_set](#rule_set)
|
|
||||||
:material-plus: [source_ip_is_private](#source_ip_is_private)
|
|
||||||
:material-delete-clock: [geoip](#geoip)
|
|
||||||
:material-delete-clock: [geosite](#geosite)
|
|
||||||
|
|
||||||
### Structure
|
### Structure
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -57,7 +46,6 @@ icon: material/alert-decagram
|
|||||||
"10.0.0.0/24",
|
"10.0.0.0/24",
|
||||||
"192.168.0.1"
|
"192.168.0.1"
|
||||||
],
|
],
|
||||||
"source_ip_is_private": false,
|
|
||||||
"source_port": [
|
"source_port": [
|
||||||
12345
|
12345
|
||||||
],
|
],
|
||||||
@@ -91,16 +79,6 @@ icon: material/alert-decagram
|
|||||||
1000
|
1000
|
||||||
],
|
],
|
||||||
"clash_mode": "direct",
|
"clash_mode": "direct",
|
||||||
"wifi_ssid": [
|
|
||||||
"My WIFI"
|
|
||||||
],
|
|
||||||
"wifi_bssid": [
|
|
||||||
"00:00:00:00:00:00"
|
|
||||||
],
|
|
||||||
"rule_set": [
|
|
||||||
"geoip-cn",
|
|
||||||
"geosite-cn"
|
|
||||||
],
|
|
||||||
"invert": false,
|
"invert": false,
|
||||||
"outbound": [
|
"outbound": [
|
||||||
"direct"
|
"direct"
|
||||||
@@ -134,12 +112,10 @@ icon: material/alert-decagram
|
|||||||
The default rule uses the following matching logic:
|
The default rule uses the following matching logic:
|
||||||
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite`) &&
|
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite`) &&
|
||||||
(`port` || `port_range`) &&
|
(`port` || `port_range`) &&
|
||||||
(`source_geoip` || `source_ip_cidr` || `source_ip_is_private`) &&
|
(`source_geoip` || `source_ip_cidr`) &&
|
||||||
(`source_port` || `source_port_range`) &&
|
(`source_port` || `source_port_range`) &&
|
||||||
`other fields`
|
`other fields`
|
||||||
|
|
||||||
Additionally, included rule sets can be considered merged rather than as a single rule sub-item.
|
|
||||||
|
|
||||||
#### inbound
|
#### inbound
|
||||||
|
|
||||||
Tags of [Inbound](/configuration/inbound).
|
Tags of [Inbound](/configuration/inbound).
|
||||||
@@ -184,29 +160,15 @@ Match domain using regular expression.
|
|||||||
|
|
||||||
#### geosite
|
#### geosite
|
||||||
|
|
||||||
!!! failure "Deprecated in sing-box 1.8.0"
|
|
||||||
|
|
||||||
Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets).
|
|
||||||
|
|
||||||
Match geosite.
|
Match geosite.
|
||||||
|
|
||||||
#### source_geoip
|
#### source_geoip
|
||||||
|
|
||||||
!!! failure "Deprecated in sing-box 1.8.0"
|
|
||||||
|
|
||||||
GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets).
|
|
||||||
|
|
||||||
Match source geoip.
|
Match source geoip.
|
||||||
|
|
||||||
#### source_ip_cidr
|
#### source_ip_cidr
|
||||||
|
|
||||||
Match source IP CIDR.
|
Match source ip cidr.
|
||||||
|
|
||||||
#### source_ip_is_private
|
|
||||||
|
|
||||||
!!! question "Since sing-box 1.8.0"
|
|
||||||
|
|
||||||
Match non-public source IP.
|
|
||||||
|
|
||||||
#### source_port
|
#### source_port
|
||||||
|
|
||||||
@@ -226,7 +188,7 @@ Match port range.
|
|||||||
|
|
||||||
#### process_name
|
#### process_name
|
||||||
|
|
||||||
!!! quote ""
|
!!! error ""
|
||||||
|
|
||||||
Only supported on Linux, Windows, and macOS.
|
Only supported on Linux, Windows, and macOS.
|
||||||
|
|
||||||
@@ -234,7 +196,7 @@ Match process name.
|
|||||||
|
|
||||||
#### process_path
|
#### process_path
|
||||||
|
|
||||||
!!! quote ""
|
!!! error ""
|
||||||
|
|
||||||
Only supported on Linux, Windows, and macOS.
|
Only supported on Linux, Windows, and macOS.
|
||||||
|
|
||||||
@@ -246,7 +208,7 @@ Match android package name.
|
|||||||
|
|
||||||
#### user
|
#### user
|
||||||
|
|
||||||
!!! quote ""
|
!!! error ""
|
||||||
|
|
||||||
Only supported on Linux.
|
Only supported on Linux.
|
||||||
|
|
||||||
@@ -254,7 +216,7 @@ Match user name.
|
|||||||
|
|
||||||
#### user_id
|
#### user_id
|
||||||
|
|
||||||
!!! quote ""
|
!!! error ""
|
||||||
|
|
||||||
Only supported on Linux.
|
Only supported on Linux.
|
||||||
|
|
||||||
@@ -264,30 +226,6 @@ Match user id.
|
|||||||
|
|
||||||
Match Clash mode.
|
Match Clash mode.
|
||||||
|
|
||||||
#### wifi_ssid
|
|
||||||
|
|
||||||
<!-- md:version 1.7.0-beta.4 -->
|
|
||||||
|
|
||||||
!!! quote ""
|
|
||||||
|
|
||||||
Only supported in graphical clients on Android and iOS.
|
|
||||||
|
|
||||||
Match WiFi SSID.
|
|
||||||
|
|
||||||
#### wifi_bssid
|
|
||||||
|
|
||||||
!!! quote ""
|
|
||||||
|
|
||||||
Only supported in graphical clients on Android and iOS.
|
|
||||||
|
|
||||||
Match WiFi BSSID.
|
|
||||||
|
|
||||||
#### rule_set
|
|
||||||
|
|
||||||
!!! question "Since sing-box 1.8.0"
|
|
||||||
|
|
||||||
Match [Rule Set](/configuration/route/#rule_set).
|
|
||||||
|
|
||||||
#### invert
|
#### invert
|
||||||
|
|
||||||
Invert match result.
|
Invert match result.
|
||||||
@@ -324,4 +262,4 @@ Rewrite TTL in DNS responses.
|
|||||||
|
|
||||||
#### rules
|
#### rules
|
||||||
|
|
||||||
Included rules.
|
Included default rules.
|
||||||
@@ -1,14 +1,3 @@
|
|||||||
---
|
|
||||||
icon: material/alert-decagram
|
|
||||||
---
|
|
||||||
|
|
||||||
!!! quote "sing-box 1.8.0 中的更改"
|
|
||||||
|
|
||||||
:material-plus: [rule_set](#rule_set)
|
|
||||||
:material-plus: [source_ip_is_private](#source_ip_is_private)
|
|
||||||
:material-delete-clock: [geoip](#geoip)
|
|
||||||
:material-delete-clock: [geosite](#geosite)
|
|
||||||
|
|
||||||
### 结构
|
### 结构
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -56,7 +45,6 @@ icon: material/alert-decagram
|
|||||||
"source_ip_cidr": [
|
"source_ip_cidr": [
|
||||||
"10.0.0.0/24"
|
"10.0.0.0/24"
|
||||||
],
|
],
|
||||||
"source_ip_is_private": false,
|
|
||||||
"source_port": [
|
"source_port": [
|
||||||
12345
|
12345
|
||||||
],
|
],
|
||||||
@@ -90,16 +78,6 @@ icon: material/alert-decagram
|
|||||||
1000
|
1000
|
||||||
],
|
],
|
||||||
"clash_mode": "direct",
|
"clash_mode": "direct",
|
||||||
"wifi_ssid": [
|
|
||||||
"My WIFI"
|
|
||||||
],
|
|
||||||
"wifi_bssid": [
|
|
||||||
"00:00:00:00:00:00"
|
|
||||||
],
|
|
||||||
"rule_set": [
|
|
||||||
"geoip-cn",
|
|
||||||
"geosite-cn"
|
|
||||||
],
|
|
||||||
"invert": false,
|
"invert": false,
|
||||||
"outbound": [
|
"outbound": [
|
||||||
"direct"
|
"direct"
|
||||||
@@ -131,12 +109,10 @@ icon: material/alert-decagram
|
|||||||
默认规则使用以下匹配逻辑:
|
默认规则使用以下匹配逻辑:
|
||||||
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite`) &&
|
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite`) &&
|
||||||
(`port` || `port_range`) &&
|
(`port` || `port_range`) &&
|
||||||
(`source_geoip` || `source_ip_cidr` || `source_ip_is_private`) &&
|
(`source_geoip` || `source_ip_cidr`) &&
|
||||||
(`source_port` || `source_port_range`) &&
|
(`source_port` || `source_port_range`) &&
|
||||||
`other fields`
|
`other fields`
|
||||||
|
|
||||||
另外,引用的规则集可视为被合并,而不是作为一个单独的规则子项。
|
|
||||||
|
|
||||||
#### inbound
|
#### inbound
|
||||||
|
|
||||||
[入站](/zh/configuration/inbound) 标签.
|
[入站](/zh/configuration/inbound) 标签.
|
||||||
@@ -181,30 +157,16 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
|||||||
|
|
||||||
#### geosite
|
#### geosite
|
||||||
|
|
||||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
匹配 GeoSite。
|
||||||
|
|
||||||
Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geosite)。
|
|
||||||
|
|
||||||
匹配 Geosite。
|
|
||||||
|
|
||||||
#### source_geoip
|
#### source_geoip
|
||||||
|
|
||||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
|
||||||
|
|
||||||
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。
|
|
||||||
|
|
||||||
匹配源 GeoIP。
|
匹配源 GeoIP。
|
||||||
|
|
||||||
#### source_ip_cidr
|
#### source_ip_cidr
|
||||||
|
|
||||||
匹配源 IP CIDR。
|
匹配源 IP CIDR。
|
||||||
|
|
||||||
#### source_ip_is_private
|
|
||||||
|
|
||||||
!!! question "自 sing-box 1.8.0 起"
|
|
||||||
|
|
||||||
匹配非公开源 IP。
|
|
||||||
|
|
||||||
#### source_port
|
#### source_port
|
||||||
|
|
||||||
匹配源端口。
|
匹配源端口。
|
||||||
@@ -223,7 +185,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
|||||||
|
|
||||||
#### process_name
|
#### process_name
|
||||||
|
|
||||||
!!! quote ""
|
!!! error ""
|
||||||
|
|
||||||
仅支持 Linux、Windows 和 macOS.
|
仅支持 Linux、Windows 和 macOS.
|
||||||
|
|
||||||
@@ -231,7 +193,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
|||||||
|
|
||||||
#### process_path
|
#### process_path
|
||||||
|
|
||||||
!!! quote ""
|
!!! error ""
|
||||||
|
|
||||||
仅支持 Linux、Windows 和 macOS.
|
仅支持 Linux、Windows 和 macOS.
|
||||||
|
|
||||||
@@ -243,7 +205,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
|||||||
|
|
||||||
#### user
|
#### user
|
||||||
|
|
||||||
!!! quote ""
|
!!! error ""
|
||||||
|
|
||||||
仅支持 Linux。
|
仅支持 Linux。
|
||||||
|
|
||||||
@@ -251,7 +213,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
|||||||
|
|
||||||
#### user_id
|
#### user_id
|
||||||
|
|
||||||
!!! quote ""
|
!!! error ""
|
||||||
|
|
||||||
仅支持 Linux。
|
仅支持 Linux。
|
||||||
|
|
||||||
@@ -261,28 +223,6 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
|||||||
|
|
||||||
匹配 Clash 模式。
|
匹配 Clash 模式。
|
||||||
|
|
||||||
#### wifi_ssid
|
|
||||||
|
|
||||||
!!! quote ""
|
|
||||||
|
|
||||||
仅在 Android 与 iOS 的图形客户端中支持。
|
|
||||||
|
|
||||||
匹配 WiFi SSID。
|
|
||||||
|
|
||||||
#### wifi_bssid
|
|
||||||
|
|
||||||
!!! quote ""
|
|
||||||
|
|
||||||
仅在 Android 与 iOS 的图形客户端中支持。
|
|
||||||
|
|
||||||
匹配 WiFi BSSID。
|
|
||||||
|
|
||||||
#### rule_set
|
|
||||||
|
|
||||||
!!! question "自 sing-box 1.8.0 起"
|
|
||||||
|
|
||||||
匹配[规则集](/zh/configuration/route/#rule_set)。
|
|
||||||
|
|
||||||
#### invert
|
#### invert
|
||||||
|
|
||||||
反选匹配结果。
|
反选匹配结果。
|
||||||
@@ -319,4 +259,4 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
|||||||
|
|
||||||
#### rules
|
#### rules
|
||||||
|
|
||||||
包括的规则。
|
包括的默认规则。
|
||||||
@@ -45,12 +45,20 @@ The address of the dns server.
|
|||||||
|
|
||||||
!!! warning ""
|
!!! warning ""
|
||||||
|
|
||||||
To ensure that Android system DNS is in effect, rather than Go's built-in default resolver, enable CGO at compile time.
|
To ensure that system DNS is in effect, rather than Go's built-in default resolver, enable CGO at compile time.
|
||||||
|
|
||||||
|
!!! warning ""
|
||||||
|
|
||||||
|
QUIC and HTTP3 transport is not included by default, see [Installation](/#installation).
|
||||||
|
|
||||||
!!! info ""
|
!!! info ""
|
||||||
|
|
||||||
the RCode transport is often used to block queries. Use with rules and the `disable_cache` rule option.
|
the RCode transport is often used to block queries. Use with rules and the `disable_cache` rule option.
|
||||||
|
|
||||||
|
!!! warning ""
|
||||||
|
|
||||||
|
DHCP transport is not included by default, see [Installation](/#installation).
|
||||||
|
|
||||||
| RCode | Description |
|
| RCode | Description |
|
||||||
|-------------------|-----------------------|
|
|-------------------|-----------------------|
|
||||||
| `success` | `No error` |
|
| `success` | `No error` |
|
||||||
|
|||||||
@@ -45,12 +45,20 @@ DNS 服务器的地址。
|
|||||||
|
|
||||||
!!! warning ""
|
!!! warning ""
|
||||||
|
|
||||||
为了确保 Android 系统 DNS 生效,而不是 Go 的内置默认解析器,请在编译时启用 CGO。
|
为了确保系统 DNS 生效,而不是 Go 的内置默认解析器,请在编译时启用 CGO。
|
||||||
|
|
||||||
|
!!! warning ""
|
||||||
|
|
||||||
|
默认安装不包含 QUIC 和 HTTP3 传输层,请参阅 [安装](/zh/#_2)。
|
||||||
|
|
||||||
!!! info ""
|
!!! info ""
|
||||||
|
|
||||||
RCode 传输层传输层常用于屏蔽请求. 与 DNS 规则和 `disable_cache` 规则选项一起使用。
|
RCode 传输层传输层常用于屏蔽请求. 与 DNS 规则和 `disable_cache` 规则选项一起使用。
|
||||||
|
|
||||||
|
!!! warning ""
|
||||||
|
|
||||||
|
默认安装不包含 DHCP 传输层,请参阅 [安装](/zh/#_2)。
|
||||||
|
|
||||||
| RCode | 描述 |
|
| RCode | 描述 |
|
||||||
|-------------------|----------|
|
|-------------------|----------|
|
||||||
| `success` | `无错误` |
|
| `success` | `无错误` |
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
---
|
|
||||||
icon: material/new-box
|
|
||||||
---
|
|
||||||
|
|
||||||
!!! question "Since sing-box 1.8.0"
|
|
||||||
|
|
||||||
### Structure
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"path": "",
|
|
||||||
"cache_id": "",
|
|
||||||
"store_fakeip": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Fields
|
|
||||||
|
|
||||||
#### enabled
|
|
||||||
|
|
||||||
Enable cache file.
|
|
||||||
|
|
||||||
#### path
|
|
||||||
|
|
||||||
Path to the cache file.
|
|
||||||
|
|
||||||
`cache.db` will be used if empty.
|
|
||||||
|
|
||||||
#### cache_id
|
|
||||||
|
|
||||||
Identifier in cache file.
|
|
||||||
|
|
||||||
If not empty, configuration specified data will use a separate store keyed by it.
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
---
|
|
||||||
icon: material/new-box
|
|
||||||
---
|
|
||||||
|
|
||||||
!!! question "自 sing-box 1.8.0 起"
|
|
||||||
|
|
||||||
### 结构
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"path": "",
|
|
||||||
"cache_id": "",
|
|
||||||
"store_fakeip": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 字段
|
|
||||||
|
|
||||||
#### enabled
|
|
||||||
|
|
||||||
启用缓存文件。
|
|
||||||
|
|
||||||
#### path
|
|
||||||
|
|
||||||
缓存文件路径,默认使用`cache.db`。
|
|
||||||
|
|
||||||
#### cache_id
|
|
||||||
|
|
||||||
缓存文件中的标识符。
|
|
||||||
|
|
||||||
如果不为空,配置特定的数据将使用由其键控的单独存储。
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
---
|
|
||||||
icon: material/alert-decagram
|
|
||||||
---
|
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.8.0"
|
|
||||||
|
|
||||||
:material-delete-alert: [store_mode](#store_mode)
|
|
||||||
:material-delete-alert: [store_selected](#store_selected)
|
|
||||||
:material-delete-alert: [store_fakeip](#store_fakeip)
|
|
||||||
:material-delete-alert: [cache_file](#cache_file)
|
|
||||||
:material-delete-alert: [cache_id](#cache_id)
|
|
||||||
|
|
||||||
### Structure
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"external_controller": "127.0.0.1:9090",
|
|
||||||
"external_ui": "",
|
|
||||||
"external_ui_download_url": "",
|
|
||||||
"external_ui_download_detour": "",
|
|
||||||
"secret": "",
|
|
||||||
"default_mode": "",
|
|
||||||
|
|
||||||
// Deprecated
|
|
||||||
|
|
||||||
"store_mode": false,
|
|
||||||
"store_selected": false,
|
|
||||||
"store_fakeip": false,
|
|
||||||
"cache_file": "",
|
|
||||||
"cache_id": ""
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Fields
|
|
||||||
|
|
||||||
#### external_controller
|
|
||||||
|
|
||||||
RESTful web API listening address. Clash API will be disabled if empty.
|
|
||||||
|
|
||||||
#### external_ui
|
|
||||||
|
|
||||||
A relative path to the configuration directory or an absolute path to a
|
|
||||||
directory in which you put some static web resource. sing-box will then
|
|
||||||
serve it at `http://{{external-controller}}/ui`.
|
|
||||||
|
|
||||||
#### external_ui_download_url
|
|
||||||
|
|
||||||
ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty.
|
|
||||||
|
|
||||||
`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty.
|
|
||||||
|
|
||||||
#### external_ui_download_detour
|
|
||||||
|
|
||||||
The tag of the outbound to download the external UI.
|
|
||||||
|
|
||||||
Default outbound will be used if empty.
|
|
||||||
|
|
||||||
#### secret
|
|
||||||
|
|
||||||
Secret for the RESTful API (optional)
|
|
||||||
Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}`
|
|
||||||
ALWAYS set a secret if RESTful API is listening on 0.0.0.0
|
|
||||||
|
|
||||||
#### default_mode
|
|
||||||
|
|
||||||
Default mode in clash, `Rule` will be used if empty.
|
|
||||||
|
|
||||||
This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item.
|
|
||||||
|
|
||||||
#### store_mode
|
|
||||||
|
|
||||||
!!! failure "Deprecated in sing-box 1.8.0"
|
|
||||||
|
|
||||||
`store_mode` is deprecated in Clash API and enabled by default if `cache_file.enabled`.
|
|
||||||
|
|
||||||
Store Clash mode in cache file.
|
|
||||||
|
|
||||||
#### store_selected
|
|
||||||
|
|
||||||
!!! failure "Deprecated in sing-box 1.8.0"
|
|
||||||
|
|
||||||
`store_selected` is deprecated in Clash API and enabled by default if `cache_file.enabled`.
|
|
||||||
|
|
||||||
!!! note ""
|
|
||||||
|
|
||||||
The tag must be set for target outbounds.
|
|
||||||
|
|
||||||
Store selected outbound for the `Selector` outbound in cache file.
|
|
||||||
|
|
||||||
#### store_fakeip
|
|
||||||
|
|
||||||
!!! failure "Deprecated in sing-box 1.8.0"
|
|
||||||
|
|
||||||
`store_selected` is deprecated in Clash API and migrated to `cache_file.store_fakeip`.
|
|
||||||
|
|
||||||
Store fakeip in cache file.
|
|
||||||
|
|
||||||
#### cache_file
|
|
||||||
|
|
||||||
!!! failure "Deprecated in sing-box 1.8.0"
|
|
||||||
|
|
||||||
`cache_file` is deprecated in Clash API and migrated to `cache_file.enabled` and `cache_file.path`.
|
|
||||||
|
|
||||||
Cache file path, `cache.db` will be used if empty.
|
|
||||||
|
|
||||||
#### cache_id
|
|
||||||
|
|
||||||
!!! failure "Deprecated in sing-box 1.8.0"
|
|
||||||
|
|
||||||
`cache_id` is deprecated in Clash API and migrated to `cache_file.cache_id`.
|
|
||||||
|
|
||||||
Identifier in cache file.
|
|
||||||
|
|
||||||
If not empty, configuration specified data will use a separate store keyed by it.
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
---
|
|
||||||
icon: material/alert-decagram
|
|
||||||
---
|
|
||||||
|
|
||||||
!!! quote "sing-box 1.8.0 中的更改"
|
|
||||||
|
|
||||||
:material-delete-alert: [store_mode](#store_mode)
|
|
||||||
:material-delete-alert: [store_selected](#store_selected)
|
|
||||||
:material-delete-alert: [store_fakeip](#store_fakeip)
|
|
||||||
:material-delete-alert: [cache_file](#cache_file)
|
|
||||||
:material-delete-alert: [cache_id](#cache_id)
|
|
||||||
|
|
||||||
### 结构
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"external_controller": "127.0.0.1:9090",
|
|
||||||
"external_ui": "",
|
|
||||||
"external_ui_download_url": "",
|
|
||||||
"external_ui_download_detour": "",
|
|
||||||
"secret": "",
|
|
||||||
"default_mode": "",
|
|
||||||
|
|
||||||
// Deprecated
|
|
||||||
|
|
||||||
"store_mode": false,
|
|
||||||
"store_selected": false,
|
|
||||||
"store_fakeip": false,
|
|
||||||
"cache_file": "",
|
|
||||||
"cache_id": ""
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Fields
|
|
||||||
|
|
||||||
#### external_controller
|
|
||||||
|
|
||||||
RESTful web API 监听地址。如果为空,则禁用 Clash API。
|
|
||||||
|
|
||||||
#### external_ui
|
|
||||||
|
|
||||||
到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。
|
|
||||||
|
|
||||||
#### external_ui_download_url
|
|
||||||
|
|
||||||
静态网页资源的 ZIP 下载 URL,如果指定的 `external_ui` 目录为空,将使用。
|
|
||||||
|
|
||||||
默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`。
|
|
||||||
|
|
||||||
#### external_ui_download_detour
|
|
||||||
|
|
||||||
用于下载静态网页资源的出站的标签。
|
|
||||||
|
|
||||||
如果为空,将使用默认出站。
|
|
||||||
|
|
||||||
#### secret
|
|
||||||
|
|
||||||
RESTful API 的密钥(可选)
|
|
||||||
通过指定 HTTP 标头 `Authorization: Bearer ${secret}` 进行身份验证
|
|
||||||
如果 RESTful API 正在监听 0.0.0.0,请始终设置一个密钥。
|
|
||||||
|
|
||||||
#### default_mode
|
|
||||||
|
|
||||||
Clash 中的默认模式,默认使用 `Rule`。
|
|
||||||
|
|
||||||
此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。
|
|
||||||
|
|
||||||
#### store_mode
|
|
||||||
|
|
||||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
|
||||||
|
|
||||||
`store_mode` 已在 Clash API 中废弃,且默认启用当 `cache_file.enabled`。
|
|
||||||
|
|
||||||
将 Clash 模式存储在缓存文件中。
|
|
||||||
|
|
||||||
#### store_selected
|
|
||||||
|
|
||||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
|
||||||
|
|
||||||
`store_selected` 已在 Clash API 中废弃,且默认启用当 `cache_file.enabled`。
|
|
||||||
|
|
||||||
!!! note ""
|
|
||||||
|
|
||||||
必须为目标出站设置标签。
|
|
||||||
|
|
||||||
将 `Selector` 中出站的选定的目标出站存储在缓存文件中。
|
|
||||||
|
|
||||||
#### store_fakeip
|
|
||||||
|
|
||||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
|
||||||
|
|
||||||
`store_selected` 已在 Clash API 中废弃,且已迁移到 `cache_file.store_fakeip`。
|
|
||||||
|
|
||||||
将 fakeip 存储在缓存文件中。
|
|
||||||
|
|
||||||
#### cache_file
|
|
||||||
|
|
||||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
|
||||||
|
|
||||||
`cache_file` 已在 Clash API 中废弃,且已迁移到 `cache_file.enabled` 和 `cache_file.path`。
|
|
||||||
|
|
||||||
缓存文件路径,默认使用`cache.db`。
|
|
||||||
|
|
||||||
#### cache_id
|
|
||||||
|
|
||||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
|
||||||
|
|
||||||
`cache_id` 已在 Clash API 中废弃,且已迁移到 `cache_file.cache_id`。
|
|
||||||
|
|
||||||
缓存 ID。
|
|
||||||
|
|
||||||
如果不为空,配置特定的数据将使用由其键控的单独存储。
|
|
||||||
@@ -1,30 +1,139 @@
|
|||||||
---
|
|
||||||
icon: material/alert-decagram
|
|
||||||
---
|
|
||||||
|
|
||||||
# Experimental
|
# Experimental
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.8.0"
|
|
||||||
|
|
||||||
:material-plus: [cache_file](#cache_file)
|
|
||||||
:material-alert-decagram: [clash_api](#clash_api)
|
|
||||||
|
|
||||||
### Structure
|
### Structure
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"experimental": {
|
"experimental": {
|
||||||
"cache_file": {},
|
"clash_api": {
|
||||||
"clash_api": {},
|
"external_controller": "127.0.0.1:9090",
|
||||||
"v2ray_api": {}
|
"external_ui": "",
|
||||||
|
"external_ui_download_url": "",
|
||||||
|
"external_ui_download_detour": "",
|
||||||
|
"secret": "",
|
||||||
|
"default_mode": "",
|
||||||
|
"store_mode": false,
|
||||||
|
"store_selected": false,
|
||||||
|
"store_fakeip": false,
|
||||||
|
"cache_file": "",
|
||||||
|
"cache_id": ""
|
||||||
|
},
|
||||||
|
"v2ray_api": {
|
||||||
|
"listen": "127.0.0.1:8080",
|
||||||
|
"stats": {
|
||||||
|
"enabled": true,
|
||||||
|
"inbounds": [
|
||||||
|
"socks-in"
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
"proxy",
|
||||||
|
"direct"
|
||||||
|
],
|
||||||
|
"users": [
|
||||||
|
"sekai"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fields
|
!!! note ""
|
||||||
|
|
||||||
| Key | Format |
|
Traffic statistics and connection management can degrade performance.
|
||||||
|--------------|----------------------------|
|
|
||||||
| `cache_file` | [Cache File](./cache-file) |
|
### Clash API Fields
|
||||||
| `clash_api` | [Clash API](./clash-api) |
|
|
||||||
| `v2ray_api` | [V2Ray API](./v2ray-api) |
|
!!! error ""
|
||||||
|
|
||||||
|
Clash API is not included by default, see [Installation](/#installation).
|
||||||
|
|
||||||
|
#### external_controller
|
||||||
|
|
||||||
|
RESTful web API listening address. Clash API will be disabled if empty.
|
||||||
|
|
||||||
|
#### external_ui
|
||||||
|
|
||||||
|
A relative path to the configuration directory or an absolute path to a
|
||||||
|
directory in which you put some static web resource. sing-box will then
|
||||||
|
serve it at `http://{{external-controller}}/ui`.
|
||||||
|
|
||||||
|
#### external_ui_download_url
|
||||||
|
|
||||||
|
ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty.
|
||||||
|
|
||||||
|
`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty.
|
||||||
|
|
||||||
|
#### external_ui_download_detour
|
||||||
|
|
||||||
|
The tag of the outbound to download the external UI.
|
||||||
|
|
||||||
|
Default outbound will be used if empty.
|
||||||
|
|
||||||
|
#### secret
|
||||||
|
|
||||||
|
Secret for the RESTful API (optional)
|
||||||
|
Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}`
|
||||||
|
ALWAYS set a secret if RESTful API is listening on 0.0.0.0
|
||||||
|
|
||||||
|
#### default_mode
|
||||||
|
|
||||||
|
Default mode in clash, `Rule` will be used if empty.
|
||||||
|
|
||||||
|
This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item.
|
||||||
|
|
||||||
|
#### store_mode
|
||||||
|
|
||||||
|
Store Clash mode in cache file.
|
||||||
|
|
||||||
|
#### store_selected
|
||||||
|
|
||||||
|
!!! note ""
|
||||||
|
|
||||||
|
The tag must be set for target outbounds.
|
||||||
|
|
||||||
|
Store selected outbound for the `Selector` outbound in cache file.
|
||||||
|
|
||||||
|
#### store_fakeip
|
||||||
|
|
||||||
|
Store fakeip in cache file.
|
||||||
|
|
||||||
|
#### cache_file
|
||||||
|
|
||||||
|
Cache file path, `cache.db` will be used if empty.
|
||||||
|
|
||||||
|
#### cache_id
|
||||||
|
|
||||||
|
Cache ID.
|
||||||
|
|
||||||
|
If not empty, `store_selected` will use a separate store keyed by it.
|
||||||
|
|
||||||
|
### V2Ray API Fields
|
||||||
|
|
||||||
|
!!! error ""
|
||||||
|
|
||||||
|
V2Ray API is not included by default, see [Installation](/#installation).
|
||||||
|
|
||||||
|
#### listen
|
||||||
|
|
||||||
|
gRPC API listening address. V2Ray API will be disabled if empty.
|
||||||
|
|
||||||
|
#### stats
|
||||||
|
|
||||||
|
Traffic statistics service settings.
|
||||||
|
|
||||||
|
#### stats.enabled
|
||||||
|
|
||||||
|
Enable statistics service.
|
||||||
|
|
||||||
|
#### stats.inbounds
|
||||||
|
|
||||||
|
Inbound list to count traffic.
|
||||||
|
|
||||||
|
#### stats.outbounds
|
||||||
|
|
||||||
|
Outbound list to count traffic.
|
||||||
|
|
||||||
|
#### stats.users
|
||||||
|
|
||||||
|
User list to count traffic.
|
||||||
@@ -1,30 +1,137 @@
|
|||||||
---
|
|
||||||
icon: material/alert-decagram
|
|
||||||
---
|
|
||||||
|
|
||||||
# 实验性
|
# 实验性
|
||||||
|
|
||||||
!!! quote "sing-box 1.8.0 中的更改"
|
|
||||||
|
|
||||||
:material-plus: [cache_file](#cache_file)
|
|
||||||
:material-alert-decagram: [clash_api](#clash_api)
|
|
||||||
|
|
||||||
### 结构
|
### 结构
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"experimental": {
|
"experimental": {
|
||||||
"cache_file": {},
|
"clash_api": {
|
||||||
"clash_api": {},
|
"external_controller": "127.0.0.1:9090",
|
||||||
"v2ray_api": {}
|
"external_ui": "",
|
||||||
|
"external_ui_download_url": "",
|
||||||
|
"external_ui_download_detour": "",
|
||||||
|
"secret": "",
|
||||||
|
"default_mode": "",
|
||||||
|
"store_mode": false,
|
||||||
|
"store_selected": false,
|
||||||
|
"store_fakeip": false,
|
||||||
|
"cache_file": "",
|
||||||
|
"cache_id": ""
|
||||||
|
},
|
||||||
|
"v2ray_api": {
|
||||||
|
"listen": "127.0.0.1:8080",
|
||||||
|
"stats": {
|
||||||
|
"enabled": true,
|
||||||
|
"inbounds": [
|
||||||
|
"socks-in"
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
"proxy",
|
||||||
|
"direct"
|
||||||
|
],
|
||||||
|
"users": [
|
||||||
|
"sekai"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 字段
|
!!! note ""
|
||||||
|
|
||||||
| 键 | 格式 |
|
流量统计和连接管理会降低性能。
|
||||||
|--------------|--------------------------|
|
|
||||||
| `cache_file` | [缓存文件](./cache-file) |
|
### Clash API 字段
|
||||||
| `clash_api` | [Clash API](./clash-api) |
|
|
||||||
| `v2ray_api` | [V2Ray API](./v2ray-api) |
|
!!! error ""
|
||||||
|
|
||||||
|
默认安装不包含 Clash API,参阅 [安装](/zh/#_2)。
|
||||||
|
|
||||||
|
#### external_controller
|
||||||
|
|
||||||
|
RESTful web API 监听地址。如果为空,则禁用 Clash API。
|
||||||
|
|
||||||
|
#### external_ui
|
||||||
|
|
||||||
|
到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。
|
||||||
|
|
||||||
|
#### external_ui_download_url
|
||||||
|
|
||||||
|
静态网页资源的 ZIP 下载 URL,如果指定的 `external_ui` 目录为空,将使用。
|
||||||
|
|
||||||
|
默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`。
|
||||||
|
|
||||||
|
#### external_ui_download_detour
|
||||||
|
|
||||||
|
用于下载静态网页资源的出站的标签。
|
||||||
|
|
||||||
|
如果为空,将使用默认出站。
|
||||||
|
|
||||||
|
#### secret
|
||||||
|
|
||||||
|
RESTful API 的密钥(可选)
|
||||||
|
通过指定 HTTP 标头 `Authorization: Bearer ${secret}` 进行身份验证
|
||||||
|
如果 RESTful API 正在监听 0.0.0.0,请始终设置一个密钥。
|
||||||
|
|
||||||
|
#### default_mode
|
||||||
|
|
||||||
|
Clash 中的默认模式,默认使用 `Rule`。
|
||||||
|
|
||||||
|
此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。
|
||||||
|
|
||||||
|
#### store_mode
|
||||||
|
|
||||||
|
将 Clash 模式存储在缓存文件中。
|
||||||
|
|
||||||
|
#### store_selected
|
||||||
|
|
||||||
|
!!! note ""
|
||||||
|
|
||||||
|
必须为目标出站设置标签。
|
||||||
|
|
||||||
|
将 `Selector` 中出站的选定的目标出站存储在缓存文件中。
|
||||||
|
|
||||||
|
#### store_fakeip
|
||||||
|
|
||||||
|
将 fakeip 存储在缓存文件中。
|
||||||
|
|
||||||
|
#### cache_file
|
||||||
|
|
||||||
|
缓存文件路径,默认使用`cache.db`。
|
||||||
|
|
||||||
|
#### cache_id
|
||||||
|
|
||||||
|
缓存 ID。
|
||||||
|
|
||||||
|
如果不为空,`store_selected` 将会使用以此为键的独立存储。
|
||||||
|
|
||||||
|
### V2Ray API 字段
|
||||||
|
|
||||||
|
!!! error ""
|
||||||
|
|
||||||
|
默认安装不包含 V2Ray API,参阅 [安装](/zh/#_2)。
|
||||||
|
|
||||||
|
#### listen
|
||||||
|
|
||||||
|
gRPC API 监听地址。如果为空,则禁用 V2Ray API。
|
||||||
|
|
||||||
|
#### stats
|
||||||
|
|
||||||
|
流量统计服务设置。
|
||||||
|
|
||||||
|
#### stats.enabled
|
||||||
|
|
||||||
|
启用统计服务。
|
||||||
|
|
||||||
|
#### stats.inbounds
|
||||||
|
|
||||||
|
统计流量的入站列表。
|
||||||
|
|
||||||
|
#### stats.outbounds
|
||||||
|
|
||||||
|
统计流量的出站列表。
|
||||||
|
|
||||||
|
#### stats.users
|
||||||
|
|
||||||
|
统计流量的用户列表。
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
!!! quote ""
|
|
||||||
|
|
||||||
V2Ray API is not included by default, see [Installation](/installation/build-from-source/#build-tags).
|
|
||||||
|
|
||||||
### Structure
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"listen": "127.0.0.1:8080",
|
|
||||||
"stats": {
|
|
||||||
"enabled": true,
|
|
||||||
"inbounds": [
|
|
||||||
"socks-in"
|
|
||||||
],
|
|
||||||
"outbounds": [
|
|
||||||
"proxy",
|
|
||||||
"direct"
|
|
||||||
],
|
|
||||||
"users": [
|
|
||||||
"sekai"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Fields
|
|
||||||
|
|
||||||
#### listen
|
|
||||||
|
|
||||||
gRPC API listening address. V2Ray API will be disabled if empty.
|
|
||||||
|
|
||||||
#### stats
|
|
||||||
|
|
||||||
Traffic statistics service settings.
|
|
||||||
|
|
||||||
#### stats.enabled
|
|
||||||
|
|
||||||
Enable statistics service.
|
|
||||||
|
|
||||||
#### stats.inbounds
|
|
||||||
|
|
||||||
Inbound list to count traffic.
|
|
||||||
|
|
||||||
#### stats.outbounds
|
|
||||||
|
|
||||||
Outbound list to count traffic.
|
|
||||||
|
|
||||||
#### stats.users
|
|
||||||
|
|
||||||
User list to count traffic.
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
!!! quote ""
|
|
||||||
|
|
||||||
默认安装不包含 V2Ray API,参阅 [安装](/zh/installation/build-from-source/#_5)。
|
|
||||||
|
|
||||||
### 结构
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"listen": "127.0.0.1:8080",
|
|
||||||
"stats": {
|
|
||||||
"enabled": true,
|
|
||||||
"inbounds": [
|
|
||||||
"socks-in"
|
|
||||||
],
|
|
||||||
"outbounds": [
|
|
||||||
"proxy",
|
|
||||||
"direct"
|
|
||||||
],
|
|
||||||
"users": [
|
|
||||||
"sekai"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 字段
|
|
||||||
|
|
||||||
#### listen
|
|
||||||
|
|
||||||
gRPC API 监听地址。如果为空,则禁用 V2Ray API。
|
|
||||||
|
|
||||||
#### stats
|
|
||||||
|
|
||||||
流量统计服务设置。
|
|
||||||
|
|
||||||
#### stats.enabled
|
|
||||||
|
|
||||||
启用统计服务。
|
|
||||||
|
|
||||||
#### stats.inbounds
|
|
||||||
|
|
||||||
统计流量的入站列表。
|
|
||||||
|
|
||||||
#### stats.outbounds
|
|
||||||
|
|
||||||
统计流量的出站列表。
|
|
||||||
|
|
||||||
#### stats.users
|
|
||||||
|
|
||||||
统计流量的用户列表。
|
|
||||||
@@ -36,7 +36,7 @@ No authentication required if empty.
|
|||||||
|
|
||||||
#### set_system_proxy
|
#### set_system_proxy
|
||||||
|
|
||||||
!!! quote ""
|
!!! error ""
|
||||||
|
|
||||||
Only supported on Linux, Android, Windows, and macOS.
|
Only supported on Linux, Android, Windows, and macOS.
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ HTTP 用户
|
|||||||
|
|
||||||
#### set_system_proxy
|
#### set_system_proxy
|
||||||
|
|
||||||
!!! quote ""
|
!!! error ""
|
||||||
|
|
||||||
仅支持 Linux、Android、Windows 和 macOS。
|
仅支持 Linux、Android、Windows 和 macOS。
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! warning ""
|
||||||
|
|
||||||
|
QUIC, which is required by hysteria is not included by default, see [Installation](/#installation).
|
||||||
|
|
||||||
### Listen Fields
|
### Listen Fields
|
||||||
|
|
||||||
See [Listen Fields](/configuration/shared/listen) for details.
|
See [Listen Fields](/configuration/shared/listen) for details.
|
||||||
|
|||||||
@@ -29,6 +29,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! warning ""
|
||||||
|
|
||||||
|
默认安装不包含被 Hysteria 依赖的 QUIC,参阅 [安装](/zh/#_2)。
|
||||||
|
|
||||||
### 监听字段
|
### 监听字段
|
||||||
|
|
||||||
参阅 [监听字段](/zh/configuration/shared/listen/)。
|
参阅 [监听字段](/zh/configuration/shared/listen/)。
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
{
|
{
|
||||||
"type": "hysteria2",
|
"type": "hysteria2",
|
||||||
"tag": "hy2-in",
|
"tag": "hy2-in",
|
||||||
...
|
|
||||||
// Listen Fields
|
... // Listen Fields
|
||||||
|
|
||||||
"up_mbps": 100,
|
"up_mbps": 100,
|
||||||
"down_mbps": 100,
|
"down_mbps": 100,
|
||||||
@@ -26,12 +26,9 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! warning "Difference from official Hysteria2"
|
!!! warning ""
|
||||||
|
|
||||||
The official program supports an authentication method called **userpass**,
|
QUIC, which is required by Hysteria2 is not included by default, see [Installation](/#installation).
|
||||||
which essentially uses a combination of `<username>:<password>` as the actual password,
|
|
||||||
while sing-box does not provide this alias.
|
|
||||||
To use sing-box with the official program, you need to fill in that combination as the actual password.
|
|
||||||
|
|
||||||
### Listen Fields
|
### Listen Fields
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
{
|
{
|
||||||
"type": "hysteria2",
|
"type": "hysteria2",
|
||||||
"tag": "hy2-in",
|
"tag": "hy2-in",
|
||||||
...
|
|
||||||
// 监听字段
|
... // 监听字段
|
||||||
|
|
||||||
"up_mbps": 100,
|
"up_mbps": 100,
|
||||||
"down_mbps": 100,
|
"down_mbps": 100,
|
||||||
@@ -26,11 +26,9 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! warning "与官方 Hysteria2 的区别"
|
!!! warning ""
|
||||||
|
|
||||||
官方程序支持一种名为 **userpass** 的验证方式,
|
默认安装不包含被 Hysteria2 依赖的 QUIC,参阅 [安装](/zh/#_2)。
|
||||||
本质上上是将用户名与密码的组合 `<username>:<password>` 作为实际上的密码,而 sing-box 不提供此别名。
|
|
||||||
要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。
|
|
||||||
|
|
||||||
### 监听字段
|
### 监听字段
|
||||||
|
|
||||||
@@ -64,7 +62,7 @@ Hysteria 用户
|
|||||||
|
|
||||||
#### ignore_client_bandwidth
|
#### ignore_client_bandwidth
|
||||||
|
|
||||||
命令客户端使用 BBR 拥塞控制算法而不是 Hysteria CC。
|
命令客户端使用 BBR 流量控制算法而不是 Hysteria CC。
|
||||||
|
|
||||||
与 `up_mbps` 和 `down_mbps` 冲突。
|
与 `up_mbps` 和 `down_mbps` 冲突。
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ No authentication required if empty.
|
|||||||
|
|
||||||
#### set_system_proxy
|
#### set_system_proxy
|
||||||
|
|
||||||
!!! quote ""
|
!!! error ""
|
||||||
|
|
||||||
Only supported on Linux, Android, Windows, and macOS.
|
Only supported on Linux, Android, Windows, and macOS.
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ SOCKS 和 HTTP 用户
|
|||||||
|
|
||||||
#### set_system_proxy
|
#### set_system_proxy
|
||||||
|
|
||||||
!!! quote ""
|
!!! error ""
|
||||||
|
|
||||||
仅支持 Linux、Android、Windows 和 macOS。
|
仅支持 Linux、Android、Windows 和 macOS。
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! warning ""
|
||||||
|
|
||||||
|
HTTP3 transport is not included by default, see [Installation](/#installation).
|
||||||
|
|
||||||
### Listen Fields
|
### Listen Fields
|
||||||
|
|
||||||
See [Listen Fields](/configuration/shared/listen) for details.
|
See [Listen Fields](/configuration/shared/listen) for details.
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! warning ""
|
||||||
|
|
||||||
|
默认安装不包含 HTTP3 传输层, 参阅 [安装](/zh/#_2)。
|
||||||
|
|
||||||
### 监听字段
|
### 监听字段
|
||||||
|
|
||||||
参阅 [监听字段](/zh/configuration/shared/listen/)。
|
参阅 [监听字段](/zh/configuration/shared/listen/)。
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
!!! quote ""
|
!!! error ""
|
||||||
|
|
||||||
Only supported on Linux and macOS.
|
Only supported on Linux and macOS.
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user