Compare commits

..

11 Commits

Author SHA1 Message Date
世界
5d85fa16ce documentation: Update changelog 2023-08-09 10:38:42 +08:00
世界
3442c7a692 Add TCP MultiPath support 2023-08-09 10:38:40 +08:00
世界
c485c11a0c platform: Enable Clash API support by default 2023-08-09 10:38:39 +08:00
世界
3e43a676a6 Save fakeip metadata immediately 2023-08-09 10:38:39 +08:00
世界
5acc40d72f Pause recurring tasks when no network 2023-08-09 10:38:38 +08:00
世界
2d0aa96cbe documentation: Add TUIC 2023-08-09 10:38:38 +08:00
世界
a7e16ee502 Add TUIC protocol 2023-08-09 10:38:38 +08:00
世界
56974ff465 Update quic-go 2023-08-09 10:38:29 +08:00
世界
6de6804212 Update quic-go to v0.37.3 2023-08-08 20:08:25 +08:00
世界
3e6d34adea Update dependencies 2023-08-08 20:08:16 +08:00
世界
6ec8ff79b5 Fix network monitor 2023-08-07 22:37:17 +08:00
433 changed files with 11948 additions and 11024 deletions

View File

@@ -1,79 +1,70 @@
name: Bug report
description: "Report sing-box bug"
name: Bug Report
description: "Create a report to help us improve."
body:
- type: dropdown
- type: checkboxes
id: terms
attributes:
label: Operating system
description: Operating system type
label: Welcome
options:
- iOS
- macOS
- Apple tvOS
- Android
- Windows
- Linux
- Others
validations:
required: true
- type: input
attributes:
label: System version
description: Please provide the operating system version
validations:
required: true
- type: dropdown
attributes:
label: Installation type
description: Please provide the sing-box installation type
options:
- Original sing-box Command Line
- sing-box for iOS Graphical Client
- sing-box for macOS Graphical Client
- sing-box for Apple tvOS Graphical Client
- sing-box for Android Graphical Client
- Third-party graphical clients that advertise themselves as using sing-box (Windows)
- Third-party graphical clients that advertise themselves as using sing-box (Android)
- Others
validations:
required: true
- type: input
attributes:
description: Graphical client version
label: If you are using a graphical client, please provide the version of the client.
- label: Yes, I'm using the latest major release. Only such installations are supported.
required: true
- label: Yes, I'm using the latest Golang release. Only such installations are supported.
required: true
- label: Yes, I've searched similar issues on GitHub and didn't find any.
required: true
- label: Yes, I've included all information below (version, **FULL** config, **FULL** log, etc).
required: true
- type: textarea
id: problem
attributes:
label: Version
description: If you are using the original command line program, please provide the output of the `sing-box version` command.
label: Description of the problem
placeholder: Your problem description
validations:
required: true
- type: textarea
id: version
attributes:
label: Version of sing-box
value: |-
<details>
```console
# Replace this line with the output
$ sing-box version
# Paste output here
```
</details>
- type: textarea
attributes:
label: Description
description: Please provide a detailed description of the error.
validations:
required: true
- type: textarea
id: config
attributes:
label: Reproduction
description: Please provide the steps to reproduce the error, including the configuration files and procedures that can locally (not dependent on the remote server) reproduce the error using the original command line program of sing-box.
validations:
required: true
- type: textarea
attributes:
label: Logs
description: |-
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 the Android client, please check the `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` file for crash logs.
label: Server and client configuration file
value: |-
<details>
```console
# Replace this line with logs
# paste json here
```
</details>
</details>
validations:
required: true
- type: textarea
id: log
attributes:
label: Server and client log file
value: |-
<details>
```console
# paste log here
```
</details>
validations:
required: true

View File

@@ -1,79 +0,0 @@
name: 错误反馈
description: "提交 sing-box 漏洞"
body:
- type: dropdown
attributes:
label: 操作系统
description: 请提供操作系统类型
options:
- iOS
- macOS
- Apple tvOS
- Android
- Windows
- Linux
- 其他
validations:
required: true
- type: input
attributes:
label: 系统版本
description: 请提供操作系统版本
validations:
required: true
- type: dropdown
attributes:
label: 安装类型
description: 请提供该 sing-box 安装类型
options:
- sing-box 原始命令行程序
- sing-box for iOS 图形客户端程序
- sing-box for macOS 图形客户端程序
- sing-box for Apple tvOS 图形客户端程序
- sing-box for Android 图形客户端程序
- 宣传使用 sing-box 的第三方图形客户端程序 (Windows)
- 宣传使用 sing-box 的第三方图形客户端程序 (Android)
- 其他
validations:
required: true
- type: input
attributes:
description: 图形客户端版本
label: 如果您使用图形客户端程序,请提供该程序版本。
- type: textarea
attributes:
label: 版本
description: 如果您使用原始命令行程序,请提供 `sing-box version` 命令的输出。
value: |-
<details>
```console
# 使用输出内容覆盖此行
```
</details>
- type: textarea
attributes:
label: 描述
description: 请提供错误的详细描述。
validations:
required: true
- type: textarea
attributes:
label: 重现方式
description: 请提供重现错误的步骤,必须包括可以在本地(不依赖与远程服务器)使用 sing-box 原始命令行程序重现错误的配置文件与流程。
validations:
required: true
- type: textarea
attributes:
label: 日志
description: |-
如果您遭遇图形界面应用程序崩溃,请提供崩溃日志。
对于 Apple 平台图形客户端程序,请检查 `Settings - View Service Log` 以导出崩溃日志。
对于 Android 图形客户端程序,请检查 `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` 文件以导出崩溃日志。
value: |-
<details>
```console
# 使用日志内容覆盖此行
```
</details>

View File

@@ -3,7 +3,6 @@ name: Debug build
on:
push:
branches:
- stable-next
- main-next
- dev-next
paths-ignore:
@@ -12,7 +11,6 @@ on:
- '!.github/workflows/debug.yml'
pull_request:
branches:
- stable-next
- main-next
- dev-next
@@ -22,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get latest go version
@@ -50,7 +48,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Go
@@ -64,27 +62,7 @@ jobs:
~/go/pkg/mod
key: go118-${{ hashFiles('**/go.sum') }}
- name: Run Test
run: make ci_build_go118
build_go120:
name: Debug build (Go 1.20)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: 1.20.7
- name: Cache go module
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
key: go118-${{ hashFiles('**/go.sum') }}
- name: Run Test
run: make ci_build
run: make
cross:
strategy:
matrix:
@@ -201,7 +179,7 @@ jobs:
TAGS: with_clash_api,with_quic
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get latest go version

View File

@@ -9,20 +9,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
uses: actions/checkout@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v2
- name: Setup QEMU for Docker Buildx
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker metadata
id: metadata
uses: docker/metadata-action@v5
uses: docker/metadata-action@v4
with:
images: ghcr.io/sagernet/sing-box
- name: Get tag to build
@@ -35,12 +35,10 @@ jobs:
echo "versioned=ghcr.io/sagernet/sing-box:${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
fi
- name: Build and release Docker images
uses: docker/build-push-action@v5
uses: docker/build-push-action@v4
with:
platforms: linux/386,linux/amd64,linux/arm64,linux/s390x
target: dist
build-args: |
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
tags: |
${{ steps.tag.outputs.latest }}
${{ steps.tag.outputs.versioned }}

View File

@@ -3,7 +3,6 @@ name: Lint
on:
push:
branches:
- stable-next
- main-next
- dev-next
paths-ignore:
@@ -12,7 +11,6 @@ on:
- '!.github/workflows/lint.yml'
pull_request:
branches:
- stable-next
- main-next
- dev-next
@@ -22,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get latest go version
@@ -36,6 +34,4 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
args: --timeout=30m
install-mode: binary
version: latest

20
.github/workflows/mkdocs.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Generate Documents
on:
push:
branches:
- dev
paths:
- docs/**
- .github/workflows/mkdocs.yml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.x
- run: |
pip install mkdocs-material=="9.*" mkdocs-static-i18n=="0.53"
- run: |
mkdocs gh-deploy -m "{sha}" --force --ignore-version --no-history

View File

@@ -8,7 +8,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v8
- uses: actions/stale@v7
with:
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
days-before-stale: 60

View File

@@ -16,15 +16,12 @@ builds:
- with_quic
- with_dhcp
- with_wireguard
- with_ech
- with_utls
- with_reality_server
- with_acme
- with_clash_api
env:
- CGO_ENABLED=0
targets:
- linux_386
- linux_amd64_v1
- linux_amd64_v3
- linux_arm64
@@ -38,36 +35,6 @@ builds:
- darwin_amd64_v3
- darwin_arm64
mod_timestamp: '{{ .CommitTimestamp }}'
- id: legacy
main: ./cmd/sing-box
flags:
- -v
- -trimpath
asmflags:
- all=-trimpath={{.Env.GOPATH}}
gcflags:
- all=-trimpath={{.Env.GOPATH}}
ldflags:
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_ech
- with_utls
- with_reality_server
- with_acme
- with_clash_api
env:
- CGO_ENABLED=0
- GOROOT=/nix/store/kg6i737jjqs923jcijnm003h68c1dghj-go-1.20.11/share/go
gobinary: /nix/store/kg6i737jjqs923jcijnm003h68c1dghj-go-1.20.11/bin/go
targets:
- windows_amd64_v1
- windows_386
- darwin_amd64_v1
mod_timestamp: '{{ .CommitTimestamp }}'
- id: android
main: ./cmd/sing-box
flags:
@@ -84,10 +51,7 @@ builds:
- with_quic
- with_dhcp
- with_wireguard
- with_ech
- with_utls
- with_reality_server
- with_acme
- with_clash_api
env:
- CGO_ENABLED=1
@@ -124,9 +88,6 @@ snapshot:
name_template: "{{ .Version }}.{{ .ShortCommit }}"
archives:
- id: archive
builds:
- main
- android
format: tar.gz
format_overrides:
- goos: windows
@@ -135,17 +96,6 @@ archives:
files:
- LICENSE
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
- id: archive-legacy
builds:
- legacy
format: tar.gz
format_overrides:
- goos: windows
format: zip
wrap_in_directory: true
files:
- LICENSE
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-legacy'
nfpms:
- id: package
package_name: sing-box
@@ -158,7 +108,6 @@ nfpms:
formats:
- deb
- rpm
- archlinux
priority: extra
contents:
- src: release/config/config.json

View File

@@ -1,27 +1,23 @@
FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS builder
FROM golang:1.20-alpine AS builder
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
COPY . /go/src/github.com/sagernet/sing-box
WORKDIR /go/src/github.com/sagernet/sing-box
ARG TARGETOS TARGETARCH
ARG GOPROXY=""
ENV GOPROXY ${GOPROXY}
ENV CGO_ENABLED=0
ENV GOOS=$TARGETOS
ENV GOARCH=$TARGETARCH
RUN set -ex \
&& apk add git build-base \
&& export COMMIT=$(git rev-parse --short HEAD) \
&& export VERSION=$(go run ./cmd/internal/read_tag) \
&& go build -v -trimpath -tags \
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \
&& go build -v -trimpath -tags with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_clash_api,with_acme \
-o /go/bin/sing-box \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
./cmd/sing-box
FROM --platform=$TARGETPLATFORM alpine AS dist
FROM alpine AS dist
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
RUN set -ex \
&& apk upgrade \
&& apk add bash tzdata ca-certificates \
&& rm -rf /var/cache/apk/*
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
ENTRYPOINT ["sing-box"]
ENTRYPOINT ["sing-box"]

136
Makefile
View File

@@ -1,34 +1,23 @@
NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD)
TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_utls,with_reality_server,with_clash_api
TAGS_GO120 = with_quic,with_ech
TAGS ?= $(TAGS_GO118),$(TAGS_GO120)
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_clash_api
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server,with_shadowsocksr
GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH)
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag)
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
MAIN_PARAMS = $(PARAMS) -tags $(TAGS)
PARAMS = -v -trimpath -tags "$(TAGS)" -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
MAIN = ./cmd/sing-box
PREFIX ?= $(shell go env GOPATH)
.PHONY: test release docs
.PHONY: test release
build:
go build $(MAIN_PARAMS) $(MAIN)
ci_build_go118:
go build $(PARAMS) $(MAIN)
go build $(PARAMS) -tags "$(TAGS_GO118)" $(MAIN)
ci_build:
go build $(PARAMS) $(MAIN)
go build $(MAIN_PARAMS) $(MAIN)
install:
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
go build -o $(PREFIX)/bin/$(NAME) $(PARAMS) $(MAIN)
fmt:
@gofumpt -l -w .
@@ -58,103 +47,24 @@ proto_install:
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
snapshot:
go run ./cmd/internal/build goreleaser release --clean --snapshot || exit 1
mkdir dist/release
mv dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/release
ghr --delete --draft --prerelease -p 1 nightly dist/release
rm -r dist
release:
go run ./cmd/internal/build goreleaser release --clean --skip-publish || exit 1
mkdir dist/release
mv dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/*.pkg.tar.zst dist/release
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release
rm -r dist/release
mv dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/release
ghr --delete --draft --prerelease -p 3 $(shell git describe --tags) dist/release
rm -r dist
release_install:
go install -v github.com/goreleaser/goreleaser@latest
go install -v github.com/tcnksm/ghr@latest
update_android_version:
go run ./cmd/internal/update_android_version
build_android:
cd ../sing-box-for-android && ./gradlew :app:assemblePlayRelease && ./gradlew --stop
upload_android:
mkdir -p dist/release_android
cp ../sing-box-for-android/app/build/outputs/apk/play/release/*.apk dist/release_android
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release_android
rm -rf dist/release_android
release_android: lib_android update_android_version build_android upload_android
publish_android:
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle
publish_android_appcenter:
cd ../sing-box-for-android && ./gradlew :app:appCenterAssembleAndUploadPlayRelease
build_ios:
cd ../sing-box-for-apple && \
rm -rf build/SFI.xcarchive && \
xcodebuild archive -scheme SFI -configuration Release -archivePath build/SFI.xcarchive
upload_ios_app_store:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
release_ios: build_ios upload_ios_app_store
build_macos:
cd ../sing-box-for-apple && \
rm -rf build/SFM.xcarchive && \
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive
upload_macos_app_store:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath build/SFM.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
release_macos: build_macos upload_macos_app_store
build_macos_independent:
cd ../sing-box-for-apple && \
rm -rf build/SFT.System.xcarchive && \
xcodebuild archive -scheme SFM.System -configuration Release -archivePath build/SFM.System.xcarchive
notarize_macos_independent:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath "build/SFM.System.xcarchive" -exportOptionsPlist SFM.System/Upload.plist -allowProvisioningUpdates
wait_notarize_macos_independent:
sleep 60
export_macos_independent:
rm -rf dist/SFM
mkdir -p dist/SFM
cd ../sing-box-for-apple && \
xcodebuild -exportNotarizedApp -archivePath build/SFM.System.xcarchive -exportPath "../sing-box/dist/SFM"
upload_macos_independent:
cd dist/SFM && \
rm -f *.zip && \
zip -ry "SFM-${VERSION}-universal.zip" SFM.app && \
ghr --replace --draft --prerelease "v${VERSION}" *.zip
release_macos_independent: build_macos_independent notarize_macos_independent wait_notarize_macos_independent export_macos_independent upload_macos_independent
build_tvos:
cd ../sing-box-for-apple && \
rm -rf build/SFT.xcarchive && \
xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive
upload_tvos_app_store:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
release_tvos: build_tvos upload_tvos_app_store
update_apple_version:
go run ./cmd/internal/update_apple_version
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_independent
release_apple_beta: update_apple_version release_ios release_macos release_tvos
test:
@go test -v ./... && \
cd test && \
@@ -167,10 +77,10 @@ test_stdio:
go mod tidy && \
go test -v -tags "$(TAGS_TEST),force_stdio" .
lib_android:
android:
go run ./cmd/internal/build_libbox -target android
lib_ios:
ios:
go run ./cmd/internal/build_libbox -target ios
lib:
@@ -179,17 +89,9 @@ lib:
lib_install:
go get -v -d
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
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20230728014906-3de089147f59
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20230728014906-3de089147f59
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:
rm -rf bin dist sing-box
rm -f $(shell go env GOPATH)/sing-box

View File

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

View File

@@ -12,7 +12,6 @@ type ClashServer interface {
Service
PreStarter
Mode() string
ModeList() []string
StoreSelected() bool
StoreFakeIP() bool
CacheFile() ClashCacheFile
@@ -22,12 +21,8 @@ type ClashServer interface {
}
type ClashCacheFile interface {
LoadMode() string
StoreMode(mode string) error
LoadSelected(group string) string
StoreSelected(group string, selected string) error
LoadGroupExpand(group string) (isExpand bool, loaded bool)
StoreGroupExpand(group string, expand bool) error
FakeIPStorage
}

View File

@@ -75,11 +75,3 @@ func AppendContext(ctx context.Context) (context.Context, *InboundContext) {
metadata = new(InboundContext)
return WithContext(ctx, metadata), metadata
}
func ExtendContext(ctx context.Context) (context.Context, *InboundContext) {
var newMetadata InboundContext
if metadata := ContextFrom(ctx); metadata != nil {
newMetadata = *metadata
}
return WithContext(ctx, &newMetadata), &newMetadata
}

View File

@@ -2,13 +2,14 @@ package adapter
import (
"context"
"net"
"net/netip"
"github.com/sagernet/sing-box/common/geoip"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
"github.com/sagernet/sing/service"
N "github.com/sagernet/sing/common/network"
mdns "github.com/miekg/dns"
)
@@ -22,7 +23,8 @@ type Router interface {
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
LoadGeosite(code string) (Rule, error)
@@ -30,7 +32,6 @@ type Router interface {
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
ClearDNSCache()
InterfaceFinder() control.InterfaceFinder
UpdateInterfaces() error
@@ -41,9 +42,10 @@ type Router interface {
NetworkMonitor() tun.NetworkUpdateMonitor
InterfaceMonitor() tun.DefaultInterfaceMonitor
PackageManager() tun.PackageManager
WIFIState() WIFIState
Rules() []Rule
TimeService
ClashServer() ClashServer
SetClashServer(server ClashServer)
@@ -53,12 +55,18 @@ type Router interface {
ResetNetwork() error
}
type routerContextKey struct{}
func ContextWithRouter(ctx context.Context, router Router) context.Context {
return service.ContextWith(ctx, router)
return context.WithValue(ctx, (*routerContextKey)(nil), router)
}
func RouterFromContext(ctx context.Context) Router {
return service.FromContext[Router](ctx)
metadata := ctx.Value((*routerContextKey)(nil))
if metadata == nil {
return nil
}
return metadata.(Router)
}
type Rule interface {
@@ -79,8 +87,3 @@ type DNSRule interface {
type InterfaceUpdateListener interface {
InterfaceUpdated()
}
type WIFIState struct {
SSID string
BSSID string
}

View File

@@ -5,6 +5,7 @@ import (
"net"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
@@ -18,6 +19,7 @@ type V2RayServerTransport interface {
type V2RayServerTransportHandler interface {
N.TCPConnectionHandler
E.Handler
FallbackConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error
}
type V2RayClientTransport interface {

13
box.go
View File

@@ -19,7 +19,6 @@ import (
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/pause"
)
@@ -41,7 +40,6 @@ type Options struct {
option.Options
Context context.Context
PlatformInterface platform.Interface
PlatformLogWriter log.PlatformWriter
}
func New(options Options) (*Box, error) {
@@ -49,14 +47,13 @@ func New(options Options) (*Box, error) {
if ctx == nil {
ctx = context.Background()
}
ctx = service.ContextWithDefaultRegistry(ctx)
ctx = pause.ContextWithDefaultManager(ctx)
createdAt := time.Now()
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
var needClashAPI bool
var needV2RayAPI bool
if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil {
if experimentalOptions.ClashAPI != nil || options.PlatformInterface != nil {
needClashAPI = true
}
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
@@ -72,7 +69,7 @@ func New(options Options) (*Box, error) {
Observable: needClashAPI,
DefaultWriter: defaultLogWriter,
BaseTime: createdAt,
PlatformWriter: options.PlatformLogWriter,
PlatformWriter: options.PlatformInterface,
})
if err != nil {
return nil, E.Cause(err, "create log factory")
@@ -148,9 +145,7 @@ func New(options Options) (*Box, error) {
preServices := make(map[string]adapter.Service)
postServices := make(map[string]adapter.Service)
if needClashAPI {
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
clashServer, err := experimental.NewClashServer(ctx, router, logFactory.(log.ObservableFactory), clashAPIOptions)
clashServer, err := experimental.NewClashServer(ctx, router, logFactory.(log.ObservableFactory), common.PtrValueOrDefault(options.Experimental.ClashAPI))
if err != nil {
return nil, E.Cause(err, "create clash api server")
}
@@ -158,7 +153,7 @@ func New(options Options) (*Box, error) {
preServices["clash api"] = clashServer
}
if needV2RayAPI {
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(options.Experimental.V2RayAPI))
if err != nil {
return nil, E.Cause(err, "create v2ray api server")
}

View File

@@ -69,7 +69,7 @@ func (s *Box) startOutbounds() error {
}
problemOutbound := outbounds[problemOutboundTag]
if problemOutbound == nil {
return E.New("dependency[", problemOutboundTag, "] not found for outbound[", outboundTags[oCurrent], "]")
return E.New("dependency[", problemOutbound, "] not found for outbound[", outboundTags[oCurrent], "]")
}
return lintOutbound(append(oTree, problemOutboundTag), problemOutbound)
}

View File

@@ -12,7 +12,7 @@ import (
func main() {
build_shared.FindSDK()
if os.Getenv("GOPATH") == "" {
if os.Getenv("build.Default.GOPATH") == "" {
os.Setenv("GOPATH", build.Default.GOPATH)
}

View File

@@ -54,7 +54,7 @@ func init() {
sharedFlags = append(sharedFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
debugFlags = append(debugFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api")
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api")
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
debugTags = append(debugTags, "debug")
}

View File

@@ -2,7 +2,6 @@ package build_shared
import (
"github.com/sagernet/sing-box/common/badversion"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/shell"
)
@@ -17,17 +16,8 @@ func ReadTag() (string, error) {
}
shortCommit, _ := shell.Exec("git", "rev-parse", "--short", "HEAD").ReadOutput()
version := badversion.Parse(currentTagRev[1:])
if version.PreReleaseIdentifier == "" {
version.Patch++
}
return version.String() + "-" + shortCommit, nil
}
func ReadTagVersion() (badversion.Version, error) {
currentTag := common.Must1(shell.Exec("git", "describe", "--tags").ReadOutput())
currentTagRev := common.Must1(shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput())
version := badversion.Parse(currentTagRev[1:])
if currentTagRev != currentTag {
if version.PreReleaseIdentifier == "" {
version.Patch++
}
}
return version, nil
}

View File

@@ -1,51 +0,0 @@
package main
import (
"os"
"path/filepath"
"strconv"
"strings"
"github.com/sagernet/sing-box/cmd/internal/build_shared"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
)
func main() {
newVersion := common.Must1(build_shared.ReadTagVersion())
androidPath, err := filepath.Abs("../sing-box-for-android")
if err != nil {
log.Fatal(err)
}
common.Must(os.Chdir(androidPath))
localProps := common.Must1(os.ReadFile("local.properties"))
var propsList [][]string
for _, propLine := range strings.Split(string(localProps), "\n") {
propsList = append(propsList, strings.Split(propLine, "="))
}
for _, propPair := range propsList {
if propPair[0] == "VERSION_NAME" {
if propPair[1] == newVersion.String() {
log.Info("version not changed")
return
}
propPair[1] = newVersion.String()
log.Info("updated version to ", newVersion.String())
}
}
for _, propPair := range propsList {
switch propPair[0] {
case "VERSION_CODE":
versionCode := common.Must1(strconv.ParseInt(propPair[1], 10, 64))
propPair[1] = strconv.Itoa(int(versionCode + 1))
log.Info("updated version code to ", propPair[1])
case "RELEASE_NOTES":
propPair[1] = "sing-box " + newVersion.String()
}
}
var newProps []string
for _, propPair := range propsList {
newProps = append(newProps, strings.Join(propPair, "="))
}
common.Must(os.WriteFile("local.properties", []byte(strings.Join(newProps, "\n")), 0o644))
}

View File

@@ -1,131 +0,0 @@
package main
import (
"os"
"path/filepath"
"regexp"
"strings"
"github.com/sagernet/sing-box/cmd/internal/build_shared"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
"howett.net/plist"
)
func main() {
newVersion := common.Must1(build_shared.ReadTagVersion())
applePath, err := filepath.Abs("../sing-box-for-apple")
if err != nil {
log.Fatal(err)
}
common.Must(os.Chdir(applePath))
projectFile := common.Must1(os.Open("sing-box.xcodeproj/project.pbxproj"))
var project map[string]any
decoder := plist.NewDecoder(projectFile)
common.Must(decoder.Decode(&project))
objectsMap := project["objects"].(map[string]any)
projectContent := string(common.Must1(os.ReadFile("sing-box.xcodeproj/project.pbxproj")))
newContent, updated0 := findAndReplace(objectsMap, projectContent, []string{"io.nekohasekai.sfa"}, newVersion.VersionString())
newContent, updated1 := findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfa.independent", "io.nekohasekai.sfa.system"}, newVersion.String())
if updated0 || updated1 {
log.Info("updated version to ", newVersion.VersionString(), " (", newVersion.String(), ")")
}
var updated2 bool
if macProjectVersion := os.Getenv("MACOS_PROJECT_VERSION"); macProjectVersion != "" {
newContent, updated2 = findAndReplaceProjectVersion(objectsMap, newContent, []string{"SFM"}, macProjectVersion)
if updated2 {
log.Info("updated macos project version to ", macProjectVersion)
}
}
if updated0 || updated1 || updated2 {
common.Must(os.WriteFile("sing-box.xcodeproj/project.pbxproj", []byte(newContent), 0o644))
}
}
func findAndReplace(objectsMap map[string]any, projectContent string, bundleIDList []string, newVersion string) (string, bool) {
objectKeyList := findObjectKey(objectsMap, bundleIDList)
var updated bool
for _, objectKey := range objectKeyList {
matchRegexp := common.Must1(regexp.Compile(objectKey + ".*= \\{"))
indexes := matchRegexp.FindStringIndex(projectContent)
if len(indexes) < 2 {
println(projectContent)
log.Fatal("failed to find object key ", objectKey, ": ", strings.Index(projectContent, objectKey))
}
indexStart := indexes[1]
indexEnd := indexStart + strings.Index(projectContent[indexStart:], "}")
versionStart := indexStart + strings.Index(projectContent[indexStart:indexEnd], "MARKETING_VERSION = ") + 20
versionEnd := versionStart + strings.Index(projectContent[versionStart:indexEnd], ";")
version := projectContent[versionStart:versionEnd]
if version == newVersion {
continue
}
updated = true
projectContent = projectContent[:versionStart] + newVersion + projectContent[versionEnd:]
}
return projectContent, updated
}
func findAndReplaceProjectVersion(objectsMap map[string]any, projectContent string, directoryList []string, newVersion string) (string, bool) {
objectKeyList := findObjectKeyByDirectory(objectsMap, directoryList)
var updated bool
for _, objectKey := range objectKeyList {
matchRegexp := common.Must1(regexp.Compile(objectKey + ".*= \\{"))
indexes := matchRegexp.FindStringIndex(projectContent)
if len(indexes) < 2 {
println(projectContent)
log.Fatal("failed to find object key ", objectKey, ": ", strings.Index(projectContent, objectKey))
}
indexStart := indexes[1]
indexEnd := indexStart + strings.Index(projectContent[indexStart:], "}")
versionStart := indexStart + strings.Index(projectContent[indexStart:indexEnd], "CURRENT_PROJECT_VERSION = ") + 26
versionEnd := versionStart + strings.Index(projectContent[versionStart:indexEnd], ";")
version := projectContent[versionStart:versionEnd]
if version == newVersion {
continue
}
updated = true
projectContent = projectContent[:versionStart] + newVersion + projectContent[versionEnd:]
}
return projectContent, updated
}
func findObjectKey(objectsMap map[string]any, bundleIDList []string) []string {
var objectKeyList []string
for objectKey, object := range objectsMap {
buildSettings := object.(map[string]any)["buildSettings"]
if buildSettings == nil {
continue
}
bundleIDObject := buildSettings.(map[string]any)["PRODUCT_BUNDLE_IDENTIFIER"]
if bundleIDObject == nil {
continue
}
if common.Contains(bundleIDList, bundleIDObject.(string)) {
objectKeyList = append(objectKeyList, objectKey)
}
}
return objectKeyList
}
func findObjectKeyByDirectory(objectsMap map[string]any, directoryList []string) []string {
var objectKeyList []string
for objectKey, object := range objectsMap {
buildSettings := object.(map[string]any)["buildSettings"]
if buildSettings == nil {
continue
}
infoPListFile := buildSettings.(map[string]any)["INFOPLIST_FILE"]
if infoPListFile == nil {
continue
}
for _, searchDirectory := range directoryList {
if strings.HasPrefix(infoPListFile.(string), searchDirectory+"/") {
objectKeyList = append(objectKeyList, objectKey)
}
}
}
return objectKeyList
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/gofrs/uuid/v5"
"github.com/spf13/cobra"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
var commandGenerate = &cobra.Command{
@@ -21,7 +22,8 @@ var commandGenerate = &cobra.Command{
func init() {
commandGenerate.AddCommand(commandGenerateUUID)
commandGenerate.AddCommand(commandGenerateRandom)
commandGenerate.AddCommand(commandGenerateWireGuardKeyPair)
commandGenerate.AddCommand(commandGenerateRealityKeyPair)
mainCommand.AddCommand(commandGenerate)
}
@@ -90,3 +92,48 @@ func generateUUID() error {
_, err = os.Stdout.WriteString(newUUID.String() + "\n")
return err
}
var commandGenerateWireGuardKeyPair = &cobra.Command{
Use: "wg-keypair",
Short: "Generate WireGuard key pair",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
err := generateWireGuardKey()
if err != nil {
log.Fatal(err)
}
},
}
func generateWireGuardKey() error {
privateKey, err := wgtypes.GeneratePrivateKey()
if err != nil {
return err
}
os.Stdout.WriteString("PrivateKey: " + privateKey.String() + "\n")
os.Stdout.WriteString("PublicKey: " + privateKey.PublicKey().String() + "\n")
return nil
}
var commandGenerateRealityKeyPair = &cobra.Command{
Use: "reality-keypair",
Short: "Generate reality key pair",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
err := generateRealityKey()
if err != nil {
log.Fatal(err)
}
},
}
func generateRealityKey() error {
privateKey, err := wgtypes.GeneratePrivateKey()
if err != nil {
return err
}
publicKey := privateKey.PublicKey()
os.Stdout.WriteString("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey[:]) + "\n")
os.Stdout.WriteString("PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey[:]) + "\n")
return nil
}

View File

@@ -1,39 +0,0 @@
package main
import (
"os"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/log"
"github.com/spf13/cobra"
)
var pqSignatureSchemesEnabled bool
var commandGenerateECHKeyPair = &cobra.Command{
Use: "ech-keypair <plain_server_name>",
Short: "Generate TLS ECH key pair",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := generateECHKeyPair(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGenerateECHKeyPair.Flags().BoolVar(&pqSignatureSchemesEnabled, "pq-signature-schemes-enabled", false, "Enable PQ signature schemes")
commandGenerate.AddCommand(commandGenerateECHKeyPair)
}
func generateECHKeyPair(serverName string) error {
configPem, keyPem, err := tls.ECHKeygenDefault(serverName, pqSignatureSchemesEnabled)
if err != nil {
return err
}
os.Stdout.WriteString(configPem)
os.Stdout.WriteString(keyPem)
return nil
}

View File

@@ -1,40 +0,0 @@
package main
import (
"os"
"time"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/log"
"github.com/spf13/cobra"
)
var flagGenerateTLSKeyPairMonths int
var commandGenerateTLSKeyPair = &cobra.Command{
Use: "tls-keypair <server_name>",
Short: "Generate TLS self sign key pair",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := generateTLSKeyPair(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGenerateTLSKeyPair.Flags().IntVarP(&flagGenerateTLSKeyPairMonths, "months", "m", 1, "Valid months")
commandGenerate.AddCommand(commandGenerateTLSKeyPair)
}
func generateTLSKeyPair(serverName string) error {
privateKeyPem, publicKeyPem, err := tls.GenerateKeyPair(time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0))
if err != nil {
return err
}
os.Stdout.WriteString(string(privateKeyPem) + "\n")
os.Stdout.WriteString(string(publicKeyPem) + "\n")
return nil
}

View File

@@ -1,40 +0,0 @@
//go:build go1.20
package main
import (
"crypto/ecdh"
"crypto/rand"
"encoding/base64"
"os"
"github.com/sagernet/sing-box/log"
"github.com/spf13/cobra"
)
var commandGenerateVAPIDKeyPair = &cobra.Command{
Use: "vapid-keypair",
Short: "Generate VAPID key pair",
Run: func(cmd *cobra.Command, args []string) {
err := generateVAPIDKeyPair()
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGenerate.AddCommand(commandGenerateVAPIDKeyPair)
}
func generateVAPIDKeyPair() error {
privateKey, err := ecdh.P256().GenerateKey(rand.Reader)
if err != nil {
return err
}
publicKey := privateKey.PublicKey()
os.Stdout.WriteString("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey.Bytes()) + "\n")
os.Stdout.WriteString("PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey.Bytes()) + "\n")
return nil
}

View File

@@ -1,61 +0,0 @@
package main
import (
"encoding/base64"
"os"
"github.com/sagernet/sing-box/log"
"github.com/spf13/cobra"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
func init() {
commandGenerate.AddCommand(commandGenerateWireGuardKeyPair)
commandGenerate.AddCommand(commandGenerateRealityKeyPair)
}
var commandGenerateWireGuardKeyPair = &cobra.Command{
Use: "wg-keypair",
Short: "Generate WireGuard key pair",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
err := generateWireGuardKey()
if err != nil {
log.Fatal(err)
}
},
}
func generateWireGuardKey() error {
privateKey, err := wgtypes.GeneratePrivateKey()
if err != nil {
return err
}
os.Stdout.WriteString("PrivateKey: " + privateKey.String() + "\n")
os.Stdout.WriteString("PublicKey: " + privateKey.PublicKey().String() + "\n")
return nil
}
var commandGenerateRealityKeyPair = &cobra.Command{
Use: "reality-keypair",
Short: "Generate reality key pair",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
err := generateRealityKey()
if err != nil {
log.Fatal(err)
}
},
}
func generateRealityKey() error {
privateKey, err := wgtypes.GeneratePrivateKey()
if err != nil {
return err
}
publicKey := privateKey.PublicKey()
os.Stdout.WriteString("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey[:]) + "\n")
os.Stdout.WriteString("PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey[:]) + "\n")
return nil
}

View File

@@ -1,174 +0,0 @@
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"github.com/sagernet/sing-box/common/json"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
"github.com/spf13/cobra"
)
var commandMerge = &cobra.Command{
Use: "merge [output]",
Short: "Merge configurations",
Run: func(cmd *cobra.Command, args []string) {
err := merge(args[0])
if err != nil {
log.Fatal(err)
}
},
Args: cobra.ExactArgs(1),
}
func init() {
mainCommand.AddCommand(commandMerge)
}
func merge(outputPath string) error {
mergedOptions, err := readConfigAndMerge()
if err != nil {
return err
}
err = mergePathResources(&mergedOptions)
if err != nil {
return err
}
buffer := new(bytes.Buffer)
encoder := json.NewEncoder(buffer)
encoder.SetIndent("", " ")
err = encoder.Encode(mergedOptions)
if err != nil {
return E.Cause(err, "encode config")
}
if existsContent, err := os.ReadFile(outputPath); err != nil {
if string(existsContent) == buffer.String() {
return nil
}
}
err = rw.WriteFile(outputPath, buffer.Bytes())
if err != nil {
return err
}
outputPath, _ = filepath.Abs(outputPath)
os.Stderr.WriteString(outputPath + "\n")
return nil
}
func mergePathResources(options *option.Options) error {
for index, inbound := range options.Inbounds {
switch inbound.Type {
case C.TypeHTTP:
inbound.HTTPOptions.TLS = mergeTLSInboundOptions(inbound.HTTPOptions.TLS)
case C.TypeMixed:
inbound.MixedOptions.TLS = mergeTLSInboundOptions(inbound.MixedOptions.TLS)
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
}
for index, outbound := range options.Outbounds {
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:
outbound.SSHOptions = mergeSSHOutboundOptions(outbound.SSHOptions)
case C.TypeVLESS:
outbound.VLESSOptions.TLS = mergeTLSOutboundOptions(outbound.VLESSOptions.TLS)
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
}
return nil
}
func mergeTLSInboundOptions(options *option.InboundTLSOptions) *option.InboundTLSOptions {
if options == nil {
return nil
}
if options.CertificatePath != "" {
if content, err := os.ReadFile(options.CertificatePath); err == nil {
options.Certificate = trimStringArray(strings.Split(string(content), "\n"))
}
}
if options.KeyPath != "" {
if content, err := os.ReadFile(options.KeyPath); err == nil {
options.Key = trimStringArray(strings.Split(string(content), "\n"))
}
}
if options.ECH != nil {
if options.ECH.KeyPath != "" {
if content, err := os.ReadFile(options.ECH.KeyPath); err == nil {
options.ECH.Key = trimStringArray(strings.Split(string(content), "\n"))
}
}
}
return options
}
func mergeTLSOutboundOptions(options *option.OutboundTLSOptions) *option.OutboundTLSOptions {
if options == nil {
return nil
}
if options.CertificatePath != "" {
if content, err := os.ReadFile(options.CertificatePath); err == nil {
options.Certificate = trimStringArray(strings.Split(string(content), "\n"))
}
}
if options.ECH != nil {
if options.ECH.ConfigPath != "" {
if content, err := os.ReadFile(options.ECH.ConfigPath); err == nil {
options.ECH.Config = trimStringArray(strings.Split(string(content), "\n"))
}
}
}
return options
}
func mergeSSHOutboundOptions(options option.SSHOutboundOptions) option.SSHOutboundOptions {
if options.PrivateKeyPath != "" {
if content, err := os.ReadFile(os.ExpandEnv(options.PrivateKeyPath)); err == nil {
options.PrivateKey = trimStringArray(strings.Split(string(content), "\n"))
}
}
return options
}
func trimStringArray(array []string) []string {
return common.Filter(array, func(it string) bool {
return strings.TrimSpace(it) != ""
})
}

View File

@@ -143,16 +143,14 @@ func create() (*box.Box, context.CancelFunc, error) {
signal.Stop(osSignals)
close(osSignals)
}()
startCtx, finishStart := context.WithCancel(context.Background())
go func() {
_, loaded := <-osSignals
if loaded {
cancel()
closeMonitor(startCtx)
}
}()
err = instance.Start()
finishStart()
if err != nil {
cancel()
return nil, nil, E.Cause(err, "start service")

View File

@@ -0,0 +1,62 @@
package baderror
import (
"context"
"io"
"net"
"strings"
E "github.com/sagernet/sing/common/exceptions"
)
func Contains(err error, msgList ...string) bool {
for _, msg := range msgList {
if strings.Contains(err.Error(), msg) {
return true
}
}
return false
}
func WrapH2(err error) error {
if err == nil {
return nil
}
err = E.Unwrap(err)
if err == io.ErrUnexpectedEOF {
return io.EOF
}
if Contains(err, "client disconnected", "body closed by handler", "response body closed", "; CANCEL") {
return net.ErrClosed
}
return err
}
func WrapGRPC(err error) error {
// grpc uses stupid internal error types
if err == nil {
return nil
}
if Contains(err, "EOF") {
return io.EOF
}
if Contains(err, "Canceled") {
return context.Canceled
}
if Contains(err,
"the client connection is closing",
"server closed the stream without sending trailers") {
return net.ErrClosed
}
return err
}
func WrapQUIC(err error) error {
if err == nil {
return nil
}
if Contains(err, "canceled by local with error code 0") {
return net.ErrClosed
}
return err
}

View File

@@ -5,10 +5,8 @@ package badtls
import (
"crypto/tls"
"os"
aTLS "github.com/sagernet/sing/common/tls"
)
func Create(conn *tls.Conn) (aTLS.Conn, error) {
func Create(conn *tls.Conn) (TLSConn, error) {
return nil, os.ErrInvalid
}

View File

@@ -57,10 +57,6 @@ func (v Version) After(anotherVersion Version) bool {
return false
}
func (v Version) VersionString() string {
return F.ToString(v.Major, ".", v.Minor, ".", v.Patch)
}
func (v Version) String() string {
version := F.ToString(v.Major, ".", v.Minor, ".", v.Patch)
if v.PreReleaseIdentifier != "" {

View File

@@ -17,7 +17,7 @@ func NewConn(conn net.Conn) (net.Conn, error) {
element := openConnection.PushBack(conn)
connAccess.Unlock()
if KillerEnabled {
err := KillerCheck()
err := killerCheck()
if err != nil {
conn.Close()
return nil, err

View File

@@ -1,20 +1,20 @@
package conntrack
import (
"runtime"
runtimeDebug "runtime/debug"
"time"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/memory"
)
var (
KillerEnabled bool
MemoryLimit uint64
MemoryLimit int64
killerLastCheck time.Time
)
func KillerCheck() error {
func killerCheck() error {
if !KillerEnabled {
return nil
}
@@ -23,7 +23,10 @@ func KillerCheck() error {
return nil
}
killerLastCheck = nowTime
if memory.Total() > MemoryLimit {
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
inuseMemory := int64(memStats.StackInuse + memStats.HeapInuse + memStats.HeapIdle - memStats.HeapReleased)
if inuseMemory > MemoryLimit {
Close()
go func() {
time.Sleep(time.Second)

View File

@@ -18,7 +18,7 @@ func NewPacketConn(conn net.PacketConn) (net.PacketConn, error) {
element := openConnection.PushBack(conn)
connAccess.Unlock()
if KillerEnabled {
err := KillerCheck()
err := killerCheck()
if err != nil {
conn.Close()
return nil, err

View File

@@ -6,18 +6,19 @@ import (
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/conntrack"
"github.com/sagernet/sing-box/common/dialer/conntrack"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/tfo-go"
)
type DefaultDialer struct {
dialer4 tcpDialer
dialer6 tcpDialer
dialer4 tfo.Dialer
dialer6 tfo.Dialer
udpDialer4 net.Dialer
udpDialer6 net.Dialer
udpListener net.ListenConfig
@@ -93,22 +94,14 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()
}
if options.TCPMultiPath {
if !go121Available {
if !multipathTCPAvailable {
return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.")
}
setMultiPathTCP(&dialer4)
}
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
if err != nil {
return nil, err
}
tcpDialer6, err := newTCPDialer(dialer6, options.TCPFastOpen)
if err != nil {
return nil, err
}
return &DefaultDialer{
tcpDialer4,
tcpDialer6,
tfo.Dialer{Dialer: dialer4, DisableTFO: !options.TCPFastOpen},
tfo.Dialer{Dialer: dialer6, DisableTFO: !options.TCPFastOpen},
udpDialer4,
udpDialer6,
listener,
@@ -137,12 +130,10 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address
}
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if destination.IsIPv6() {
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6))
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4))
} else {
if !destination.IsIPv6() {
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4))
} else {
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6))
}
}

View File

@@ -1,15 +0,0 @@
//go:build go1.20
package dialer
import (
"net"
"github.com/sagernet/tfo-go"
)
type tcpDialer = tfo.Dialer
func newTCPDialer(dialer net.Dialer, tfoEnabled bool) (tcpDialer, error) {
return tfo.Dialer{Dialer: dialer, DisableTFO: !tfoEnabled}, nil
}

View File

@@ -4,7 +4,7 @@ package dialer
import "net"
const go121Available = true
const multipathTCPAvailable = true
func setMultiPathTCP(dialer *net.Dialer) {
dialer.SetMultipathTCP(true)

View File

@@ -1,18 +0,0 @@
//go:build !go1.20
package dialer
import (
"net"
E "github.com/sagernet/sing/common/exceptions"
)
type tcpDialer = net.Dialer
func newTCPDialer(dialer net.Dialer, tfoEnabled bool) (tcpDialer, error) {
if tfoEnabled {
return dialer, E.New("TCP Fast Open requires go1.20, please recompile your binary.")
}
return dialer, nil
}

View File

@@ -6,7 +6,7 @@ import (
"net"
)
const go121Available = false
const multipathTCPAvailable = false
func setMultiPathTCP(dialer *net.Dialer) {
}

View File

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

View File

@@ -16,16 +16,14 @@ import (
type ResolveDialer struct {
dialer N.Dialer
parallel bool
router adapter.Router
strategy dns.DomainStrategy
fallbackDelay time.Duration
}
func NewResolveDialer(router adapter.Router, dialer N.Dialer, parallel bool, strategy dns.DomainStrategy, fallbackDelay time.Duration) *ResolveDialer {
func NewResolveDialer(router adapter.Router, dialer N.Dialer, strategy dns.DomainStrategy, fallbackDelay time.Duration) *ResolveDialer {
return &ResolveDialer{
dialer,
parallel,
router,
strategy,
fallbackDelay,
@@ -36,7 +34,7 @@ func (d *ResolveDialer) DialContext(ctx context.Context, network string, destina
if !destination.IsFqdn() {
return d.dialer.DialContext(ctx, network, destination)
}
ctx, metadata := adapter.ExtendContext(ctx)
ctx, metadata := adapter.AppendContext(ctx)
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
metadata.Destination = destination
metadata.Domain = ""
@@ -50,18 +48,14 @@ func (d *ResolveDialer) DialContext(ctx context.Context, network string, destina
if err != nil {
return nil, err
}
if d.parallel {
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, d.fallbackDelay)
} else {
return N.DialSerial(ctx, d.dialer, network, destination, addresses)
}
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, d.fallbackDelay)
}
func (d *ResolveDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if !destination.IsFqdn() {
return d.dialer.ListenPacket(ctx, destination)
}
ctx, metadata := adapter.ExtendContext(ctx)
ctx, metadata := adapter.AppendContext(ctx)
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
metadata.Destination = destination
metadata.Domain = ""

View File

@@ -1,5 +1,3 @@
//go:build go1.20
package dialer
import (
@@ -7,7 +5,6 @@ import (
"io"
"net"
"os"
"sync"
"time"
"github.com/sagernet/sing/common"
@@ -25,11 +22,10 @@ type slowOpenConn struct {
destination M.Socksaddr
conn net.Conn
create chan struct{}
access sync.Mutex
err error
}
func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
func DialSlowContext(dialer *tfo.Dialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if dialer.DisableTFO || N.NetworkName(network) != N.NetworkTCP {
switch N.NetworkName(network) {
case N.NetworkTCP, N.NetworkUDP:
@@ -62,26 +58,15 @@ func (c *slowOpenConn) Read(b []byte) (n int, err error) {
}
func (c *slowOpenConn) Write(b []byte) (n int, err error) {
if c.conn != nil {
return c.conn.Write(b)
}
c.access.Lock()
defer c.access.Unlock()
select {
case <-c.create:
if c.err != nil {
return 0, c.err
if c.conn == nil {
c.conn, err = c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
if err != nil {
c.err = E.Cause(err, "dial tcp fast open")
}
return c.conn.Write(b)
default:
close(c.create)
return
}
c.conn, err = c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
if err != nil {
c.conn = nil
c.err = E.Cause(err, "dial tcp fast open")
}
close(c.create)
return
return c.conn.Write(b)
}
func (c *slowOpenConn) Close() error {

View File

@@ -1,20 +0,0 @@
//go:build !go1.20
package dialer
import (
"context"
"net"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
switch N.NetworkName(network) {
case N.NetworkTCP, N.NetworkUDP:
return dialer.DialContext(ctx, network, destination.String())
default:
return dialer.DialContext(ctx, network, destination.AddrString())
}
}

View File

@@ -1,158 +0,0 @@
package humanize
import (
"fmt"
"math"
"strconv"
"strings"
"unicode"
)
// IEC Sizes.
// kibis of bits
const (
Byte = 1 << (iota * 10)
KiByte
MiByte
GiByte
TiByte
PiByte
EiByte
)
// SI Sizes.
const (
IByte = 1
KByte = IByte * 1000
MByte = KByte * 1000
GByte = MByte * 1000
TByte = GByte * 1000
PByte = TByte * 1000
EByte = PByte * 1000
)
var defaultSizeTable = map[string]uint64{
"b": Byte,
"kib": KiByte,
"kb": KByte,
"mib": MiByte,
"mb": MByte,
"gib": GiByte,
"gb": GByte,
"tib": TiByte,
"tb": TByte,
"pib": PiByte,
"pb": PByte,
"eib": EiByte,
"eb": EByte,
// Without suffix
"": Byte,
"ki": KiByte,
"k": KByte,
"mi": MiByte,
"m": MByte,
"gi": GiByte,
"g": GByte,
"ti": TiByte,
"t": TByte,
"pi": PiByte,
"p": PByte,
"ei": EiByte,
"e": EByte,
}
var memorysSizeTable = map[string]uint64{
"b": Byte,
"kb": KiByte,
"mb": MiByte,
"gb": GiByte,
"tb": TiByte,
"pb": PiByte,
"eb": EiByte,
"": Byte,
"k": KiByte,
"m": MiByte,
"g": GiByte,
"t": TiByte,
"p": PiByte,
"e": EiByte,
}
var (
defaultSizes = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
iSizes = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
)
func Bytes(s uint64) string {
return humanateBytes(s, 1000, defaultSizes)
}
func MemoryBytes(s uint64) string {
return humanateBytes(s, 1024, defaultSizes)
}
func IBytes(s uint64) string {
return humanateBytes(s, 1024, iSizes)
}
func logn(n, b float64) float64 {
return math.Log(n) / math.Log(b)
}
func humanateBytes(s uint64, base float64, sizes []string) string {
if s < 10 {
return fmt.Sprintf("%d B", s)
}
e := math.Floor(logn(float64(s), base))
suffix := sizes[int(e)]
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
f := "%.0f %s"
if val < 10 {
f = "%.1f %s"
}
return fmt.Sprintf(f, val, suffix)
}
func ParseBytes(s string) (uint64, error) {
return parseBytes0(s, defaultSizeTable)
}
func ParseMemoryBytes(s string) (uint64, error) {
return parseBytes0(s, memorysSizeTable)
}
func parseBytes0(s string, sizeTable map[string]uint64) (uint64, error) {
lastDigit := 0
hasComma := false
for _, r := range s {
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
break
}
if r == ',' {
hasComma = true
}
lastDigit++
}
num := s[:lastDigit]
if hasComma {
num = strings.Replace(num, ",", "", -1)
}
f, err := strconv.ParseFloat(num, 64)
if err != nil {
return 0, err
}
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
if m, ok := sizeTable[extra]; ok {
f *= float64(m)
if f >= math.MaxUint64 {
return 0, fmt.Errorf("too large: %v", s)
}
return uint64(f), nil
}
return 0, fmt.Errorf("unhandled size name: %v", extra)
}

View File

@@ -1,75 +0,0 @@
package interrupt
import (
"net"
"github.com/sagernet/sing/common/x/list"
)
/*type GroupedConn interface {
MarkAsInternal()
}
func MarkAsInternal(conn any) {
if groupedConn, isGroupConn := common.Cast[GroupedConn](conn); isGroupConn {
groupedConn.MarkAsInternal()
}
}*/
type Conn struct {
net.Conn
group *Group
element *list.Element[*groupConnItem]
}
/*func (c *Conn) MarkAsInternal() {
c.element.Value.internal = true
}*/
func (c *Conn) Close() error {
c.group.access.Lock()
defer c.group.access.Unlock()
c.group.connections.Remove(c.element)
return c.Conn.Close()
}
func (c *Conn) ReaderReplaceable() bool {
return true
}
func (c *Conn) WriterReplaceable() bool {
return true
}
func (c *Conn) Upstream() any {
return c.Conn
}
type PacketConn struct {
net.PacketConn
group *Group
element *list.Element[*groupConnItem]
}
/*func (c *PacketConn) MarkAsInternal() {
c.element.Value.internal = true
}*/
func (c *PacketConn) Close() error {
c.group.access.Lock()
defer c.group.access.Unlock()
c.group.connections.Remove(c.element)
return c.PacketConn.Close()
}
func (c *PacketConn) ReaderReplaceable() bool {
return true
}
func (c *PacketConn) WriterReplaceable() bool {
return true
}
func (c *PacketConn) Upstream() any {
return c.PacketConn
}

View File

@@ -1,13 +0,0 @@
package interrupt
import "context"
type contextKeyIsExternalConnection struct{}
func ContextWithIsExternalConnection(ctx context.Context) context.Context {
return context.WithValue(ctx, contextKeyIsExternalConnection{}, true)
}
func IsExternalConnectionFromContext(ctx context.Context) bool {
return ctx.Value(contextKeyIsExternalConnection{}) != nil
}

View File

@@ -1,52 +0,0 @@
package interrupt
import (
"io"
"net"
"sync"
"github.com/sagernet/sing/common/x/list"
)
type Group struct {
access sync.Mutex
connections list.List[*groupConnItem]
}
type groupConnItem struct {
conn io.Closer
isExternal bool
}
func NewGroup() *Group {
return &Group{}
}
func (g *Group) NewConn(conn net.Conn, isExternal bool) net.Conn {
g.access.Lock()
defer g.access.Unlock()
item := g.connections.PushBack(&groupConnItem{conn, isExternal})
return &Conn{Conn: conn, group: g, element: item}
}
func (g *Group) NewPacketConn(conn net.PacketConn, isExternal bool) net.PacketConn {
g.access.Lock()
defer g.access.Unlock()
item := g.connections.PushBack(&groupConnItem{conn, isExternal})
return &PacketConn{PacketConn: conn, group: g, element: item}
}
func (g *Group) Interrupt(interruptExternalConnections bool) {
g.access.Lock()
defer g.access.Unlock()
var toDelete []*list.Element[*groupConnItem]
for element := g.connections.Front(); element != nil; element = element.Next() {
if !element.Value.isExternal || interruptExternalConnections {
element.Value.conn.Close()
toDelete = append(toDelete, element)
}
}
for _, element := range toDelete {
g.connections.Remove(element)
}
}

View File

@@ -1,42 +1,21 @@
package mux
import (
C "github.com/sagernet/sing-box/constant"
"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 Client = mux.Client
func NewClientWithOptions(dialer N.Dialer, logger logger.Logger, options option.OutboundMultiplexOptions) (*Client, error) {
func NewClientWithOptions(dialer N.Dialer, options option.MultiplexOptions) (*Client, error) {
if !options.Enabled {
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{
Dialer: dialer,
Logger: logger,
Protocol: options.Protocol,
MaxConnections: options.MaxConnections,
MinStreams: options.MinStreams,
MaxStreams: options.MaxStreams,
Padding: options.Padding,
Brutal: brutalOptions,
})
}

14
common/mux/protocol.go Normal file
View File

@@ -0,0 +1,14 @@
package mux
import (
"github.com/sagernet/sing-mux"
)
type (
Client = mux.Client
)
var (
Destination = mux.Destination
HandleConnection = mux.HandleConnection
)

View File

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

View File

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

View File

@@ -0,0 +1,50 @@
package proxyproto
import (
"context"
"net"
"net/netip"
"github.com/sagernet/sing-box/adapter"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/pires/go-proxyproto"
)
var _ N.Dialer = (*Dialer)(nil)
type Dialer struct {
N.Dialer
}
func (d *Dialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
switch N.NetworkName(network) {
case N.NetworkTCP:
conn, err := d.Dialer.DialContext(ctx, network, destination)
if err != nil {
return nil, err
}
var source M.Socksaddr
metadata := adapter.ContextFrom(ctx)
if metadata != nil {
source = metadata.Source
}
if !source.IsValid() {
source = M.SocksaddrFromNet(conn.LocalAddr())
}
if destination.Addr.Is6() {
source = M.SocksaddrFrom(netip.AddrFrom16(source.Addr.As16()), source.Port)
}
h := proxyproto.HeaderProxyFromAddrs(1, source.TCPAddr(), destination.TCPAddr())
_, err = h.WriteTo(conn)
if err != nil {
conn.Close()
return nil, E.Cause(err, "write proxy protocol header")
}
return conn, nil
default:
return d.Dialer.DialContext(ctx, network, destination)
}
}

View File

@@ -0,0 +1,62 @@
package proxyproto
import (
std_bufio "bufio"
"net"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
"github.com/pires/go-proxyproto"
)
type Listener struct {
net.Listener
AcceptNoHeader bool
}
func (l *Listener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
bufReader := std_bufio.NewReader(conn)
header, err := proxyproto.Read(bufReader)
if err != nil && !(l.AcceptNoHeader && err == proxyproto.ErrNoProxyProtocol) {
return nil, &Error{err}
}
if bufReader.Buffered() > 0 {
cache := buf.NewSize(bufReader.Buffered())
_, err = cache.ReadFullFrom(bufReader, cache.FreeLen())
if err != nil {
return nil, &Error{err}
}
conn = bufio.NewCachedConn(conn, cache)
}
if header != nil {
return &bufio.AddrConn{Conn: conn, Metadata: M.Metadata{
Source: M.SocksaddrFromNet(header.SourceAddr).Unwrap(),
Destination: M.SocksaddrFromNet(header.DestinationAddr).Unwrap(),
}}, nil
}
return conn, nil
}
var _ net.Error = (*Error)(nil)
type Error struct {
error
}
func (e *Error) Unwrap() error {
return e.error
}
func (e *Error) Timeout() bool {
return false
}
func (e *Error) Temporary() bool {
return true
}

View File

@@ -1,73 +1,43 @@
package settings
import (
"context"
"os"
"strings"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/shell"
)
type AndroidSystemProxy struct {
useRish bool
rishPath string
serverAddr M.Socksaddr
supportSOCKS bool
isEnabled bool
}
var (
useRish bool
rishPath string
)
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*AndroidSystemProxy, error) {
func init() {
userId := os.Getuid()
var (
useRish bool
rishPath string
)
if userId == 0 || userId == 1000 || userId == 2000 {
useRish = false
} else {
rishPath, useRish = C.FindPath("rish")
if !useRish {
return nil, E.Cause(os.ErrPermission, "root or system (adb) permission is required for set system proxy")
}
}
return &AndroidSystemProxy{
useRish: useRish,
rishPath: rishPath,
serverAddr: serverAddr,
supportSOCKS: supportSOCKS,
}, nil
}
func (p *AndroidSystemProxy) IsEnabled() bool {
return p.isEnabled
}
func (p *AndroidSystemProxy) Enable() error {
err := p.runAndroidShell("settings", "put", "global", "http_proxy", p.serverAddr.String())
if err != nil {
return err
}
p.isEnabled = true
return nil
}
func (p *AndroidSystemProxy) Disable() error {
err := p.runAndroidShell("settings", "put", "global", "http_proxy", ":0")
if err != nil {
return err
}
p.isEnabled = false
return nil
}
func (p *AndroidSystemProxy) runAndroidShell(name string, args ...string) error {
if !p.useRish {
func runAndroidShell(name string, args ...string) error {
if !useRish {
return shell.Exec(name, args...).Attach().Run()
} else {
return shell.Exec("sh", p.rishPath, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run()
return shell.Exec("sh", rishPath, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run()
}
}
func SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() error, error) {
err := runAndroidShell("settings", "put", "global", "http_proxy", F.ToString("127.0.0.1:", port))
if err != nil {
return nil, err
}
return func() error {
return runAndroidShell("settings", "put", "global", "http_proxy", ":0")
}, nil
}

View File

@@ -1,56 +1,56 @@
package settings
import (
"context"
"net/netip"
"strconv"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-tun"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/shell"
"github.com/sagernet/sing/common/x/list"
)
type DarwinSystemProxy struct {
type systemProxy struct {
monitor tun.DefaultInterfaceMonitor
interfaceName string
element *list.Element[tun.DefaultInterfaceUpdateCallback]
serverAddr M.Socksaddr
supportSOCKS bool
isEnabled bool
port uint16
isMixed bool
}
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*DarwinSystemProxy, error) {
interfaceMonitor := adapter.RouterFromContext(ctx).InterfaceMonitor()
if interfaceMonitor == nil {
return nil, E.New("missing interface monitor")
func (p *systemProxy) update(event int) {
newInterfaceName := p.monitor.DefaultInterfaceName(netip.IPv4Unspecified())
if p.interfaceName == newInterfaceName {
return
}
proxy := &DarwinSystemProxy{
monitor: interfaceMonitor,
serverAddr: serverAddr,
supportSOCKS: supportSOCKS,
if p.interfaceName != "" {
_ = p.unset()
}
proxy.element = interfaceMonitor.RegisterCallback(proxy.update)
return proxy, nil
p.interfaceName = newInterfaceName
interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName)
if err != nil {
return
}
if p.isMixed {
err = shell.Exec("networksetup", "-setsocksfirewallproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
}
if err == nil {
err = shell.Exec("networksetup", "-setwebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
}
if err == nil {
_ = shell.Exec("networksetup", "-setsecurewebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
}
return
}
func (p *DarwinSystemProxy) IsEnabled() bool {
return p.isEnabled
}
func (p *DarwinSystemProxy) Enable() error {
return p.update0()
}
func (p *DarwinSystemProxy) Disable() error {
func (p *systemProxy) unset() error {
interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName)
if err != nil {
return err
}
if p.supportSOCKS {
if p.isMixed {
err = shell.Exec("networksetup", "-setsocksfirewallproxystate", interfaceDisplayName, "off").Attach().Run()
}
if err == nil {
@@ -59,53 +59,9 @@ func (p *DarwinSystemProxy) Disable() error {
if err == nil {
err = shell.Exec("networksetup", "-setsecurewebproxystate", interfaceDisplayName, "off").Attach().Run()
}
if err == nil {
p.isEnabled = false
}
return err
}
func (p *DarwinSystemProxy) update(event int) {
if event&tun.EventInterfaceUpdate == 0 {
return
}
if !p.isEnabled {
return
}
_ = p.update0()
}
func (p *DarwinSystemProxy) update0() error {
newInterfaceName := p.monitor.DefaultInterfaceName(netip.IPv4Unspecified())
if p.interfaceName == newInterfaceName {
return nil
}
if p.interfaceName != "" {
_ = p.Disable()
}
p.interfaceName = newInterfaceName
interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName)
if err != nil {
return err
}
if p.supportSOCKS {
err = shell.Exec("networksetup", "-setsocksfirewallproxy", interfaceDisplayName, p.serverAddr.AddrString(), strconv.Itoa(int(p.serverAddr.Port))).Attach().Run()
}
if err != nil {
return err
}
err = shell.Exec("networksetup", "-setwebproxy", interfaceDisplayName, p.serverAddr.AddrString(), strconv.Itoa(int(p.serverAddr.Port))).Attach().Run()
if err != nil {
return err
}
err = shell.Exec("networksetup", "-setsecurewebproxy", interfaceDisplayName, p.serverAddr.AddrString(), strconv.Itoa(int(p.serverAddr.Port))).Attach().Run()
if err != nil {
return err
}
p.isEnabled = true
return nil
}
func getInterfaceDisplayName(name string) (string, error) {
content, err := shell.Exec("networksetup", "-listallhardwareports").ReadOutput()
if err != nil {
@@ -121,3 +77,21 @@ func getInterfaceDisplayName(name string) (string, error) {
}
return "", E.New(name, " not found in networksetup -listallhardwareports")
}
func SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() error, error) {
interfaceMonitor := router.InterfaceMonitor()
if interfaceMonitor == nil {
return nil, E.New("missing interface monitor")
}
proxy := &systemProxy{
monitor: interfaceMonitor,
port: port,
isMixed: isMixed,
}
proxy.update(tun.EventInterfaceUpdate)
proxy.element = interfaceMonitor.RegisterCallback(proxy.update)
return func() error {
interfaceMonitor.UnregisterCallback(proxy.element)
return proxy.unset()
}, nil
}

View File

@@ -3,161 +3,75 @@
package settings
import (
"context"
"os"
"os/exec"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/shell"
)
type LinuxSystemProxy struct {
hasGSettings bool
hasKWriteConfig5 bool
sudoUser string
serverAddr M.Socksaddr
supportSOCKS bool
isEnabled bool
}
var (
hasGSettings bool
sudoUser string
)
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*LinuxSystemProxy, error) {
hasGSettings := common.Error(exec.LookPath("gsettings")) == nil
hasKWriteConfig5 := common.Error(exec.LookPath("kwriteconfig5")) == nil
var sudoUser string
func init() {
hasGSettings = common.Error(exec.LookPath("gsettings")) == nil
if os.Getuid() == 0 {
sudoUser = os.Getenv("SUDO_USER")
}
if !hasGSettings && !hasKWriteConfig5 {
return nil, E.New("unsupported desktop environment")
}
return &LinuxSystemProxy{
hasGSettings: hasGSettings,
hasKWriteConfig5: hasKWriteConfig5,
sudoUser: sudoUser,
serverAddr: serverAddr,
supportSOCKS: supportSOCKS,
}, nil
}
func (p *LinuxSystemProxy) IsEnabled() bool {
return p.isEnabled
}
func (p *LinuxSystemProxy) Enable() error {
if p.hasGSettings {
err := p.runAsUser("gsettings", "set", "org.gnome.system.proxy.http", "enabled", "true")
if err != nil {
return err
}
if p.supportSOCKS {
err = p.setGnomeProxy("ftp", "http", "https", "socks")
} else {
err = p.setGnomeProxy("http", "https")
}
if err != nil {
return err
}
err = p.runAsUser("gsettings", "set", "org.gnome.system.proxy", "use-same-proxy", F.ToString(p.supportSOCKS))
if err != nil {
return err
}
err = p.runAsUser("gsettings", "set", "org.gnome.system.proxy", "mode", "manual")
if err != nil {
return err
}
}
if p.hasKWriteConfig5 {
err := p.runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "1")
if err != nil {
return err
}
if p.supportSOCKS {
err = p.setKDEProxy("ftp", "http", "https", "socks")
} else {
err = p.setKDEProxy("http", "https")
}
if err != nil {
return err
}
err = p.runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "Authmode", "0")
if err != nil {
return err
}
err = p.runAsUser("dbus-send", "--type=signal", "/KIO/Scheduler", "org.kde.KIO.Scheduler.reparseSlaveConfiguration", "string:''")
if err != nil {
return err
}
}
p.isEnabled = true
return nil
}
func (p *LinuxSystemProxy) Disable() error {
if p.hasGSettings {
err := p.runAsUser("gsettings", "set", "org.gnome.system.proxy", "mode", "none")
if err != nil {
return err
}
}
if p.hasKWriteConfig5 {
err := p.runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "0")
if err != nil {
return err
}
err = p.runAsUser("dbus-send", "--type=signal", "/KIO/Scheduler", "org.kde.KIO.Scheduler.reparseSlaveConfiguration", "string:''")
if err != nil {
return err
}
}
p.isEnabled = false
return nil
}
func (p *LinuxSystemProxy) runAsUser(name string, args ...string) error {
func runAsUser(name string, args ...string) error {
if os.Getuid() != 0 {
return shell.Exec(name, args...).Attach().Run()
} else if p.sudoUser != "" {
return shell.Exec("su", "-", p.sudoUser, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run()
} else if sudoUser != "" {
return shell.Exec("su", "-", sudoUser, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run()
} else {
return E.New("set system proxy: unable to set as root")
}
}
func (p *LinuxSystemProxy) setGnomeProxy(proxyTypes ...string) error {
for _, proxyType := range proxyTypes {
err := p.runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "host", p.serverAddr.AddrString())
if err != nil {
return err
}
err = p.runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "port", F.ToString(p.serverAddr.Port))
if err != nil {
return err
}
func SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() error, error) {
if !hasGSettings {
return nil, E.New("unsupported desktop environment")
}
return nil
err := runAsUser("gsettings", "set", "org.gnome.system.proxy.http", "enabled", "true")
if err != nil {
return nil, err
}
if isMixed {
err = setGnomeProxy(port, "ftp", "http", "https", "socks")
} else {
err = setGnomeProxy(port, "http", "https")
}
if err != nil {
return nil, err
}
err = runAsUser("gsettings", "set", "org.gnome.system.proxy", "use-same-proxy", F.ToString(isMixed))
if err != nil {
return nil, err
}
err = runAsUser("gsettings", "set", "org.gnome.system.proxy", "mode", "manual")
if err != nil {
return nil, err
}
return func() error {
return runAsUser("gsettings", "set", "org.gnome.system.proxy", "mode", "none")
}, nil
}
func (p *LinuxSystemProxy) setKDEProxy(proxyTypes ...string) error {
func setGnomeProxy(port uint16, proxyTypes ...string) error {
for _, proxyType := range proxyTypes {
var proxyUrl string
if proxyType == "socks" {
proxyUrl = "socks://" + p.serverAddr.String()
} else {
proxyUrl = "http://" + p.serverAddr.String()
err := runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "host", "127.0.0.1")
if err != nil {
return err
}
err := p.runAsUser(
"kwriteconfig5",
"--file",
"kioslaverc",
"--group",
"Proxy Settings",
"--key", proxyType+"Proxy",
proxyUrl,
)
err = runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "port", F.ToString(port))
if err != nil {
return err
}

View File

@@ -3,12 +3,11 @@
package settings
import (
"context"
"os"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing-box/adapter"
)
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (SystemProxy, error) {
func SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() error, error) {
return nil, os.ErrInvalid
}

View File

@@ -1,43 +1,17 @@
package settings
import (
"context"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing-box/adapter"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/wininet"
)
type WindowsSystemProxy struct {
serverAddr M.Socksaddr
supportSOCKS bool
isEnabled bool
}
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*WindowsSystemProxy, error) {
return &WindowsSystemProxy{
serverAddr: serverAddr,
supportSOCKS: supportSOCKS,
func SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() error, error) {
err := wininet.SetSystemProxy(F.ToString("http://127.0.0.1:", port), "")
if err != nil {
return nil, err
}
return func() error {
return wininet.ClearSystemProxy()
}, nil
}
func (p *WindowsSystemProxy) IsEnabled() bool {
return p.isEnabled
}
func (p *WindowsSystemProxy) Enable() error {
err := wininet.SetSystemProxy("http://"+p.serverAddr.String(), "")
if err != nil {
return err
}
p.isEnabled = true
return nil
}
func (p *WindowsSystemProxy) Disable() error {
err := wininet.ClearSystemProxy()
if err != nil {
return err
}
p.isEnabled = false
return nil
}

View File

@@ -1,7 +0,0 @@
package settings
type SystemProxy interface {
IsEnabled() bool
Enable() error
Disable() error
}

View File

@@ -182,52 +182,11 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
break
}
switch frameType {
case 0x00: // PADDING
case 0x0:
continue
case 0x01: // PING
case 0x1:
continue
case 0x02, 0x03: // ACK
_, 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
case 0x6:
var offset uint64
offset, err = qtls.ReadUvarint(decryptedReader)
if err != nil {
@@ -249,26 +208,8 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
if err != nil {
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:
return nil, os.ErrInvalid
// ignore unknown frame type
}
}
tlsHdr := make([]byte, 5)

View File

@@ -22,29 +22,23 @@ func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout
if timeout == 0 {
timeout = C.ReadPayloadTimeout
}
deadline := time.Now().Add(timeout)
err := conn.SetReadDeadline(time.Now().Add(timeout))
if err != nil {
return nil, E.Cause(err, "set read deadline")
}
_, err = buffer.ReadOnceFrom(conn)
err = E.Errors(err, conn.SetReadDeadline(time.Time{}))
if err != nil {
return nil, E.Cause(err, "read payload")
}
var metadata *adapter.InboundContext
var errors []error
for i := 0; i < 3; i++ {
err := conn.SetReadDeadline(deadline)
if err != nil {
return nil, E.Cause(err, "set read deadline")
}
_, err = buffer.ReadOnceFrom(conn)
err = E.Errors(err, conn.SetReadDeadline(time.Time{}))
if err != nil {
if i > 0 {
break
}
return nil, E.Cause(err, "read payload")
}
for _, sniffer := range sniffers {
metadata, err := sniffer(ctx, bytes.NewReader(buffer.Bytes()))
if metadata != nil {
return metadata, nil
}
errors = append(errors, err)
for _, sniffer := range sniffers {
metadata, err = sniffer(ctx, bytes.NewReader(buffer.Bytes()))
if metadata != nil {
return metadata, nil
}
errors = append(errors, err)
}
return nil, E.Errors(errors...)
}

View File

@@ -9,13 +9,10 @@ import (
"strings"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/caddyserver/certmagic"
"github.com/libdns/alidns"
"github.com/libdns/cloudflare"
"github.com/mholt/acmez/acme"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
@@ -77,24 +74,6 @@ func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Con
AltTLSALPNPort: int(options.AlternativeTLSPort),
Logger: config.Logger,
}
if dnsOptions := options.DNS01Challenge; dnsOptions != nil && dnsOptions.Provider != "" {
var solver certmagic.DNS01Solver
switch dnsOptions.Provider {
case C.DNSProviderAliDNS:
solver.DNSProvider = &alidns.Provider{
AccKeyID: dnsOptions.AliDNSOptions.AccessKeyID,
AccKeySecret: dnsOptions.AliDNSOptions.AccessKeySecret,
RegionID: dnsOptions.AliDNSOptions.RegionID,
}
case C.DNSProviderCloudflare:
solver.DNSProvider = &cloudflare.Provider{
APIToken: dnsOptions.CloudflareOptions.APIToken,
}
default:
return nil, nil, E.New("unsupported ACME DNS01 provider type: " + dnsOptions.Provider)
}
acmeConfig.DNS01Solver = &solver
}
if options.ExternalAccount != nil && options.ExternalAccount.KeyID != "" {
acmeConfig.ExternalAccount = (*acme.EAB)(options.ExternalAccount)
}

View File

@@ -13,29 +13,29 @@ import (
aTLS "github.com/sagernet/sing/common/tls"
)
func NewDialerFromOptions(ctx context.Context, router adapter.Router, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
func NewDialerFromOptions(router adapter.Router, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
if !options.Enabled {
return dialer, nil
}
config, err := NewClient(ctx, serverAddress, options)
config, err := NewClient(router, serverAddress, options)
if err != nil {
return nil, err
}
return NewDialer(dialer, config), nil
}
func NewClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
func NewClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
if !options.Enabled {
return nil, nil
}
if options.ECH != nil && options.ECH.Enabled {
return NewECHClient(ctx, serverAddress, options)
return NewECHClient(router, serverAddress, options)
} else if options.Reality != nil && options.Reality.Enabled {
return NewRealityClient(ctx, serverAddress, options)
return NewRealityClient(router, serverAddress, options)
} else if options.UTLS != nil && options.UTLS.Enabled {
return NewUTLSClient(ctx, serverAddress, options)
return NewUTLSClient(router, serverAddress, options)
} else {
return NewSTDClient(ctx, serverAddress, options)
return NewSTDClient(router, serverAddress, options)
}
}

View File

@@ -7,53 +7,50 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"net"
"net/netip"
"os"
"strings"
cftls "github.com/sagernet/cloudflare-tls"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-dns"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
mDNS "github.com/miekg/dns"
)
type echClientConfig struct {
type ECHClientConfig struct {
config *cftls.Config
}
func (c *echClientConfig) ServerName() string {
return c.config.ServerName
func (e *ECHClientConfig) ServerName() string {
return e.config.ServerName
}
func (c *echClientConfig) SetServerName(serverName string) {
c.config.ServerName = serverName
func (e *ECHClientConfig) SetServerName(serverName string) {
e.config.ServerName = serverName
}
func (c *echClientConfig) NextProtos() []string {
return c.config.NextProtos
func (e *ECHClientConfig) NextProtos() []string {
return e.config.NextProtos
}
func (c *echClientConfig) SetNextProtos(nextProto []string) {
c.config.NextProtos = nextProto
func (e *ECHClientConfig) SetNextProtos(nextProto []string) {
e.config.NextProtos = nextProto
}
func (c *echClientConfig) Config() (*STDConfig, error) {
func (e *ECHClientConfig) Config() (*STDConfig, error) {
return nil, E.New("unsupported usage for ECH")
}
func (c *echClientConfig) Client(conn net.Conn) (Conn, error) {
return &echConnWrapper{cftls.Client(conn, c.config)}, nil
func (e *ECHClientConfig) Client(conn net.Conn) (Conn, error) {
return &echConnWrapper{cftls.Client(conn, e.config)}, nil
}
func (c *echClientConfig) Clone() Config {
return &echClientConfig{
config: c.config.Clone(),
func (e *ECHClientConfig) Clone() Config {
return &ECHClientConfig{
config: e.config.Clone(),
}
}
@@ -83,7 +80,7 @@ func (c *echConnWrapper) Upstream() any {
return c.Conn
}
func NewECHClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
func NewECHClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
var serverName string
if options.ServerName != "" {
serverName = options.ServerName
@@ -97,7 +94,7 @@ func NewECHClient(ctx context.Context, serverAddress string, options option.Outb
}
var tlsConfig cftls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.Time = router.TimeFunc()
if options.DisableSNI {
tlsConfig.ServerName = "127.0.0.1"
} else {
@@ -149,8 +146,8 @@ func NewECHClient(ctx context.Context, serverAddress string, options option.Outb
}
}
var certificate []byte
if len(options.Certificate) > 0 {
certificate = []byte(strings.Join(options.Certificate, "\n"))
if options.Certificate != "" {
certificate = []byte(options.Certificate)
} else if options.CertificatePath != "" {
content, err := os.ReadFile(options.CertificatePath)
if err != nil {
@@ -171,36 +168,24 @@ func NewECHClient(ctx context.Context, serverAddress string, options option.Outb
tlsConfig.ECHEnabled = true
tlsConfig.PQSignatureSchemesEnabled = options.ECH.PQSignatureSchemesEnabled
tlsConfig.DynamicRecordSizingDisabled = options.ECH.DynamicRecordSizingDisabled
var echConfig []byte
if len(options.ECH.Config) > 0 {
echConfig = []byte(strings.Join(options.ECH.Config, "\n"))
} else if options.ECH.ConfigPath != "" {
content, err := os.ReadFile(options.ECH.ConfigPath)
if options.ECH.Config != "" {
clientConfigContent, err := base64.StdEncoding.DecodeString(options.ECH.Config)
if err != nil {
return nil, E.Cause(err, "read ECH config")
return nil, err
}
echConfig = content
}
if len(echConfig) > 0 {
block, rest := pem.Decode(echConfig)
if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 {
return nil, E.New("invalid ECH configs pem")
}
echConfigs, err := cftls.UnmarshalECHConfigs(block.Bytes)
clientConfig, err := cftls.UnmarshalECHConfigs(clientConfigContent)
if err != nil {
return nil, E.Cause(err, "parse ECH configs")
return nil, err
}
tlsConfig.ClientECHConfigs = echConfigs
tlsConfig.ClientECHConfigs = clientConfig
} else {
tlsConfig.GetClientECHConfigs = fetchECHClientConfig(ctx)
tlsConfig.GetClientECHConfigs = fetchECHClientConfig(router)
}
return &echClientConfig{&tlsConfig}, nil
return &ECHClientConfig{&tlsConfig}, nil
}
func fetchECHClientConfig(ctx context.Context) func(_ context.Context, serverName string) ([]cftls.ECHConfig, error) {
return func(_ context.Context, serverName string) ([]cftls.ECHConfig, error) {
func fetchECHClientConfig(router adapter.Router) func(ctx context.Context, serverName string) ([]cftls.ECHConfig, error) {
return func(ctx context.Context, serverName string) ([]cftls.ECHConfig, error) {
message := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
RecursionDesired: true,
@@ -213,7 +198,7 @@ func fetchECHClientConfig(ctx context.Context) func(_ context.Context, serverNam
},
},
}
response, err := adapter.RouterFromContext(ctx).Exchange(ctx, message)
response, err := router.Exchange(ctx, message)
if err != nil {
return nil, err
}

View File

@@ -1,169 +0,0 @@
//go:build with_ech
package tls
import (
"bytes"
"encoding/binary"
"encoding/pem"
cftls "github.com/sagernet/cloudflare-tls"
E "github.com/sagernet/sing/common/exceptions"
"github.com/cloudflare/circl/hpke"
"github.com/cloudflare/circl/kem"
)
func ECHKeygenDefault(serverName string, pqSignatureSchemesEnabled bool) (configPem string, keyPem string, err error) {
cipherSuites := []echCipherSuite{
{
kdf: hpke.KDF_HKDF_SHA256,
aead: hpke.AEAD_AES128GCM,
}, {
kdf: hpke.KDF_HKDF_SHA256,
aead: hpke.AEAD_ChaCha20Poly1305,
},
}
keyConfig := []myECHKeyConfig{
{id: 0, kem: hpke.KEM_X25519_HKDF_SHA256},
}
if pqSignatureSchemesEnabled {
keyConfig = append(keyConfig, myECHKeyConfig{id: 1, kem: hpke.KEM_X25519_KYBER768_DRAFT00})
}
keyPairs, err := echKeygen(0xfe0d, serverName, keyConfig, cipherSuites)
if err != nil {
return
}
var configBuffer bytes.Buffer
var totalLen uint16
for _, keyPair := range keyPairs {
totalLen += uint16(len(keyPair.rawConf))
}
binary.Write(&configBuffer, binary.BigEndian, totalLen)
for _, keyPair := range keyPairs {
configBuffer.Write(keyPair.rawConf)
}
var keyBuffer bytes.Buffer
for _, keyPair := range keyPairs {
keyBuffer.Write(keyPair.rawKey)
}
configPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBuffer.Bytes()}))
keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBuffer.Bytes()}))
return
}
type echKeyConfigPair struct {
id uint8
key cftls.EXP_ECHKey
rawKey []byte
conf myECHKeyConfig
rawConf []byte
}
type echCipherSuite struct {
kdf hpke.KDF
aead hpke.AEAD
}
type myECHKeyConfig struct {
id uint8
kem hpke.KEM
seed []byte
}
func echKeygen(version uint16, serverName string, conf []myECHKeyConfig, suite []echCipherSuite) ([]echKeyConfigPair, error) {
be := binary.BigEndian
// prepare for future update
if version != 0xfe0d {
return nil, E.New("unsupported ECH version", version)
}
suiteBuf := make([]byte, 0, len(suite)*4+2)
suiteBuf = be.AppendUint16(suiteBuf, uint16(len(suite))*4)
for _, s := range suite {
if !s.kdf.IsValid() || !s.aead.IsValid() {
return nil, E.New("invalid HPKE cipher suite")
}
suiteBuf = be.AppendUint16(suiteBuf, uint16(s.kdf))
suiteBuf = be.AppendUint16(suiteBuf, uint16(s.aead))
}
pairs := []echKeyConfigPair{}
for _, c := range conf {
pair := echKeyConfigPair{}
pair.id = c.id
pair.conf = c
if !c.kem.IsValid() {
return nil, E.New("invalid HPKE KEM")
}
kpGenerator := c.kem.Scheme().GenerateKeyPair
if len(c.seed) > 0 {
kpGenerator = func() (kem.PublicKey, kem.PrivateKey, error) {
pub, sec := c.kem.Scheme().DeriveKeyPair(c.seed)
return pub, sec, nil
}
if len(c.seed) < c.kem.Scheme().PrivateKeySize() {
return nil, E.New("HPKE KEM seed too short")
}
}
pub, sec, err := kpGenerator()
if err != nil {
return nil, E.Cause(err, "generate ECH config key pair")
}
b := []byte{}
b = be.AppendUint16(b, version)
b = be.AppendUint16(b, 0) // length field
// contents
// key config
b = append(b, c.id)
b = be.AppendUint16(b, uint16(c.kem))
pubBuf, err := pub.MarshalBinary()
if err != nil {
return nil, E.Cause(err, "serialize ECH public key")
}
b = be.AppendUint16(b, uint16(len(pubBuf)))
b = append(b, pubBuf...)
b = append(b, suiteBuf...)
// end key config
// max name len, not supported
b = append(b, 0)
// server name
b = append(b, byte(len(serverName)))
b = append(b, []byte(serverName)...)
// extensions, not supported
b = be.AppendUint16(b, 0)
be.PutUint16(b[2:], uint16(len(b)-4))
pair.rawConf = b
secBuf, err := sec.MarshalBinary()
sk := []byte{}
sk = be.AppendUint16(sk, uint16(len(secBuf)))
sk = append(sk, secBuf...)
sk = be.AppendUint16(sk, uint16(len(b)))
sk = append(sk, b...)
cfECHKeys, err := cftls.EXP_UnmarshalECHKeys(sk)
if err != nil {
return nil, E.Cause(err, "bug: can't parse generated ECH server key")
}
if len(cfECHKeys) != 1 {
return nil, E.New("bug: unexpected server key count")
}
pair.key = cfECHKeys[0]
pair.rawKey = sk
pairs = append(pairs, pair)
}
return pairs, nil
}

View File

@@ -1,56 +0,0 @@
//go:build with_quic && with_ech
package tls
import (
"context"
"net"
"net/http"
"github.com/sagernet/cloudflare-tls"
"github.com/sagernet/quic-go/ech"
"github.com/sagernet/quic-go/http3_ech"
"github.com/sagernet/sing-quic"
M "github.com/sagernet/sing/common/metadata"
)
var (
_ qtls.Config = (*echClientConfig)(nil)
_ qtls.ServerConfig = (*echServerConfig)(nil)
)
func (c *echClientConfig) Dial(ctx context.Context, conn net.PacketConn, addr net.Addr, config *quic.Config) (quic.Connection, error) {
return quic.Dial(ctx, conn, addr, c.config, config)
}
func (c *echClientConfig) DialEarly(ctx context.Context, conn net.PacketConn, addr net.Addr, config *quic.Config) (quic.EarlyConnection, error) {
return quic.DialEarly(ctx, conn, addr, c.config, config)
}
func (c *echClientConfig) CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config, enableDatagrams bool) http.RoundTripper {
return &http3.RoundTripper{
TLSClientConfig: c.config,
QuicConfig: quicConfig,
EnableDatagrams: enableDatagrams,
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
quicConn, err := quic.DialEarly(ctx, conn, serverAddr.UDPAddr(), tlsCfg, cfg)
if err != nil {
return nil, err
}
*quicConnPtr = quicConn
return quicConn, nil
},
}
}
func (c *echServerConfig) Listen(conn net.PacketConn, config *quic.Config) (qtls.Listener, error) {
return quic.Listen(conn, c.config, config)
}
func (c *echServerConfig) ListenEarly(conn net.PacketConn, config *quic.Config) (qtls.EarlyListener, error) {
return quic.ListenEarly(conn, c.config, config)
}
func (c *echServerConfig) ConfigureHTTP3() {
http3.ConfigureTLSConfig(c.config)
}

View File

@@ -1,343 +0,0 @@
//go:build with_ech
package tls
import (
"context"
"crypto/tls"
"encoding/pem"
"net"
"os"
"strings"
cftls "github.com/sagernet/cloudflare-tls"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
"github.com/fsnotify/fsnotify"
)
type echServerConfig struct {
config *cftls.Config
logger log.Logger
certificate []byte
key []byte
certificatePath string
keyPath string
watcher *fsnotify.Watcher
echKeyPath string
echWatcher *fsnotify.Watcher
}
func (c *echServerConfig) ServerName() string {
return c.config.ServerName
}
func (c *echServerConfig) SetServerName(serverName string) {
c.config.ServerName = serverName
}
func (c *echServerConfig) NextProtos() []string {
return c.config.NextProtos
}
func (c *echServerConfig) SetNextProtos(nextProto []string) {
c.config.NextProtos = nextProto
}
func (c *echServerConfig) Config() (*STDConfig, error) {
return nil, E.New("unsupported usage for ECH")
}
func (c *echServerConfig) Client(conn net.Conn) (Conn, error) {
return &echConnWrapper{cftls.Client(conn, c.config)}, nil
}
func (c *echServerConfig) Server(conn net.Conn) (Conn, error) {
return &echConnWrapper{cftls.Server(conn, c.config)}, nil
}
func (c *echServerConfig) Clone() Config {
return &echServerConfig{
config: c.config.Clone(),
}
}
func (c *echServerConfig) Start() error {
if c.certificatePath != "" && c.keyPath != "" {
err := c.startWatcher()
if err != nil {
c.logger.Warn("create fsnotify watcher: ", err)
}
}
if c.echKeyPath != "" {
err := c.startECHWatcher()
if err != nil {
c.logger.Warn("create fsnotify watcher: ", err)
}
}
return nil
}
func (c *echServerConfig) startWatcher() error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
if c.certificatePath != "" {
err = watcher.Add(c.certificatePath)
if err != nil {
return err
}
}
if c.keyPath != "" {
err = watcher.Add(c.keyPath)
if err != nil {
return err
}
}
c.watcher = watcher
go c.loopUpdate()
return nil
}
func (c *echServerConfig) loopUpdate() {
for {
select {
case event, ok := <-c.watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write != fsnotify.Write {
continue
}
err := c.reloadKeyPair()
if err != nil {
c.logger.Error(E.Cause(err, "reload TLS key pair"))
}
case err, ok := <-c.watcher.Errors:
if !ok {
return
}
c.logger.Error(E.Cause(err, "fsnotify error"))
}
}
}
func (c *echServerConfig) reloadKeyPair() error {
if c.certificatePath != "" {
certificate, err := os.ReadFile(c.certificatePath)
if err != nil {
return E.Cause(err, "reload certificate from ", c.certificatePath)
}
c.certificate = certificate
}
if c.keyPath != "" {
key, err := os.ReadFile(c.keyPath)
if err != nil {
return E.Cause(err, "reload key from ", c.keyPath)
}
c.key = key
}
keyPair, err := cftls.X509KeyPair(c.certificate, c.key)
if err != nil {
return E.Cause(err, "reload key pair")
}
c.config.Certificates = []cftls.Certificate{keyPair}
c.logger.Info("reloaded TLS certificate")
return nil
}
func (c *echServerConfig) startECHWatcher() error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
err = watcher.Add(c.echKeyPath)
if err != nil {
return err
}
c.echWatcher = watcher
go c.loopECHUpdate()
return nil
}
func (c *echServerConfig) loopECHUpdate() {
for {
select {
case event, ok := <-c.echWatcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write != fsnotify.Write {
continue
}
err := c.reloadECHKey()
if err != nil {
c.logger.Error(E.Cause(err, "reload ECH key"))
}
case err, ok := <-c.echWatcher.Errors:
if !ok {
return
}
c.logger.Error(E.Cause(err, "fsnotify error"))
}
}
}
func (c *echServerConfig) reloadECHKey() error {
echKeyContent, err := os.ReadFile(c.echKeyPath)
if err != nil {
return err
}
block, rest := pem.Decode(echKeyContent)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return E.New("invalid ECH keys pem")
}
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
if err != nil {
return E.Cause(err, "parse ECH keys")
}
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
if err != nil {
return E.Cause(err, "create ECH key set")
}
c.config.ServerECHProvider = echKeySet
c.logger.Info("reloaded ECH keys")
return nil
}
func (c *echServerConfig) Close() error {
var err error
if c.watcher != nil {
err = E.Append(err, c.watcher.Close(), func(err error) error {
return E.Cause(err, "close certificate watcher")
})
}
if c.echWatcher != nil {
err = E.Append(err, c.echWatcher.Close(), func(err error) error {
return E.Cause(err, "close ECH key watcher")
})
}
return err
}
func NewECHServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
if !options.Enabled {
return nil, nil
}
var tlsConfig cftls.Config
if options.ACME != nil && len(options.ACME.Domain) > 0 {
return nil, E.New("acme is unavailable in ech")
}
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
if options.ServerName != "" {
tlsConfig.ServerName = options.ServerName
}
if len(options.ALPN) > 0 {
tlsConfig.NextProtos = append(options.ALPN, tlsConfig.NextProtos...)
}
if options.MinVersion != "" {
minVersion, err := ParseTLSVersion(options.MinVersion)
if err != nil {
return nil, E.Cause(err, "parse min_version")
}
tlsConfig.MinVersion = minVersion
}
if options.MaxVersion != "" {
maxVersion, err := ParseTLSVersion(options.MaxVersion)
if err != nil {
return nil, E.Cause(err, "parse max_version")
}
tlsConfig.MaxVersion = maxVersion
}
if options.CipherSuites != nil {
find:
for _, cipherSuite := range options.CipherSuites {
for _, tlsCipherSuite := range tls.CipherSuites() {
if cipherSuite == tlsCipherSuite.Name {
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID)
continue find
}
}
return nil, E.New("unknown cipher_suite: ", cipherSuite)
}
}
var certificate []byte
var key []byte
if len(options.Certificate) > 0 {
certificate = []byte(strings.Join(options.Certificate, "\n"))
} else if options.CertificatePath != "" {
content, err := os.ReadFile(options.CertificatePath)
if err != nil {
return nil, E.Cause(err, "read certificate")
}
certificate = content
}
if len(options.Key) > 0 {
key = []byte(strings.Join(options.Key, "\n"))
} else if options.KeyPath != "" {
content, err := os.ReadFile(options.KeyPath)
if err != nil {
return nil, E.Cause(err, "read key")
}
key = content
}
if certificate == nil {
return nil, E.New("missing certificate")
} else if key == nil {
return nil, E.New("missing key")
}
keyPair, err := cftls.X509KeyPair(certificate, key)
if err != nil {
return nil, E.Cause(err, "parse x509 key pair")
}
tlsConfig.Certificates = []cftls.Certificate{keyPair}
var echKey []byte
if len(options.ECH.Key) > 0 {
echKey = []byte(strings.Join(options.ECH.Key, "\n"))
} else if options.KeyPath != "" {
content, err := os.ReadFile(options.ECH.KeyPath)
if err != nil {
return nil, E.Cause(err, "read ECH key")
}
echKey = content
} else {
return nil, E.New("missing ECH key")
}
block, rest := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return nil, E.New("invalid ECH keys pem")
}
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
if err != nil {
return nil, E.Cause(err, "parse ECH keys")
}
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
if err != nil {
return nil, E.Cause(err, "create ECH key set")
}
tlsConfig.ECHEnabled = true
tlsConfig.PQSignatureSchemesEnabled = options.ECH.PQSignatureSchemesEnabled
tlsConfig.DynamicRecordSizingDisabled = options.ECH.DynamicRecordSizingDisabled
tlsConfig.ServerECHProvider = echKeySet
return &echServerConfig{
config: &tlsConfig,
logger: logger,
certificate: certificate,
key: key,
certificatePath: options.CertificatePath,
keyPath: options.KeyPath,
echKeyPath: options.ECH.KeyPath,
}, nil
}

View File

@@ -3,23 +3,11 @@
package tls
import (
"context"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
var errECHNotIncluded = E.New(`ECH is not included in this build, rebuild with -tags with_ech`)
func NewECHServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
return nil, errECHNotIncluded
}
func NewECHClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return nil, errECHNotIncluded
}
func ECHKeygenDefault(host string, pqSignatureSchemesEnabled bool) (configPem string, keyPem string, err error) {
return "", "", errECHNotIncluded
func NewECHClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return nil, E.New(`ECH is not included in this build, rebuild with -tags with_ech`)
}

View File

@@ -11,34 +11,22 @@ import (
"time"
)
func GenerateCertificate(timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
privateKeyPem, publicKeyPem, err := GenerateKeyPair(timeFunc, serverName, timeFunc().Add(time.Hour))
if err != nil {
return nil, err
}
certificate, err := tls.X509KeyPair(publicKeyPem, privateKeyPem)
if err != nil {
return nil, err
}
return &certificate, err
}
func GenerateKeyPair(timeFunc func() time.Time, serverName string, expire time.Time) (privateKeyPem []byte, publicKeyPem []byte, err error) {
func GenerateKeyPair(timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
if timeFunc == nil {
timeFunc = time.Now
}
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return
return nil, err
}
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return
return nil, err
}
template := &x509.Certificate{
SerialNumber: serialNumber,
NotBefore: timeFunc().Add(time.Hour * -1),
NotAfter: expire,
NotAfter: timeFunc().Add(time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
@@ -49,13 +37,17 @@ func GenerateKeyPair(timeFunc func() time.Time, serverName string, expire time.T
}
publicDer, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
if err != nil {
return
return nil, err
}
privateDer, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return
return nil, err
}
publicKeyPem = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer})
privateKeyPem = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateDer})
return
publicPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer})
privPem := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateDer})
keyPair, err := tls.X509KeyPair(publicPem, privPem)
if err != nil {
return nil, err
}
return &keyPair, err
}

View File

@@ -26,6 +26,7 @@ import (
"time"
"unsafe"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/debug"
E "github.com/sagernet/sing/common/exceptions"
@@ -44,12 +45,12 @@ type RealityClientConfig struct {
shortID [8]byte
}
func NewRealityClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (*RealityClientConfig, error) {
func NewRealityClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (*RealityClientConfig, error) {
if options.UTLS == nil || !options.UTLS.Enabled {
return nil, E.New("uTLS is required by reality client")
}
uClient, err := NewUTLSClient(ctx, serverAddress, options)
uClient, err := NewUTLSClient(router, serverAddress, options)
if err != nil {
return nil, err
}

View File

@@ -19,7 +19,6 @@ import (
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp"
)
var _ ServerConfigCompat = (*RealityServerConfig)(nil)
@@ -28,13 +27,13 @@ type RealityServerConfig struct {
config *reality.Config
}
func NewRealityServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (*RealityServerConfig, error) {
func NewRealityServer(ctx context.Context, router adapter.Router, logger log.Logger, options option.InboundTLSOptions) (*RealityServerConfig, error) {
var tlsConfig reality.Config
if options.ACME != nil && len(options.ACME.Domain) > 0 {
return nil, E.New("acme is unavailable in reality")
}
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.Time = router.TimeFunc()
if options.ServerName != "" {
tlsConfig.ServerName = options.ServerName
}
@@ -67,10 +66,10 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb
return nil, E.New("unknown cipher_suite: ", cipherSuite)
}
}
if len(options.Certificate) > 0 || options.CertificatePath != "" {
if options.Certificate != "" || options.CertificatePath != "" {
return nil, E.New("certificate is unavailable in reality")
}
if len(options.Key) > 0 || options.KeyPath != "" {
if options.Key != "" || options.KeyPath != "" {
return nil, E.New("key is unavailable in reality")
}
@@ -102,7 +101,7 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb
tlsConfig.ShortIds[shortID] = true
}
handshakeDialer, err := dialer.New(adapter.RouterFromContext(ctx), options.Reality.Handshake.DialerOptions)
handshakeDialer, err := dialer.New(router, options.Reality.Handshake.DialerOptions)
if err != nil {
return nil, err
}

View File

@@ -5,11 +5,12 @@ package tls
import (
"context"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func NewRealityServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
func NewRealityServer(ctx context.Context, router adapter.Router, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
return nil, E.New(`reality server is not included in this build, rebuild with -tags with_reality_server`)
}

View File

@@ -4,22 +4,21 @@ 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"
aTLS "github.com/sagernet/sing/common/tls"
)
func NewServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
func NewServer(ctx context.Context, router adapter.Router, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
if !options.Enabled {
return nil, nil
}
if options.ECH != nil && options.ECH.Enabled {
return NewECHServer(ctx, logger, options)
} else if options.Reality != nil && options.Reality.Enabled {
return NewRealityServer(ctx, logger, options)
if options.Reality != nil && options.Reality.Enabled {
return NewRealityServer(ctx, router, logger, options)
} else {
return NewSTDServer(ctx, logger, options)
return NewSTDServer(ctx, router, logger, options)
}
}

View File

@@ -1,17 +1,15 @@
package tls
import (
"context"
"crypto/tls"
"crypto/x509"
"net"
"net/netip"
"os"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
)
type STDClientConfig struct {
@@ -46,7 +44,7 @@ func (s *STDClientConfig) Clone() Config {
return &STDClientConfig{s.config.Clone()}
}
func NewSTDClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
func NewSTDClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
var serverName string
if options.ServerName != "" {
serverName = options.ServerName
@@ -60,7 +58,7 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
}
var tlsConfig tls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.Time = router.TimeFunc()
if options.DisableSNI {
tlsConfig.ServerName = "127.0.0.1"
} else {
@@ -112,8 +110,8 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
}
}
var certificate []byte
if len(options.Certificate) > 0 {
certificate = []byte(strings.Join(options.Certificate, "\n"))
if options.Certificate != "" {
certificate = []byte(options.Certificate)
} else if options.CertificatePath != "" {
content, err := os.ReadFile(options.CertificatePath)
if err != nil {

View File

@@ -5,14 +5,12 @@ import (
"crypto/tls"
"net"
"os"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
"github.com/fsnotify/fsnotify"
)
@@ -158,7 +156,7 @@ func (c *STDServerConfig) Close() error {
return nil
}
func NewSTDServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
func NewSTDServer(ctx context.Context, router adapter.Router, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
if !options.Enabled {
return nil, nil
}
@@ -177,7 +175,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
} else {
tlsConfig = &tls.Config{}
}
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.Time = router.TimeFunc()
if options.ServerName != "" {
tlsConfig.ServerName = options.ServerName
}
@@ -213,8 +211,8 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
var certificate []byte
var key []byte
if acmeService == nil {
if len(options.Certificate) > 0 {
certificate = []byte(strings.Join(options.Certificate, "\n"))
if options.Certificate != "" {
certificate = []byte(options.Certificate)
} else if options.CertificatePath != "" {
content, err := os.ReadFile(options.CertificatePath)
if err != nil {
@@ -222,8 +220,8 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
}
certificate = content
}
if len(options.Key) > 0 {
key = []byte(strings.Join(options.Key, "\n"))
if options.Key != "" {
key = []byte(options.Key)
} else if options.KeyPath != "" {
content, err := os.ReadFile(options.KeyPath)
if err != nil {
@@ -233,7 +231,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
}
if certificate == nil && key == nil && options.Insecure {
tlsConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
return GenerateCertificate(ntp.TimeFuncFromContext(ctx), info.ServerName)
return GenerateKeyPair(router.TimeFunc(), info.ServerName)
}
} else {
if certificate == nil {

View File

@@ -10,11 +10,10 @@ import (
"net"
"net/netip"
"os"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
utls "github.com/sagernet/utls"
"golang.org/x/net/http2"
@@ -114,7 +113,7 @@ func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error {
return c.UConn.HandshakeContext(ctx)
}
func NewUTLSClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (*UTLSClientConfig, error) {
func NewUTLSClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (*UTLSClientConfig, error) {
var serverName string
if options.ServerName != "" {
serverName = options.ServerName
@@ -128,7 +127,7 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
}
var tlsConfig utls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.Time = router.TimeFunc()
if options.DisableSNI {
tlsConfig.ServerName = "127.0.0.1"
} else {
@@ -169,8 +168,8 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
}
}
var certificate []byte
if len(options.Certificate) > 0 {
certificate = []byte(strings.Join(options.Certificate, "\n"))
if options.Certificate != "" {
certificate = []byte(options.Certificate)
} else if options.CertificatePath != "" {
content, err := os.ReadFile(options.CertificatePath)
if err != nil {

View File

@@ -3,16 +3,15 @@
package tls
import (
"context"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func NewUTLSClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
func NewUTLSClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`)
}
func NewRealityClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
func NewRealityClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return nil, E.New(`uTLS, which is required by reality client is not included in this build, rebuild with -tags with_utls`)
}

View File

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

View File

@@ -8,9 +8,9 @@ import (
"sync"
"time"
"github.com/sagernet/sing/common"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list"
)
type History struct {
@@ -21,7 +21,7 @@ type History struct {
type HistoryStorage struct {
access sync.RWMutex
delayHistory map[string]*History
updateHook chan<- struct{}
callbacks list.List[func()]
}
func NewHistoryStorage() *HistoryStorage {
@@ -30,8 +30,16 @@ func NewHistoryStorage() *HistoryStorage {
}
}
func (s *HistoryStorage) SetHook(hook chan<- struct{}) {
s.updateHook = hook
func (s *HistoryStorage) AddListener(listener func()) *list.Element[func()] {
s.access.Lock()
defer s.access.Unlock()
return s.callbacks.PushBack(listener)
}
func (s *HistoryStorage) RemoveListener(element *list.Element[func()]) {
s.access.Lock()
defer s.access.Unlock()
s.callbacks.Remove(element)
}
func (s *HistoryStorage) LoadURLTestHistory(tag string) *History {
@@ -58,20 +66,13 @@ func (s *HistoryStorage) StoreURLTestHistory(tag string, history *History) {
}
func (s *HistoryStorage) notifyUpdated() {
updateHook := s.updateHook
if updateHook != nil {
select {
case updateHook <- struct{}{}:
default:
}
s.access.RLock()
defer s.access.RUnlock()
for element := s.callbacks.Front(); element != nil; element = element.Next() {
element.Value()
}
}
func (s *HistoryStorage) Close() error {
s.updateHook = nil
return nil
}
func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err error) {
if link == "" {
link = "https://www.gstatic.com/generate_204"
@@ -97,25 +98,33 @@ func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err e
return
}
defer instance.Close()
if earlyConn, isEarlyConn := common.Cast[N.EarlyConn](instance); isEarlyConn && earlyConn.NeedHandshake() {
start = time.Now()
}
req, err := http.NewRequest(http.MethodHead, link, nil)
if err != nil {
return
}
client := http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return instance, nil
},
req = req.WithContext(ctx)
transport := &http.Transport{
Dial: func(string, string) (net.Conn, error) {
return instance, nil
},
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
client := http.Client{
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
defer client.CloseIdleConnections()
resp, err := client.Do(req.WithContext(ctx))
resp, err := client.Do(req)
if err != nil {
return
}

View File

@@ -1,6 +0,0 @@
package constant
const (
DNSProviderAliDNS = "alidns"
DNSProviderCloudflare = "cloudflare"
)

View File

@@ -22,7 +22,6 @@ const (
TypeShadowsocksR = "shadowsocksr"
TypeVLESS = "vless"
TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2"
)
const (
@@ -66,8 +65,6 @@ func ProxyDisplayName(proxyType string) string {
return "VLESS"
case TypeTUIC:
return "TUIC"
case TypeHysteria2:
return "Hysteria2"
case TypeSelector:
return "Selector"
case TypeURLTest:

View File

@@ -1,3 +0,0 @@
package constant
const MbpsToBps = 125000

View File

@@ -1,9 +1,8 @@
package constant
const (
V2RayTransportTypeHTTP = "http"
V2RayTransportTypeWebsocket = "ws"
V2RayTransportTypeQUIC = "quic"
V2RayTransportTypeGRPC = "grpc"
V2RayTransportTypeHTTPUpgrade = "httpupgrade"
V2RayTransportTypeHTTP = "http"
V2RayTransportTypeWebsocket = "ws"
V2RayTransportTypeQUIC = "quic"
V2RayTransportTypeGRPC = "grpc"
)

View File

@@ -5,7 +5,7 @@ package box
import (
"runtime/debug"
"github.com/sagernet/sing-box/common/conntrack"
"github.com/sagernet/sing-box/common/dialer/conntrack"
"github.com/sagernet/sing-box/option"
)
@@ -28,7 +28,7 @@ func applyDebugOptions(options option.DebugOptions) {
}
if options.MemoryLimit != 0 {
// debug.SetMemoryLimit(int64(options.MemoryLimit))
conntrack.MemoryLimit = uint64(options.MemoryLimit)
conntrack.MemoryLimit = int64(options.MemoryLimit)
}
if options.OOMKiller != nil {
conntrack.KillerEnabled = *options.OOMKiller

View File

@@ -5,7 +5,7 @@ package box
import (
"runtime/debug"
"github.com/sagernet/sing-box/common/conntrack"
"github.com/sagernet/sing-box/common/dialer/conntrack"
"github.com/sagernet/sing-box/option"
)
@@ -27,8 +27,8 @@ func applyDebugOptions(options option.DebugOptions) {
debug.SetTraceback(options.TraceBack)
}
if options.MemoryLimit != 0 {
debug.SetMemoryLimit(int64(float64(options.MemoryLimit) / 1.5))
conntrack.MemoryLimit = uint64(options.MemoryLimit)
debug.SetMemoryLimit(int64(options.MemoryLimit))
conntrack.MemoryLimit = int64(options.MemoryLimit)
}
if options.OOMKiller != nil {
conntrack.KillerEnabled = *options.OOMKiller

View File

@@ -7,12 +7,12 @@ import (
"runtime/debug"
"github.com/sagernet/sing-box/common/badjson"
"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/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/dustin/go-humanize"
"github.com/go-chi/chi/v5"
)
@@ -37,9 +37,9 @@ func applyDebugListenOption(options option.DebugOptions) {
runtime.ReadMemStats(&memStats)
var memObject badjson.JSONObject
memObject.Put("heap", humanize.MemoryBytes(memStats.HeapInuse))
memObject.Put("stack", humanize.MemoryBytes(memStats.StackInuse))
memObject.Put("idle", humanize.MemoryBytes(memStats.HeapIdle-memStats.HeapReleased))
memObject.Put("heap", humanize.IBytes(memStats.HeapInuse))
memObject.Put("stack", humanize.IBytes(memStats.StackInuse))
memObject.Put("idle", humanize.IBytes(memStats.HeapIdle-memStats.HeapReleased))
memObject.Put("goroutines", runtime.NumGoroutine())
memObject.Put("rss", rusageMaxRSS())

View File

@@ -1,609 +1,3 @@
---
icon: material/alert-decagram
---
# ChangeLog
#### 1.7.1
* Fixes and improvements
#### 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
* Fixes and improvements
Important changes since 1.5:
* Our [Apple tvOS client](/installation/clients/sft) is now available in the App Store 🍎
* Update BBR congestion control for TUIC and Hysteria2 **1**
* Update brutal congestion control for Hysteria2
* Add `brutal_debug` option for Hysteria2
* Update legacy Hysteria protocol **2**
* Add TLS self sign key pair generate command
* Remove [Deprecated Features](/deprecated) by agreement
**1**:
None of the existing Golang BBR congestion control implementations have been reviewed or unit tested.
This update is intended to address the multi-send defects of the old implementation and may introduce new issues.
**2**
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.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
* Fix IPv6 `auto_route` for Linux **1**
* Add legacy builds for old Windows and macOS systems **2**
* Fixes and improvements
**1**:
When `auto_route` is enabled and `strict_route` is disabled, the device can now be reached from external IPv6 addresses.
**2**:
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.
#### 1.6.0-rc.4
* Fixes and improvements
#### 1.6.0-rc.1
* Add legacy builds for old Windows and macOS systems **1**
* Fixes and improvements
**1**:
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.
#### 1.6.0-beta.4
* Fix IPv6 `auto_route` for Linux **1**
* Fixes and improvements
**1**:
When `auto_route` is enabled and `strict_route` is disabled, the device can now be reached from external IPv6 addresses.
#### 1.5.4
* Fix Clash cache crash on arm32 devices
* 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.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
* Fix compatibility with Android 14
* Fixes and improvements
#### 1.6.0-beta.1
* Fixes and improvements
#### 1.6.0-alpha.5
* Fix compatibility with Android 14
* Update BBR congestion control for TUIC and Hysteria2 **1**
* Fixes and improvements
**1**:
None of the existing Golang BBR congestion control implementations have been reviewed or unit tested.
This update is intended to fix a memory leak flaw in the new implementation introduced in 1.6.0-alpha.1 and may
introduce new issues.
#### 1.6.0-alpha.4
* Add `brutal_debug` option for Hysteria2
* Fixes and improvements
#### 1.5.2
* Our [Apple tvOS client](/installation/clients/sft) is now available in the App Store 🍎
* Fixes and improvements
#### 1.6.0-alpha.3
* Fixes and improvements
#### 1.6.0-alpha.2
* Fixes and improvements
#### 1.5.1
* Fixes and improvements
#### 1.6.0-alpha.1
* Update BBR congestion control for TUIC and Hysteria2 **1**
* Update quic-go to v0.39.0
* Update gVisor to 20230814.0
* Remove [Deprecated Features](/deprecated) by agreement
* Fixes and improvements
**1**:
None of the existing Golang BBR congestion control implementations have been reviewed or unit tested.
This update is intended to address the multi-send defects of the old implementation and may introduce new issues.
#### 1.5.0
* Fixes and improvements
Important changes since 1.4:
* Add TLS [ECH server](/configuration/shared/tls) support
* Improve TLS TCH client configuration
* Add TLS ECH key pair generator **1**
* Add TLS ECH support for QUIC based protocols **2**
* Add KDE support for the `set_system_proxy` option in HTTP inbound
* Add Hysteria2 protocol support **3**
* Add `interrupt_exist_connections` option for `Selector` and `URLTest` outbounds **4**
* Add DNS01 challenge support for ACME TLS certificate issuer **5**
* Add `merge` command **6**
* Mark [Deprecated Features](/deprecated)
**1**:
Command: `sing-box generate ech-keypair <plain_server_name> [--pq-signature-schemes-enabled]`
**2**:
All inbounds and outbounds are supported, including `Naiveproxy`, `Hysteria[/2]`, `TUIC` and `V2ray QUIC transport`.
**3**:
See [Hysteria2 inbound](/configuration/inbound/hysteria2) and [Hysteria2 outbound](/configuration/outbound/hysteria2)
For protocol description, please refer to [https://v2.hysteria.network](https://v2.hysteria.network)
**4**:
Interrupt existing connections when the selected outbound has changed.
Only inbound connections are affected by this setting, internal connections will always be interrupted.
**5**:
Only `Alibaba Cloud DNS` and `Cloudflare` are supported, see [ACME Fields](/configuration/shared/tls#acme-fields)
and [DNS01 Challenge Fields](/configuration/shared/dns01_challenge).
**6**:
This command also parses path resources that appear in the configuration file and replaces them with embedded
configuration, such as TLS certificates or SSH private keys.
#### 1.5.0-rc.6
* Fixes and improvements
#### 1.4.6
* Fixes and improvements
#### 1.5.0-rc.5
* Fixed an improper authentication vulnerability in the SOCKS5 inbound
* Fixes and improvements
**Security Advisory**
This update fixes an improper authentication vulnerability in the sing-box SOCKS inbound. This vulnerability allows an
attacker to craft special requests to bypass user authentication. All users exposing SOCKS servers with user
authentication in an insecure environment are advised to update immediately.
此更新修复了 sing-box SOCKS 入站中的一个不正确身份验证漏洞。 该漏洞允许攻击者制作特殊请求来绕过用户身份验证。建议所有将使用用户认证的
SOCKS 服务器暴露在不安全环境下的用户立更新。
#### 1.4.5
* Fixed an improper authentication vulnerability in the SOCKS5 inbound
* Fixes and improvements
**Security Advisory**
This update fixes an improper authentication vulnerability in the sing-box SOCKS inbound. This vulnerability allows an
attacker to craft special requests to bypass user authentication. All users exposing SOCKS servers with user
authentication in an insecure environment are advised to update immediately.
此更新修复了 sing-box SOCKS 入站中的一个不正确身份验证漏洞。 该漏洞允许攻击者制作特殊请求来绕过用户身份验证。建议所有将使用用户认证的
SOCKS 服务器暴露在不安全环境下的用户立更新。
#### 1.5.0-rc.3
* Fixes and improvements
#### 1.5.0-beta.12
* Add `merge` command **1**
* Fixes and improvements
**1**:
This command also parses path resources that appear in the configuration file and replaces them with embedded
configuration, such as TLS certificates or SSH private keys.
```
Merge configurations
Usage:
sing-box merge [output] [flags]
Flags:
-h, --help help for merge
Global Flags:
-c, --config stringArray set configuration file path
-C, --config-directory stringArray set configuration directory path
-D, --directory string set working directory
--disable-color disable color output
```
#### 1.5.0-beta.11
* Add DNS01 challenge support for ACME TLS certificate issuer **1**
* Fixes and improvements
**1**:
Only `Alibaba Cloud DNS` and `Cloudflare` are supported,
see [ACME Fields](/configuration/shared/tls#acme-fields)
and [DNS01 Challenge Fields](/configuration/shared/dns01_challenge).
#### 1.5.0-beta.10
* Add `interrupt_exist_connections` option for `Selector` and `URLTest` outbounds **1**
* Fixes and improvements
**1**:
Interrupt existing connections when the selected outbound has changed.
Only inbound connections are affected by this setting, internal connections will always be interrupted.
#### 1.4.3
* Fixes and improvements
#### 1.5.0-beta.8
* Fixes and improvements
#### 1.4.2
* Fixes and improvements
#### 1.5.0-beta.6
* Fix compatibility issues with official Hysteria2 server and client
* Fixes and improvements
* Mark [deprecated features](/deprecated)
#### 1.5.0-beta.3
* Fixes and improvements
* Updated Hysteria2 documentation **1**
**1**:
Added notes indicating compatibility issues with the official
Hysteria2 server and client when using `fastOpen=false` or UDP MTU >= 1200.
#### 1.5.0-beta.2
* Add hysteria2 protocol support **1**
* Fixes and improvements
**1**:
See [Hysteria2 inbound](/configuration/inbound/hysteria2) and [Hysteria2 outbound](/configuration/outbound/hysteria2)
For protocol description, please refer to [https://v2.hysteria.network](https://v2.hysteria.network)
#### 1.5.0-beta.1
* Add TLS [ECH server](/configuration/shared/tls) support
* Improve TLS TCH client configuration
* Add TLS ECH key pair generator **1**
* Add TLS ECH support for QUIC based protocols **2**
* Add KDE support for the `set_system_proxy` option in HTTP inbound
**1**:
Command: `sing-box generate ech-keypair <plain_server_name> [--pq-signature-schemes-enabled]`
**2**:
All inbounds and outbounds are supported, including `Naiveproxy`, `Hysteria`, `TUIC` and `V2ray QUIC transport`.
#### 1.4.1
* Fixes and improvements
#### 1.4.0
* Fix bugs and update dependencies
Important changes since 1.3:
* Add TUIC support **1**
* Add `udp_over_stream` option for TUIC client **2**
* Add MultiPath TCP support **3**
* Add `include_interface` and `exclude_interface` options for tun inbound
* Pause recurring tasks when no network or device idle
* Improve Android and Apple platform clients
*1*:
See [TUIC inbound](/configuration/inbound/tuic)
and [TUIC outbound](/configuration/outbound/tuic)
**2**:
This is the TUIC port of the [UDP over TCP protocol](/configuration/shared/udp-over-tcp), designed to provide a QUIC
stream based UDP relay mode that TUIC does not provide. Since it is an add-on protocol, you will need to use sing-box or
another program compatible with the protocol as a server.
This mode has no positive effect in a proper UDP proxy scenario and should only be applied to relay streaming UDP
traffic (basically QUIC streams).
*3*:
Requires sing-box to be compiled with Go 1.21.
#### 1.4.0-rc.3
* Fixes and improvements
#### 1.4.0-rc.2
* Fixes and improvements
#### 1.4.0-rc.1
* Fix TUIC UDP
#### 1.4.0-beta.6
* Add `udp_over_stream` option for TUIC client **1**
* Add `include_interface` and `exclude_interface` options for tun inbound
* Fixes and improvements
**1**:
This is the TUIC port of the [UDP over TCP protocol](/configuration/shared/udp-over-tcp), designed to provide a QUIC
stream based UDP relay mode that TUIC does not provide. Since it is an add-on protocol, you will need to use sing-box or
another program compatible with the protocol as a server.
This mode has no positive effect in a proper UDP proxy scenario and should only be applied to relay streaming UDP
traffic (basically QUIC streams).
#### 1.4.0-beta.5
* Fixes and improvements
#### 1.4.0-beta.4
* Graphical clients: Persistence group expansion state
* Fixes and improvements
#### 1.4.0-beta.3
* Fixes and improvements
#### 1.4.0-beta.2
* Add MultiPath TCP support **1**

View File

@@ -1,64 +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: | / |
| `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`

View File

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

View File

@@ -1,52 +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` | ✖️ | Managed by Darwin |
| `inet4_address` | ✔️ | / |
| `inet6_address` | ✔️ | / |
| `mtu` | ✔️ | / |
| `auto_route` | ✔️ | / |
| `strict_route` | ✖️ | Not implemented |
| `inet4_route_address` | ✔️ | / |
| `inet6_route_address` | ✔️ | / |
| `inet4_route_exclude_address` | ✔️ | / |
| `inet6_route_exclude_address` | ✔️ | / |
| `endpoint_independent_nat` | ✔️ | / |
| `stack` | ✔️ | / |
| `include_interface` | ✖️ | Not implemented |
| `exclude_interface` | ✖️ | Not implemented |
| `include_uid` | ✖️ | Not implemented |
| `exclude_uid` | ✖️ | Not implemented |
| `include_android_user` | ✖️ | Not implemented |
| `include_package` | ✖️ | Not implemented |
| `exclude_package` | ✖️ | Not implemented |
| `platform` | ✔️ | / |
| 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`

View File

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

View File

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

View File

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

View File

@@ -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 客户端功能, 但代码质量很差且包含广告。

View File

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

View File

@@ -79,12 +79,6 @@
1000
],
"clash_mode": "direct",
"wifi_ssid": [
"My WIFI"
],
"wifi_bssid": [
"00:00:00:00:00:00"
],
"invert": false,
"outbound": [
"direct"
@@ -194,7 +188,7 @@ Match port range.
#### process_name
!!! quote ""
!!! error ""
Only supported on Linux, Windows, and macOS.
@@ -202,7 +196,7 @@ Match process name.
#### process_path
!!! quote ""
!!! error ""
Only supported on Linux, Windows, and macOS.
@@ -214,7 +208,7 @@ Match android package name.
#### user
!!! quote ""
!!! error ""
Only supported on Linux.
@@ -222,7 +216,7 @@ Match user name.
#### user_id
!!! quote ""
!!! error ""
Only supported on Linux.
@@ -232,24 +226,6 @@ Match user id.
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.
#### invert
Invert match result.

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