Compare commits

..

41 Commits

Author SHA1 Message Date
世界
e556dfad68 Update dependencies 2023-05-03 10:53:00 +08:00
世界
b0a978d4b6 Fix write cached packets 2023-05-01 12:45:30 +08:00
世界
c6e6621ecf Fix shadowsocks key length error message 2023-05-01 11:29:05 +08:00
世界
d3b41dfc01 documentation: Update changelog 2023-04-30 17:00:56 +08:00
XYenon
08f4384579 Fix incorrect use of sort.Slice 2023-04-30 16:58:07 +08:00
世界
d08b82b71c Reimplemented shadowsocks client 2023-04-30 16:38:12 +08:00
世界
d2d3c82ccf Fix wait copy packet 2023-04-28 11:30:19 +08:00
世界
841ef1acaf Set TCP keepalive for WireGuard gVisor TCP connections 2023-04-26 19:34:20 +08:00
Weltolk
115507fb2a documentation: Fix fakeip link broken 2023-04-26 19:34:20 +08:00
Larvan2
c5067af884 Enable mkdocs search in documentation
Signed-off-by: Larvan2 <78135608+Larvan2@users.noreply.github.com>
2023-04-26 19:34:20 +08:00
Hellojack
974b2a3165 Fix UVariantLen usage 2023-04-26 19:34:20 +08:00
世界
955028d4dc Fix cached packets order 2023-04-26 19:34:18 +08:00
世界
7050011802 Improve DNS caching 2023-04-26 19:34:11 +08:00
世界
2a76b8fbeb Fix documentation 2023-04-26 04:56:26 +08:00
世界
e3286d62ce Fix h2mux buffer to large 2023-04-26 04:56:26 +08:00
世界
37657851ae Improve direct copy 2023-04-26 04:56:25 +08:00
世界
26bfcbd33c clash-api: Reset outbounds in DELETE /connections 2023-04-24 19:01:18 +08:00
世界
50827bcff1 Add multiplexer for VLESS outbound 2023-04-24 19:01:18 +08:00
世界
6b64ebd3c0 Migrate multiplex to library 2023-04-24 19:01:17 +08:00
世界
bec606ee88 documentation: Update changelog 2023-04-23 13:47:27 +08:00
世界
8545e41b2f Improve multiplex 2023-04-23 13:46:00 +08:00
世界
d8810b6e8f Update gVisor to 20230417.0 2023-04-23 13:46:00 +08:00
世界
f568bb9fe3 Add debug http server 2023-04-22 19:51:38 +08:00
世界
ccb872a41e Add filemanager api 2023-04-22 19:51:37 +08:00
世界
9c287094e2 Improve multiplex 2023-04-22 19:51:37 +08:00
世界
f61c5600e0 Update wireguard-go 2023-04-22 19:51:36 +08:00
世界
2e98777f82 Ignore system tun stack bind interface error 2023-04-22 19:51:36 +08:00
世界
73e72e9aec Improve VLESS request 2023-04-22 19:51:36 +08:00
世界
922acced94 Update badtls 2023-04-22 19:51:35 +08:00
世界
988d7331c6 Add deadline interface 2023-04-22 19:51:35 +08:00
世界
c0d6dde95b shadowsocks: Multi-user support for legacy AEAD inbound
Signed-off-by: wwqgtxx <wwqgtxx@gmail.com>
2023-04-22 19:51:35 +08:00
世界
4dbf95875b Add headers option for HTTP outbound 2023-04-22 19:51:34 +08:00
世界
81c4312be8 URLTest improvements 2023-04-22 19:51:34 +08:00
世界
cc94dfaa4b Fix wireguard reconnect 2023-04-22 19:51:33 +08:00
世界
c6fc411164 Use HTTPS URLTest source 2023-04-22 19:51:33 +08:00
世界
be00e19162 clash-api: Add Clash.Meta APIs 2023-04-22 19:51:32 +08:00
世界
eb57cbc4ad clash api: download clash-dashboard if external-ui directory is empty 2023-04-22 19:51:31 +08:00
世界
f98cfdf5e4 Add multi-peer support for wireguard outbound 2023-04-22 19:51:31 +08:00
世界
cbf0099681 Add fakeip support 2023-04-22 19:51:30 +08:00
世界
a86afa0e5b Add L3 routing support 2023-04-22 19:51:30 +08:00
世界
b5d2062359 Add dns reverse mapping 2023-04-22 19:51:30 +08:00
501 changed files with 9823 additions and 17991 deletions

View File

@@ -1,79 +1,70 @@
name: Bug report name: Bug Report
description: "Report sing-box bug" description: "Create a report to help us improve."
body: body:
- type: dropdown - type: checkboxes
id: terms
attributes: attributes:
label: Operating system label: Welcome
description: Operating system type
options: options:
- iOS - label: Yes, I'm using the latest major release. Only such installations are supported.
- macOS required: true
- Apple tvOS - label: Yes, I'm using the latest Golang release. Only such installations are supported.
- Android required: true
- Windows - label: Yes, I've searched similar issues on GitHub and didn't find any.
- Linux required: true
- Others - label: Yes, I've included all information below (version, **FULL** config, **FULL** log, etc).
validations: required: true
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.
- type: textarea - type: textarea
id: problem
attributes: attributes:
label: Version label: Description of the problem
description: If you are using the original command line program, please provide the output of the `sing-box version` command. placeholder: Your problem description
validations:
required: true
- type: textarea
id: version
attributes:
label: Version of sing-box
value: |- value: |-
<details> <details>
```console ```console
# Replace this line with the output $ sing-box version
# Paste output here
``` ```
</details> </details>
- type: textarea
attributes:
label: Description
description: Please provide a detailed description of the error.
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: config
attributes: attributes:
label: Reproduction label: Server and client configuration file
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.
value: |- value: |-
<details> <details>
```console ```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: on:
push: push:
branches: branches:
- stable-next
- main-next - main-next
- dev-next - dev-next
paths-ignore: paths-ignore:
@@ -12,7 +11,6 @@ on:
- '!.github/workflows/debug.yml' - '!.github/workflows/debug.yml'
pull_request: pull_request:
branches: branches:
- stable-next
- main-next - main-next
- dev-next - dev-next
@@ -22,7 +20,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Get latest go version - name: Get latest go version
@@ -50,7 +48,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
@@ -64,27 +62,7 @@ jobs:
~/go/pkg/mod ~/go/pkg/mod
key: go118-${{ hashFiles('**/go.sum') }} key: go118-${{ hashFiles('**/go.sum') }}
- name: Run Test - name: Run Test
run: make ci_build_go118 run: make
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
cross: cross:
strategy: strategy:
matrix: matrix:
@@ -201,7 +179,7 @@ jobs:
TAGS: with_clash_api,with_quic TAGS: with_clash_api,with_quic
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Get latest go version - name: Get latest go version

View File

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

View File

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

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

1
.gitignore vendored
View File

@@ -1,7 +1,6 @@
/.idea/ /.idea/
/vendor/ /vendor/
/*.json /*.json
/*.srs
/*.db /*.db
/site/ /site/
/bin/ /bin/

View File

@@ -14,17 +14,13 @@ builds:
tags: tags:
- with_gvisor - with_gvisor
- with_quic - with_quic
- with_dhcp
- with_wireguard - with_wireguard
- with_ech
- with_utls - with_utls
- with_reality_server - with_reality_server
- with_acme
- with_clash_api - with_clash_api
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
targets: targets:
- linux_386
- linux_amd64_v1 - linux_amd64_v1
- linux_amd64_v3 - linux_amd64_v3
- linux_arm64 - linux_arm64
@@ -38,36 +34,6 @@ builds:
- darwin_amd64_v3 - darwin_amd64_v3
- darwin_arm64 - darwin_arm64
mod_timestamp: '{{ .CommitTimestamp }}' 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 - id: android
main: ./cmd/sing-box main: ./cmd/sing-box
flags: flags:
@@ -82,12 +48,8 @@ builds:
tags: tags:
- with_gvisor - with_gvisor
- with_quic - with_quic
- with_dhcp
- with_wireguard - with_wireguard
- with_ech
- with_utls - with_utls
- with_reality_server
- with_acme
- with_clash_api - with_clash_api
env: env:
- CGO_ENABLED=1 - CGO_ENABLED=1
@@ -124,9 +86,6 @@ snapshot:
name_template: "{{ .Version }}.{{ .ShortCommit }}" name_template: "{{ .Version }}.{{ .ShortCommit }}"
archives: archives:
- id: archive - id: archive
builds:
- main
- android
format: tar.gz format: tar.gz
format_overrides: format_overrides:
- goos: windows - goos: windows
@@ -135,17 +94,6 @@ archives:
files: files:
- LICENSE - LICENSE
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}' name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
- id: archive-legacy
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: nfpms:
- id: package - id: package
package_name: sing-box package_name: sing-box
@@ -158,7 +106,6 @@ nfpms:
formats: formats:
- deb - deb
- rpm - rpm
- archlinux
priority: extra priority: extra
contents: contents:
- src: release/config/config.json - src: release/config/config.json

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

140
Makefile
View File

@@ -1,34 +1,23 @@
NAME = sing-box NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD) COMMIT = $(shell git rev-parse --short HEAD)
TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_utls,with_reality_server,with_clash_api TAGS ?= with_gvisor,with_quic,with_wireguard,with_utls,with_reality_server,with_clash_api
TAGS_GO120 = with_quic,with_ech TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server,with_shadowsocksr
TAGS ?= $(TAGS_GO118),$(TAGS_GO120)
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
GOHOSTOS = $(shell go env GOHOSTOS) GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH) GOHOSTARCH = $(shell go env GOHOSTARCH)
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag) 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=" PARAMS = -v -trimpath -tags "$(TAGS)" -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
MAIN_PARAMS = $(PARAMS) -tags $(TAGS)
MAIN = ./cmd/sing-box MAIN = ./cmd/sing-box
PREFIX ?= $(shell go env GOPATH) PREFIX ?= $(shell go env GOPATH)
.PHONY: test release docs .PHONY: test release
build: build:
go build $(MAIN_PARAMS) $(MAIN)
ci_build_go118:
go build $(PARAMS) $(MAIN) go build $(PARAMS) $(MAIN)
go build $(PARAMS) -tags "$(TAGS_GO118)" $(MAIN)
ci_build:
go build $(PARAMS) $(MAIN)
go build $(MAIN_PARAMS) $(MAIN)
install: install:
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN) go build -o $(PREFIX)/bin/$(NAME) $(PARAMS) $(MAIN)
fmt: fmt:
@gofumpt -l -w . @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/protobuf/cmd/protoc-gen-go@latest
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
release: snapshot:
go run ./cmd/internal/build goreleaser release --clean --skip-publish || exit 1 go run ./cmd/internal/build goreleaser release --rm-dist --snapshot || exit 1
mkdir dist/release mkdir dist/release
mv dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/*.pkg.tar.zst dist/release mv dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/release
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release ghr --delete --draft --prerelease -p 1 nightly dist/release
rm -r dist/release rm -r dist
release:
go run ./cmd/internal/build goreleaser release --rm-dist --skip-publish || exit 1
mkdir 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: release_install:
go install -v github.com/goreleaser/goreleaser@latest go install -v github.com/goreleaser/goreleaser@latest
go install -v github.com/tcnksm/ghr@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: test:
@go test -v ./... && \ @go test -v ./... && \
cd test && \ cd test && \
@@ -167,10 +77,10 @@ test_stdio:
go mod tidy && \ go mod tidy && \
go test -v -tags "$(TAGS_TEST),force_stdio" . go test -v -tags "$(TAGS_TEST),force_stdio" .
lib_android: android:
go run ./cmd/internal/build_libbox -target android go run ./cmd/internal/build_libbox -target android
lib_ios: ios:
go run ./cmd/internal/build_libbox -target ios go run ./cmd/internal/build_libbox -target ios
lib: lib:
@@ -179,17 +89,9 @@ lib:
lib_install: lib_install:
go get -v -d 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/gomobile@v0.0.0-20230413023804-244d7ff07035
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20230915142329-c6740b6d2950 go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20230413023804-244d7ff07035
docs:
mkdocs serve
publish_docs:
mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
docs_install:
pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*"
clean: clean:
rm -rf bin dist sing-box rm -rf bin dist sing-box
rm -f $(shell go env GOPATH)/sing-box rm -f $(shell go env GOPATH)/sing-box

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

@@ -1,100 +1,29 @@
package adapter package adapter
import ( import (
"bytes"
"context" "context"
"encoding/binary"
"io"
"net" "net"
"time"
"github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/common/urltest"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/rw"
) )
type ClashServer interface { type ClashServer interface {
Service Service
PreStarter PreStarter
Mode() string Mode() string
ModeList() []string StoreSelected() bool
StoreFakeIP() bool
CacheFile() ClashCacheFile
HistoryStorage() *urltest.HistoryStorage HistoryStorage() *urltest.HistoryStorage
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker) RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker)
} }
type CacheFile interface { type ClashCacheFile interface {
Service
PreStarter
StoreFakeIP() bool
FakeIPStorage
LoadMode() string
StoreMode(mode string) error
LoadSelected(group string) string LoadSelected(group string) string
StoreSelected(group string, selected string) error StoreSelected(group string, selected string) error
LoadGroupExpand(group string) (isExpand bool, loaded bool) FakeIPStorage
StoreGroupExpand(group string, expand bool) error
LoadRuleSet(tag string) *SavedRuleSet
SaveRuleSet(tag string, set *SavedRuleSet) error
}
type SavedRuleSet struct {
Content []byte
LastUpdated time.Time
LastEtag string
}
func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
var buffer bytes.Buffer
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
if err != nil {
return nil, err
}
err = rw.WriteUVariant(&buffer, uint64(len(s.Content)))
if err != nil {
return nil, err
}
buffer.Write(s.Content)
err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix())
if err != nil {
return nil, err
}
err = rw.WriteVString(&buffer, s.LastEtag)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
reader := bytes.NewReader(data)
var version uint8
err := binary.Read(reader, binary.BigEndian, &version)
if err != nil {
return err
}
contentLen, err := rw.ReadUVariant(reader)
if err != nil {
return err
}
s.Content = make([]byte, contentLen)
_, err = io.ReadFull(reader, s.Content)
if err != nil {
return err
}
var lastUpdated int64
err = binary.Read(reader, binary.BigEndian, &lastUpdated)
if err != nil {
return err
}
s.LastUpdated = time.Unix(lastUpdated, 0)
s.LastEtag, err = rw.ReadVString(reader)
if err != nil {
return err
}
return nil
} }
type Tracker interface { type Tracker interface {
@@ -102,7 +31,6 @@ type Tracker interface {
} }
type OutboundGroup interface { type OutboundGroup interface {
Outbound
Now() string Now() string
All() []string All() []string
} }

View File

@@ -4,13 +4,12 @@ import (
"net/netip" "net/netip"
"github.com/sagernet/sing-dns" "github.com/sagernet/sing-dns"
"github.com/sagernet/sing/common/logger"
) )
type FakeIPStore interface { type FakeIPStore interface {
Service Service
Contains(address netip.Addr) bool Contains(address netip.Addr) bool
Create(domain string, isIPv6 bool) (netip.Addr, error) Create(domain string, strategy dns.DomainStrategy) (netip.Addr, error)
Lookup(address netip.Addr) (string, bool) Lookup(address netip.Addr) (string, bool)
Reset() error Reset() error
} }
@@ -18,15 +17,7 @@ type FakeIPStore interface {
type FakeIPStorage interface { type FakeIPStorage interface {
FakeIPMetadata() *FakeIPMetadata FakeIPMetadata() *FakeIPMetadata
FakeIPSaveMetadata(metadata *FakeIPMetadata) error FakeIPSaveMetadata(metadata *FakeIPMetadata) error
FakeIPSaveMetadataAsync(metadata *FakeIPMetadata)
FakeIPStore(address netip.Addr, domain string) error FakeIPStore(address netip.Addr, domain string) error
FakeIPStoreAsync(address netip.Addr, domain string, logger logger.Logger)
FakeIPLoad(address netip.Addr) (string, bool) FakeIPLoad(address netip.Addr) (string, bool)
FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bool)
FakeIPReset() error FakeIPReset() error
} }
type FakeIPTransport interface {
dns.Transport
Store() FakeIPStore
}

View File

@@ -46,8 +46,6 @@ type InboundContext struct {
SourceGeoIPCode string SourceGeoIPCode string
GeoIPCode string GeoIPCode string
ProcessInfo *process.Info ProcessInfo *process.Info
FakeIP bool
IPCIDRMatchSource bool
// dns cache // dns cache
@@ -76,11 +74,3 @@ func AppendContext(ctx context.Context) (context.Context, *InboundContext) {
metadata = new(InboundContext) metadata = new(InboundContext)
return WithContext(ctx, metadata), metadata 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

@@ -4,6 +4,7 @@ import (
"context" "context"
"net" "net"
"github.com/sagernet/sing-tun"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
@@ -13,8 +14,12 @@ type Outbound interface {
Type() string Type() string
Tag() string Tag() string
Network() []string Network() []string
Dependencies() []string
N.Dialer N.Dialer
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
} }
type IPOutbound interface {
Outbound
NewIPConnection(ctx context.Context, conn tun.RouteContext, metadata InboundContext) (tun.DirectDestination, error)
}

View File

@@ -4,6 +4,12 @@ type PreStarter interface {
PreStart() error PreStart() error
} }
type PostStarter interface { func PreStart(starter any) error {
PostStart() error if preService, ok := starter.(PreStarter); ok {
err := preService.PreStart()
if err != nil {
return err
}
}
return nil
} }

View File

@@ -2,13 +2,14 @@ package adapter
import ( import (
"context" "context"
"net"
"net/netip" "net/netip"
"github.com/sagernet/sing-box/common/geoip" "github.com/sagernet/sing-box/common/geoip"
"github.com/sagernet/sing-dns" "github.com/sagernet/sing-dns"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
"github.com/sagernet/sing/service" N "github.com/sagernet/sing/common/network"
mdns "github.com/miekg/dns" mdns "github.com/miekg/dns"
) )
@@ -18,21 +19,22 @@ type Router interface {
Outbounds() []Outbound Outbounds() []Outbound
Outbound(tag string) (Outbound, bool) Outbound(tag string) (Outbound, bool)
DefaultOutbound(network string) (Outbound, error) DefaultOutbound(network string) Outbound
FakeIPStore() FakeIPStore FakeIPStore() FakeIPStore
ConnectionRouter RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
RouteIPConnection(ctx context.Context, conn tun.RouteContext, metadata InboundContext) tun.RouteAction
NatRequired(outbound string) bool
GeoIPReader() *geoip.Reader GeoIPReader() *geoip.Reader
LoadGeosite(code string) (Rule, error) LoadGeosite(code string) (Rule, error)
RuleSet(tag string) (RuleSet, bool)
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error) Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error) LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
ClearDNSCache()
InterfaceFinder() control.InterfaceFinder InterfaceFinder() control.InterfaceFinder
UpdateInterfaces() error UpdateInterfaces() error
@@ -43,8 +45,11 @@ type Router interface {
NetworkMonitor() tun.NetworkUpdateMonitor NetworkMonitor() tun.NetworkUpdateMonitor
InterfaceMonitor() tun.DefaultInterfaceMonitor InterfaceMonitor() tun.DefaultInterfaceMonitor
PackageManager() tun.PackageManager PackageManager() tun.PackageManager
WIFIState() WIFIState
Rules() []Rule Rules() []Rule
IPRules() []IPRule
TimeService
ClashServer() ClashServer ClashServer() ClashServer
SetClashServer(server ClashServer) SetClashServer(server ClashServer)
@@ -55,23 +60,25 @@ type Router interface {
ResetNetwork() error ResetNetwork() error
} }
type routerContextKey struct{}
func ContextWithRouter(ctx context.Context, router Router) context.Context { 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 { func RouterFromContext(ctx context.Context) Router {
return service.FromContext[Router](ctx) metadata := ctx.Value((*routerContextKey)(nil))
} if metadata == nil {
return nil
type HeadlessRule interface { }
Match(metadata *InboundContext) bool return metadata.(Router)
} }
type Rule interface { type Rule interface {
HeadlessRule
Service Service
Type() string Type() string
UpdateGeosite() error UpdateGeosite() error
Match(metadata *InboundContext) bool
Outbound() string Outbound() string
String() string String() string
} }
@@ -82,16 +89,11 @@ type DNSRule interface {
RewriteTTL() *uint32 RewriteTTL() *uint32
} }
type RuleSet interface { type IPRule interface {
HeadlessRule Rule
Service Action() tun.ActionType
} }
type InterfaceUpdateListener interface { type InterfaceUpdateListener interface {
InterfaceUpdated() InterfaceUpdated() error
}
type WIFIState struct {
SSID string
BSSID string
} }

View File

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

104
box.go
View File

@@ -10,7 +10,6 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/experimental" "github.com/sagernet/sing-box/experimental"
"github.com/sagernet/sing-box/experimental/cachefile"
"github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/inbound" "github.com/sagernet/sing-box/inbound"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
@@ -20,8 +19,6 @@ import (
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/pause"
) )
var _ adapter.Service = (*Box)(nil) var _ adapter.Service = (*Box)(nil)
@@ -33,8 +30,7 @@ type Box struct {
outbounds []adapter.Outbound outbounds []adapter.Outbound
logFactory log.Factory logFactory log.Factory
logger log.ContextLogger logger log.ContextLogger
preServices1 map[string]adapter.Service preServices map[string]adapter.Service
preServices2 map[string]adapter.Service
postServices map[string]adapter.Service postServices map[string]adapter.Service
done chan struct{} done chan struct{}
} }
@@ -43,26 +39,19 @@ type Options struct {
option.Options option.Options
Context context.Context Context context.Context
PlatformInterface platform.Interface PlatformInterface platform.Interface
PlatformLogWriter log.PlatformWriter
} }
func New(options Options) (*Box, error) { func New(options Options) (*Box, error) {
createdAt := time.Now()
ctx := options.Context ctx := options.Context
if ctx == nil { if ctx == nil {
ctx = context.Background() ctx = context.Background()
} }
ctx = service.ContextWithDefaultRegistry(ctx) createdAt := time.Now()
ctx = pause.ContextWithDefaultManager(ctx)
experimentalOptions := common.PtrValueOrDefault(options.Experimental) experimentalOptions := common.PtrValueOrDefault(options.Experimental)
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug)) applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
var needCacheFile bool
var needClashAPI bool var needClashAPI bool
var needV2RayAPI bool var needV2RayAPI bool
if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil { if experimentalOptions.ClashAPI != nil && experimentalOptions.ClashAPI.ExternalController != "" {
needCacheFile = true
}
if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil {
needClashAPI = true needClashAPI = true
} }
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" { if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
@@ -78,7 +67,7 @@ func New(options Options) (*Box, error) {
Observable: needClashAPI, Observable: needClashAPI,
DefaultWriter: defaultLogWriter, DefaultWriter: defaultLogWriter,
BaseTime: createdAt, BaseTime: createdAt,
PlatformWriter: options.PlatformLogWriter, PlatformWriter: options.PlatformInterface,
}) })
if err != nil { if err != nil {
return nil, E.Cause(err, "create log factory") return nil, E.Cause(err, "create log factory")
@@ -151,31 +140,23 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize platform interface") return nil, E.Cause(err, "initialize platform interface")
} }
} }
preServices1 := make(map[string]adapter.Service) preServices := make(map[string]adapter.Service)
preServices2 := make(map[string]adapter.Service)
postServices := make(map[string]adapter.Service) postServices := make(map[string]adapter.Service)
if needCacheFile {
cacheFile := cachefile.NewCacheFile(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
preServices1["cache file"] = cacheFile
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
}
if needClashAPI { if needClashAPI {
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI) clashServer, err := experimental.NewClashServer(ctx, router, logFactory.(log.ObservableFactory), common.PtrValueOrDefault(options.Experimental.ClashAPI))
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
clashServer, err := experimental.NewClashServer(ctx, router, logFactory.(log.ObservableFactory), clashAPIOptions)
if err != nil { if err != nil {
return nil, E.Cause(err, "create clash api server") return nil, E.Cause(err, "create clash api server")
} }
router.SetClashServer(clashServer) router.SetClashServer(clashServer)
preServices2["clash api"] = clashServer preServices["clash api"] = clashServer
} }
if needV2RayAPI { if needV2RayAPI {
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI)) v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(options.Experimental.V2RayAPI))
if err != nil { if err != nil {
return nil, E.Cause(err, "create v2ray api server") return nil, E.Cause(err, "create v2ray api server")
} }
router.SetV2RayServer(v2rayServer) router.SetV2RayServer(v2rayServer)
preServices2["v2ray api"] = v2rayServer preServices["v2ray api"] = v2rayServer
} }
return &Box{ return &Box{
router: router, router: router,
@@ -184,8 +165,7 @@ func New(options Options) (*Box, error) {
createdAt: createdAt, createdAt: createdAt,
logFactory: logFactory, logFactory: logFactory,
logger: logFactory.Logger(), logger: logFactory.Logger(),
preServices1: preServices1, preServices: preServices,
preServices2: preServices2,
postServices: postServices, postServices: postServices,
done: make(chan struct{}), done: make(chan struct{}),
}, nil }, nil
@@ -230,28 +210,28 @@ func (s *Box) Start() error {
} }
func (s *Box) preStart() error { func (s *Box) preStart() error {
for serviceName, service := range s.preServices1 { for serviceName, service := range s.preServices {
if preService, isPreService := service.(adapter.PreStarter); isPreService { s.logger.Trace("pre-start ", serviceName)
s.logger.Trace("pre-start ", serviceName) err := adapter.PreStart(service)
err := preService.PreStart() if err != nil {
if err != nil { return E.Cause(err, "pre-starting ", serviceName)
return E.Cause(err, "pre-starting ", serviceName)
}
} }
} }
for serviceName, service := range s.preServices2 { for i, out := range s.outbounds {
if preService, isPreService := service.(adapter.PreStarter); isPreService { var tag string
s.logger.Trace("pre-start ", serviceName) if out.Tag() == "" {
err := preService.PreStart() tag = F.ToString(i)
} else {
tag = out.Tag()
}
if starter, isStarter := out.(common.Starter); isStarter {
s.logger.Trace("initializing outbound/", out.Type(), "[", tag, "]")
err := starter.Start()
if err != nil { if err != nil {
return E.Cause(err, "pre-starting ", serviceName) return E.Cause(err, "initialize outbound/", out.Type(), "[", tag, "]")
} }
} }
} }
err := s.startOutbounds()
if err != nil {
return err
}
return s.router.Start() return s.router.Start()
} }
@@ -260,14 +240,7 @@ func (s *Box) start() error {
if err != nil { if err != nil {
return err return err
} }
for serviceName, service := range s.preServices1 { for serviceName, service := range s.preServices {
s.logger.Trace("starting ", serviceName)
err = service.Start()
if err != nil {
return E.Cause(err, "start ", serviceName)
}
}
for serviceName, service := range s.preServices2 {
s.logger.Trace("starting ", serviceName) s.logger.Trace("starting ", serviceName)
err = service.Start() err = service.Start()
if err != nil { if err != nil {
@@ -287,26 +260,13 @@ func (s *Box) start() error {
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]") return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
} }
} }
return nil
}
func (s *Box) postStart() error {
for serviceName, service := range s.postServices { for serviceName, service := range s.postServices {
s.logger.Trace("starting ", service) s.logger.Trace("starting ", service)
err := service.Start() err = service.Start()
if err != nil { if err != nil {
return E.Cause(err, "start ", serviceName) return E.Cause(err, "start ", serviceName)
} }
} }
for serviceName, service := range s.outbounds {
if lateService, isLateService := service.(adapter.PostStarter); isLateService {
s.logger.Trace("post-starting ", service)
err := lateService.PostStart()
if err != nil {
return E.Cause(err, "post-start ", serviceName)
}
}
}
return nil return nil
} }
@@ -342,13 +302,7 @@ func (s *Box) Close() error {
return E.Cause(err, "close router") return E.Cause(err, "close router")
}) })
} }
for serviceName, service := range s.preServices1 { for serviceName, service := range s.preServices {
s.logger.Trace("closing ", serviceName)
errors = E.Append(errors, service.Close(), func(err error) error {
return E.Cause(err, "close ", serviceName)
})
}
for serviceName, service := range s.preServices2 {
s.logger.Trace("closing ", serviceName) s.logger.Trace("closing ", serviceName)
errors = E.Append(errors, service.Close(), func(err error) error { errors = E.Append(errors, service.Close(), func(err error) error {
return E.Cause(err, "close ", serviceName) return E.Cause(err, "close ", serviceName)

View File

@@ -1,79 +0,0 @@
package box
import (
"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"
)
func (s *Box) startOutbounds() error {
outboundTags := make(map[adapter.Outbound]string)
outbounds := make(map[string]adapter.Outbound)
for i, outboundToStart := range s.outbounds {
var outboundTag string
if outboundToStart.Tag() == "" {
outboundTag = F.ToString(i)
} else {
outboundTag = outboundToStart.Tag()
}
if _, exists := outbounds[outboundTag]; exists {
return E.New("outbound tag ", outboundTag, " duplicated")
}
outboundTags[outboundToStart] = outboundTag
outbounds[outboundTag] = outboundToStart
}
started := make(map[string]bool)
for {
canContinue := false
startOne:
for _, outboundToStart := range s.outbounds {
outboundTag := outboundTags[outboundToStart]
if started[outboundTag] {
continue
}
dependencies := outboundToStart.Dependencies()
for _, dependency := range dependencies {
if !started[dependency] {
continue startOne
}
}
started[outboundTag] = true
canContinue = true
if starter, isStarter := outboundToStart.(common.Starter); isStarter {
s.logger.Trace("initializing outbound/", outboundToStart.Type(), "[", outboundTag, "]")
err := starter.Start()
if err != nil {
return E.Cause(err, "initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
}
}
}
if len(started) == len(s.outbounds) {
break
}
if canContinue {
continue
}
currentOutbound := common.Find(s.outbounds, func(it adapter.Outbound) bool {
return !started[outboundTags[it]]
})
var lintOutbound func(oTree []string, oCurrent adapter.Outbound) error
lintOutbound = func(oTree []string, oCurrent adapter.Outbound) error {
problemOutboundTag := common.Find(oCurrent.Dependencies(), func(it string) bool {
return !started[it]
})
if common.Contains(oTree, problemOutboundTag) {
return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag)
}
problemOutbound := outbounds[problemOutboundTag]
if problemOutbound == nil {
return E.New("dependency[", problemOutboundTag, "] not found for outbound[", outboundTags[oCurrent], "]")
}
return lintOutbound(append(oTree, problemOutboundTag), problemOutbound)
}
return lintOutbound([]string{outboundTags[currentOutbound]}, currentOutbound)
}
return nil
}

View File

@@ -1,7 +1,6 @@
package main package main
import ( import (
"go/build"
"os" "os"
"os/exec" "os/exec"
@@ -12,10 +11,6 @@ import (
func main() { func main() {
build_shared.FindSDK() build_shared.FindSDK()
if os.Getenv("GOPATH") == "" {
os.Setenv("GOPATH", build.Default.GOPATH)
}
command := exec.Command(os.Args[1], os.Args[2:]...) command := exec.Command(os.Args[1], os.Args[2:]...)
command.Stdout = os.Stdout command.Stdout = os.Stdout
command.Stderr = os.Stderr command.Stderr = os.Stderr

View File

@@ -40,7 +40,6 @@ var (
sharedFlags []string sharedFlags []string
debugFlags []string debugFlags []string
sharedTags []string sharedTags []string
iosTags []string
debugTags []string debugTags []string
) )
@@ -54,8 +53,8 @@ func init() {
sharedFlags = append(sharedFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=") 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) 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") sharedTags = append(sharedTags, "test_sing_shadowsocks2")
debugTags = append(debugTags, "debug") debugTags = append(debugTags, "debug")
} }
@@ -107,7 +106,7 @@ func buildiOS() {
args := []string{ args := []string{
"bind", "bind",
"-v", "-v",
"-target", "ios,iossimulator,tvos,tvossimulator,macos", "-target", "ios,iossimulator,macos",
"-libname=box", "-libname=box",
} }
if !debugEnabled { if !debugEnabled {
@@ -116,7 +115,7 @@ func buildiOS() {
args = append(args, debugFlags...) args = append(args, debugFlags...)
} }
tags := append(sharedTags, iosTags...) tags := append(sharedTags, "with_low_memory", "with_conntrack")
args = append(args, "-tags") args = append(args, "-tags")
if !debugEnabled { if !debugEnabled {
args = append(args, strings.Join(tags, ",")) args = append(args, strings.Join(tags, ","))
@@ -133,7 +132,7 @@ func buildiOS() {
log.Fatal(err) log.Fatal(err)
} }
copyPath := filepath.Join("..", "sing-box-for-apple") copyPath := filepath.Join("..", "sing-box-for-ios")
if rw.FileExists(copyPath) { if rw.FileExists(copyPath) {
targetDir := filepath.Join(copyPath, "Libbox.xcframework") targetDir := filepath.Join(copyPath, "Libbox.xcframework")
targetDir, _ = filepath.Abs(targetDir) targetDir, _ = filepath.Abs(targetDir)

View File

@@ -1,10 +1,6 @@
package build_shared package build_shared
import ( import "github.com/sagernet/sing/common/shell"
"github.com/sagernet/sing-box/common/badversion"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/shell"
)
func ReadTag() (string, error) { func ReadTag() (string, error) {
currentTag, err := shell.Exec("git", "describe", "--tags").ReadOutput() currentTag, err := shell.Exec("git", "describe", "--tags").ReadOutput()
@@ -16,18 +12,5 @@ func ReadTag() (string, error) {
return currentTag[1:], nil return currentTag[1:], nil
} }
shortCommit, _ := shell.Exec("git", "rev-parse", "--short", "HEAD").ReadOutput() shortCommit, _ := shell.Exec("git", "rev-parse", "--short", "HEAD").ReadOutput()
version := badversion.Parse(currentTagRev[1:]) return currentTagRev[1:] + "-" + shortCommit, nil
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

@@ -7,6 +7,7 @@ import (
"github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/common/json"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -68,3 +69,41 @@ func format() error {
} }
return nil return nil
} }
func formatOne(configPath string) error {
configContent, err := os.ReadFile(configPath)
if err != nil {
return E.Cause(err, "read config")
}
var options option.Options
err = options.UnmarshalJSON(configContent)
if err != nil {
return E.Cause(err, "decode config")
}
buffer := new(bytes.Buffer)
encoder := json.NewEncoder(buffer)
encoder.SetIndent("", " ")
err = encoder.Encode(options)
if err != nil {
return E.Cause(err, "encode config")
}
if !commandFormatFlagWrite {
os.Stdout.WriteString(buffer.String() + "\n")
return nil
}
if bytes.Equal(configContent, buffer.Bytes()) {
return nil
}
output, err := os.Create(configPath)
if err != nil {
return E.Cause(err, "open output")
}
_, err = output.Write(buffer.Bytes())
output.Close()
if err != nil {
return E.Cause(err, "write output")
}
outputPath, _ := filepath.Abs(configPath)
os.Stderr.WriteString(outputPath + "\n")
return nil
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/gofrs/uuid/v5" "github.com/gofrs/uuid/v5"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
) )
var commandGenerate = &cobra.Command{ var commandGenerate = &cobra.Command{
@@ -21,7 +22,8 @@ var commandGenerate = &cobra.Command{
func init() { func init() {
commandGenerate.AddCommand(commandGenerateUUID) commandGenerate.AddCommand(commandGenerateUUID)
commandGenerate.AddCommand(commandGenerateRandom) commandGenerate.AddCommand(commandGenerateRandom)
commandGenerate.AddCommand(commandGenerateWireGuardKeyPair)
commandGenerate.AddCommand(commandGenerateRealityKeyPair)
mainCommand.AddCommand(commandGenerate) mainCommand.AddCommand(commandGenerate)
} }
@@ -90,3 +92,48 @@ func generateUUID() error {
_, err = os.Stdout.WriteString(newUUID.String() + "\n") _, err = os.Stdout.WriteString(newUUID.String() + "\n")
return err 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,43 +0,0 @@
package main
import (
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
"github.com/oschwald/maxminddb-golang"
"github.com/spf13/cobra"
)
var (
geoipReader *maxminddb.Reader
commandGeoIPFlagFile string
)
var commandGeoip = &cobra.Command{
Use: "geoip",
Short: "GeoIP tools",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
err := geoipPreRun()
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGeoip.PersistentFlags().StringVarP(&commandGeoIPFlagFile, "file", "f", "geoip.db", "geoip file")
mainCommand.AddCommand(commandGeoip)
}
func geoipPreRun() error {
reader, err := maxminddb.Open(commandGeoIPFlagFile)
if err != nil {
return err
}
if reader.Metadata.DatabaseType != "sing-geoip" {
reader.Close()
return E.New("incorrect database type, expected sing-geoip, got ", reader.Metadata.DatabaseType)
}
geoipReader = reader
return nil
}

View File

@@ -1,98 +0,0 @@
package main
import (
"io"
"net"
"os"
"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"
E "github.com/sagernet/sing/common/exceptions"
"github.com/oschwald/maxminddb-golang"
"github.com/spf13/cobra"
)
var flagGeoipExportOutput string
const flagGeoipExportDefaultOutput = "geoip-<country>.srs"
var commandGeoipExport = &cobra.Command{
Use: "export <country>",
Short: "Export geoip country as rule-set",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := geoipExport(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGeoipExport.Flags().StringVarP(&flagGeoipExportOutput, "output", "o", flagGeoipExportDefaultOutput, "Output path")
commandGeoip.AddCommand(commandGeoipExport)
}
func geoipExport(countryCode string) error {
networks := geoipReader.Networks(maxminddb.SkipAliasedNetworks)
countryMap := make(map[string][]*net.IPNet)
var (
ipNet *net.IPNet
nextCountryCode string
err error
)
for networks.Next() {
ipNet, err = networks.Network(&nextCountryCode)
if err != nil {
return err
}
countryMap[nextCountryCode] = append(countryMap[nextCountryCode], ipNet)
}
ipNets := countryMap[strings.ToLower(countryCode)]
if len(ipNets) == 0 {
return E.New("country code not found: ", countryCode)
}
var (
outputFile *os.File
outputWriter io.Writer
)
if flagGeoipExportOutput == "stdout" {
outputWriter = os.Stdout
} else if flagGeoipExportOutput == flagGeoipExportDefaultOutput {
outputFile, err = os.Create("geoip-" + countryCode + ".json")
if err != nil {
return err
}
defer outputFile.Close()
outputWriter = outputFile
} else {
outputFile, err = os.Create(flagGeoipExportOutput)
if err != nil {
return err
}
defer outputFile.Close()
outputWriter = outputFile
}
encoder := json.NewEncoder(outputWriter)
encoder.SetIndent("", " ")
var headlessRule option.DefaultHeadlessRule
headlessRule.IPCIDR = make([]string, 0, len(ipNets))
for _, cidr := range ipNets {
headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String())
}
var plainRuleSet option.PlainRuleSetCompat
plainRuleSet.Version = C.RuleSetVersion1
plainRuleSet.Options.Rules = []option.HeadlessRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: headlessRule,
},
}
return encoder.Encode(plainRuleSet)
}

View File

@@ -1,31 +0,0 @@
package main
import (
"os"
"github.com/sagernet/sing-box/log"
"github.com/spf13/cobra"
)
var commandGeoipList = &cobra.Command{
Use: "list",
Short: "List geoip country codes",
Run: func(cmd *cobra.Command, args []string) {
err := listGeoip()
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGeoip.AddCommand(commandGeoipList)
}
func listGeoip() error {
for _, code := range geoipReader.Metadata.Languages {
os.Stdout.WriteString(code + "\n")
}
return nil
}

View File

@@ -1,47 +0,0 @@
package main
import (
"net/netip"
"os"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
"github.com/spf13/cobra"
)
var commandGeoipLookup = &cobra.Command{
Use: "lookup <address>",
Short: "Lookup if an IP address is contained in the GeoIP database",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := geoipLookup(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGeoip.AddCommand(commandGeoipLookup)
}
func geoipLookup(address string) error {
addr, err := netip.ParseAddr(address)
if err != nil {
return E.Cause(err, "parse address")
}
if !N.IsPublicAddr(addr) {
os.Stdout.WriteString("private\n")
return nil
}
var code string
_ = geoipReader.Lookup(addr.AsSlice(), &code)
if code != "" {
os.Stdout.WriteString(code + "\n")
return nil
}
os.Stdout.WriteString("unknown\n")
return nil
}

View File

@@ -1,41 +0,0 @@
package main
import (
"github.com/sagernet/sing-box/common/geosite"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
"github.com/spf13/cobra"
)
var (
commandGeoSiteFlagFile string
geositeReader *geosite.Reader
geositeCodeList []string
)
var commandGeoSite = &cobra.Command{
Use: "geosite",
Short: "Geosite tools",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
err := geositePreRun()
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGeoSite.PersistentFlags().StringVarP(&commandGeoSiteFlagFile, "file", "f", "geosite.db", "geosite file")
mainCommand.AddCommand(commandGeoSite)
}
func geositePreRun() error {
reader, codeList, err := geosite.Open(commandGeoSiteFlagFile)
if err != nil {
return E.Cause(err, "open geosite file")
}
geositeReader = reader
geositeCodeList = codeList
return nil
}

View File

@@ -1,81 +0,0 @@
package main
import (
"encoding/json"
"io"
"os"
"github.com/sagernet/sing-box/common/geosite"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/spf13/cobra"
)
var commandGeositeExportOutput string
const commandGeositeExportDefaultOutput = "geosite-<category>.json"
var commandGeositeExport = &cobra.Command{
Use: "export <category>",
Short: "Export geosite category as rule-set",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := geositeExport(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGeositeExport.Flags().StringVarP(&commandGeositeExportOutput, "output", "o", commandGeositeExportDefaultOutput, "Output path")
commandGeoSite.AddCommand(commandGeositeExport)
}
func geositeExport(category string) error {
sourceSet, err := geositeReader.Read(category)
if err != nil {
return err
}
var (
outputFile *os.File
outputWriter io.Writer
)
if commandGeositeExportOutput == "stdout" {
outputWriter = os.Stdout
} else if commandGeositeExportOutput == commandGeositeExportDefaultOutput {
outputFile, err = os.Create("geosite-" + category + ".json")
if err != nil {
return err
}
defer outputFile.Close()
outputWriter = outputFile
} else {
outputFile, err = os.Create(commandGeositeExportOutput)
if err != nil {
return err
}
defer outputFile.Close()
outputWriter = outputFile
}
encoder := json.NewEncoder(outputWriter)
encoder.SetIndent("", " ")
var headlessRule option.DefaultHeadlessRule
defaultRule := geosite.Compile(sourceSet)
headlessRule.Domain = defaultRule.Domain
headlessRule.DomainSuffix = defaultRule.DomainSuffix
headlessRule.DomainKeyword = defaultRule.DomainKeyword
headlessRule.DomainRegex = defaultRule.DomainRegex
var plainRuleSet option.PlainRuleSetCompat
plainRuleSet.Version = C.RuleSetVersion1
plainRuleSet.Options.Rules = []option.HeadlessRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: headlessRule,
},
}
return encoder.Encode(plainRuleSet)
}

View File

@@ -1,50 +0,0 @@
package main
import (
"os"
"sort"
"github.com/sagernet/sing-box/log"
F "github.com/sagernet/sing/common/format"
"github.com/spf13/cobra"
)
var commandGeositeList = &cobra.Command{
Use: "list <category>",
Short: "List geosite categories",
Run: func(cmd *cobra.Command, args []string) {
err := geositeList()
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGeoSite.AddCommand(commandGeositeList)
}
func geositeList() error {
var geositeEntry []struct {
category string
items int
}
for _, category := range geositeCodeList {
sourceSet, err := geositeReader.Read(category)
if err != nil {
return err
}
geositeEntry = append(geositeEntry, struct {
category string
items int
}{category, len(sourceSet)})
}
sort.SliceStable(geositeEntry, func(i, j int) bool {
return geositeEntry[i].items < geositeEntry[j].items
})
for _, entry := range geositeEntry {
os.Stdout.WriteString(F.ToString(entry.category, " (", entry.items, ")\n"))
}
return nil
}

View File

@@ -1,97 +0,0 @@
package main
import (
"os"
"sort"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
"github.com/spf13/cobra"
)
var commandGeositeLookup = &cobra.Command{
Use: "lookup [category] <domain>",
Short: "Check if a domain is in the geosite",
Args: cobra.RangeArgs(1, 2),
Run: func(cmd *cobra.Command, args []string) {
var (
source string
target string
)
switch len(args) {
case 1:
target = args[0]
case 2:
source = args[0]
target = args[1]
}
err := geositeLookup(source, target)
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGeoSite.AddCommand(commandGeositeLookup)
}
func geositeLookup(source string, target string) error {
var sourceMatcherList []struct {
code string
matcher *searchGeositeMatcher
}
if source != "" {
sourceSet, err := geositeReader.Read(source)
if err != nil {
return err
}
sourceMatcher, err := newSearchGeositeMatcher(sourceSet)
if err != nil {
return E.Cause(err, "compile code: "+source)
}
sourceMatcherList = []struct {
code string
matcher *searchGeositeMatcher
}{
{
code: source,
matcher: sourceMatcher,
},
}
} else {
for _, code := range geositeCodeList {
sourceSet, err := geositeReader.Read(code)
if err != nil {
return err
}
sourceMatcher, err := newSearchGeositeMatcher(sourceSet)
if err != nil {
return E.Cause(err, "compile code: "+code)
}
sourceMatcherList = append(sourceMatcherList, struct {
code string
matcher *searchGeositeMatcher
}{
code: code,
matcher: sourceMatcher,
})
}
}
sort.SliceStable(sourceMatcherList, func(i, j int) bool {
return sourceMatcherList[i].code < sourceMatcherList[j].code
})
for _, matcherItem := range sourceMatcherList {
if matchRule := matcherItem.matcher.Match(target); matchRule != "" {
os.Stdout.WriteString("Match code (")
os.Stdout.WriteString(matcherItem.code)
os.Stdout.WriteString(") ")
os.Stdout.WriteString(matchRule)
os.Stdout.WriteString("\n")
}
}
return nil
}

View File

@@ -1,56 +0,0 @@
package main
import (
"regexp"
"strings"
"github.com/sagernet/sing-box/common/geosite"
)
type searchGeositeMatcher struct {
domainMap map[string]bool
suffixList []string
keywordList []string
regexList []string
}
func newSearchGeositeMatcher(items []geosite.Item) (*searchGeositeMatcher, error) {
options := geosite.Compile(items)
domainMap := make(map[string]bool)
for _, domain := range options.Domain {
domainMap[domain] = true
}
rule := &searchGeositeMatcher{
domainMap: domainMap,
suffixList: options.DomainSuffix,
keywordList: options.DomainKeyword,
regexList: options.DomainRegex,
}
return rule, nil
}
func (r *searchGeositeMatcher) Match(domain string) string {
if r.domainMap[domain] {
return "domain=" + domain
}
for _, suffix := range r.suffixList {
if strings.HasSuffix(domain, suffix) {
return "domain_suffix=" + suffix
}
}
for _, keyword := range r.keywordList {
if strings.Contains(domain, keyword) {
return "domain_keyword=" + keyword
}
}
for _, regexStr := range r.regexList {
regex, err := regexp.Compile(regexStr)
if err != nil {
continue
}
if regex.MatchString(domain) {
return "domain_regex=" + regexStr
}
}
return ""
}

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

@@ -1,14 +0,0 @@
package main
import (
"github.com/spf13/cobra"
)
var commandRuleSet = &cobra.Command{
Use: "rule-set",
Short: "Manage rule sets",
}
func init() {
mainCommand.AddCommand(commandRuleSet)
}

View File

@@ -1,80 +0,0 @@
package main
import (
"io"
"os"
"strings"
"github.com/sagernet/sing-box/common/json"
"github.com/sagernet/sing-box/common/srs"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/spf13/cobra"
)
var flagRuleSetCompileOutput string
const flagRuleSetCompileDefaultOutput = "<file_name>.srs"
var commandRuleSetCompile = &cobra.Command{
Use: "compile [source-path]",
Short: "Compile rule-set json to binary",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := compileRuleSet(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandRuleSet.AddCommand(commandRuleSetCompile)
commandRuleSetCompile.Flags().StringVarP(&flagRuleSetCompileOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file")
}
func compileRuleSet(sourcePath string) error {
var (
reader io.Reader
err error
)
if sourcePath == "stdin" {
reader = os.Stdin
} else {
reader, err = os.Open(sourcePath)
if err != nil {
return err
}
}
decoder := json.NewDecoder(json.NewCommentFilter(reader))
decoder.DisallowUnknownFields()
var plainRuleSet option.PlainRuleSetCompat
err = decoder.Decode(&plainRuleSet)
if err != nil {
return err
}
ruleSet := plainRuleSet.Upgrade()
var outputPath string
if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".json") {
outputPath = sourcePath[:len(sourcePath)-5] + ".srs"
} else {
outputPath = sourcePath + ".srs"
}
} else {
outputPath = flagRuleSetCompileOutput
}
outputFile, err := os.Create(outputPath)
if err != nil {
return err
}
err = srs.Write(outputFile, ruleSet)
if err != nil {
outputFile.Close()
os.Remove(outputPath)
return err
}
outputFile.Close()
return nil
}

View File

@@ -1,87 +0,0 @@
package main
import (
"bytes"
"io"
"os"
"path/filepath"
"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/spf13/cobra"
)
var commandRuleSetFormatFlagWrite bool
var commandRuleSetFormat = &cobra.Command{
Use: "format <source-path>",
Short: "Format rule-set json",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := formatRuleSet(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandRuleSetFormat.Flags().BoolVarP(&commandRuleSetFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout")
commandRuleSet.AddCommand(commandRuleSetFormat)
}
func formatRuleSet(sourcePath string) error {
var (
reader io.Reader
err error
)
if sourcePath == "stdin" {
reader = os.Stdin
} else {
reader, err = os.Open(sourcePath)
if err != nil {
return err
}
}
content, err := io.ReadAll(reader)
if err != nil {
return err
}
decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content)))
decoder.DisallowUnknownFields()
var plainRuleSet option.PlainRuleSetCompat
err = decoder.Decode(&plainRuleSet)
if err != nil {
return err
}
ruleSet := plainRuleSet.Upgrade()
buffer := new(bytes.Buffer)
encoder := json.NewEncoder(buffer)
encoder.SetIndent("", " ")
err = encoder.Encode(ruleSet)
if err != nil {
return E.Cause(err, "encode config")
}
outputPath, _ := filepath.Abs(sourcePath)
if !commandRuleSetFormatFlagWrite || sourcePath == "stdin" {
os.Stdout.WriteString(buffer.String() + "\n")
return nil
}
if bytes.Equal(content, buffer.Bytes()) {
return nil
}
output, err := os.Create(sourcePath)
if err != nil {
return E.Cause(err, "open output")
}
_, err = output.Write(buffer.Bytes())
output.Close()
if err != nil {
return E.Cause(err, "write output")
}
os.Stderr.WriteString(outputPath + "\n")
return nil
}

View File

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

View File

@@ -38,7 +38,11 @@ func createPreStartedClient() (*box.Box, error) {
func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) { func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) {
if outboundTag == "" { if outboundTag == "" {
return instance.Router().DefaultOutbound(N.NetworkName(network)) outbound := instance.Router().DefaultOutbound(N.NetworkName(network))
if outbound == nil {
return nil, E.New("missing default outbound")
}
return outbound, nil
} else { } else {
outbound, loaded := instance.Router().Outbound(outboundTag) outbound, loaded := instance.Router().Outbound(outboundTag)
if !loaded { if !loaded {

View File

@@ -18,7 +18,7 @@ import (
var commandConnectFlagNetwork string var commandConnectFlagNetwork string
var commandConnect = &cobra.Command{ var commandConnect = &cobra.Command{
Use: "connect <address>", Use: "connect [address]",
Short: "Connect to an address", Short: "Connect to an address",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {

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 with error code 0") {
return net.ErrClosed
}
return err
}

View File

@@ -5,10 +5,8 @@ package badtls
import ( import (
"crypto/tls" "crypto/tls"
"os" "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 return nil, os.ErrInvalid
} }

View File

@@ -11,7 +11,6 @@ type Version struct {
Major int Major int
Minor int Minor int
Patch int Patch int
Commit string
PreReleaseIdentifier string PreReleaseIdentifier string
PreReleaseVersion int PreReleaseVersion int
} }
@@ -38,29 +37,20 @@ func (v Version) After(anotherVersion Version) bool {
return false return false
} }
if v.PreReleaseIdentifier != "" && anotherVersion.PreReleaseIdentifier != "" { if v.PreReleaseIdentifier != "" && anotherVersion.PreReleaseIdentifier != "" {
if v.PreReleaseIdentifier == anotherVersion.PreReleaseIdentifier { if v.PreReleaseIdentifier == "beta" && anotherVersion.PreReleaseIdentifier == "alpha" {
if v.PreReleaseVersion > anotherVersion.PreReleaseVersion {
return true
} else if v.PreReleaseVersion < anotherVersion.PreReleaseVersion {
return false
}
} else if v.PreReleaseIdentifier == "rc" && anotherVersion.PreReleaseIdentifier == "beta" {
return true
} else if v.PreReleaseIdentifier == "beta" && anotherVersion.PreReleaseIdentifier == "rc" {
return false
} else if v.PreReleaseIdentifier == "beta" && anotherVersion.PreReleaseIdentifier == "alpha" {
return true return true
} else if v.PreReleaseIdentifier == "alpha" && anotherVersion.PreReleaseIdentifier == "beta" { } else if v.PreReleaseIdentifier == "alpha" && anotherVersion.PreReleaseIdentifier == "beta" {
return false return false
} }
if v.PreReleaseVersion > anotherVersion.PreReleaseVersion {
return true
} else if v.PreReleaseVersion < anotherVersion.PreReleaseVersion {
return false
}
} }
return false return false
} }
func (v Version) VersionString() string {
return F.ToString(v.Major, ".", v.Minor, ".", v.Patch)
}
func (v Version) String() string { func (v Version) String() string {
version := F.ToString(v.Major, ".", v.Minor, ".", v.Patch) version := F.ToString(v.Major, ".", v.Minor, ".", v.Patch)
if v.PreReleaseIdentifier != "" { if v.PreReleaseIdentifier != "" {
@@ -105,7 +95,7 @@ func Parse(versionName string) (version Version) {
version.PreReleaseIdentifier = "beta" version.PreReleaseIdentifier = "beta"
version.PreReleaseVersion, _ = strconv.Atoi(identifier[4:]) version.PreReleaseVersion, _ = strconv.Atoi(identifier[4:])
} else { } else {
version.Commit = identifier version.PreReleaseIdentifier = identifier
} }
} }
} }

87
common/debugio/log.go Normal file
View File

@@ -0,0 +1,87 @@
package debugio
import (
"net"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type LogConn struct {
N.ExtendedConn
logger log.Logger
prefix string
}
func NewLogConn(conn net.Conn, logger log.Logger, prefix string) N.ExtendedConn {
return &LogConn{bufio.NewExtendedConn(conn), logger, prefix}
}
func (c *LogConn) Read(p []byte) (n int, err error) {
n, err = c.ExtendedConn.Read(p)
if n > 0 {
c.logger.Debug(c.prefix, " read ", buf.EncodeHexString(p[:n]))
}
return
}
func (c *LogConn) Write(p []byte) (n int, err error) {
c.logger.Debug(c.prefix, " write ", buf.EncodeHexString(p))
return c.ExtendedConn.Write(p)
}
func (c *LogConn) ReadBuffer(buffer *buf.Buffer) error {
err := c.ExtendedConn.ReadBuffer(buffer)
if err == nil {
c.logger.Debug(c.prefix, " read buffer ", buf.EncodeHexString(buffer.Bytes()))
}
return err
}
func (c *LogConn) WriteBuffer(buffer *buf.Buffer) error {
c.logger.Debug(c.prefix, " write buffer ", buf.EncodeHexString(buffer.Bytes()))
return c.ExtendedConn.WriteBuffer(buffer)
}
func (c *LogConn) Upstream() any {
return c.ExtendedConn
}
type LogPacketConn struct {
N.NetPacketConn
logger log.Logger
prefix string
}
func NewLogPacketConn(conn net.PacketConn, logger log.Logger, prefix string) N.NetPacketConn {
return &LogPacketConn{bufio.NewPacketConn(conn), logger, prefix}
}
func (c *LogPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
n, addr, err = c.NetPacketConn.ReadFrom(p)
if n > 0 {
c.logger.Debug(c.prefix, " read from ", addr, " ", buf.EncodeHexString(p[:n]))
}
return
}
func (c *LogPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
c.logger.Debug(c.prefix, " write to ", addr, " ", buf.EncodeHexString(p))
return c.NetPacketConn.WriteTo(p, addr)
}
func (c *LogPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
destination, err = c.NetPacketConn.ReadPacket(buffer)
if err == nil {
c.logger.Debug(c.prefix, " read packet from ", destination, " ", buf.EncodeHexString(buffer.Bytes()))
}
return
}
func (c *LogPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
c.logger.Debug(c.prefix, " write packet to ", destination, " ", buf.EncodeHexString(buffer.Bytes()))
return c.NetPacketConn.WritePacket(buffer, destination)
}

19
common/debugio/print.go Normal file
View File

@@ -0,0 +1,19 @@
package debugio
import (
"fmt"
"reflect"
"github.com/sagernet/sing/common"
)
func PrintUpstream(obj any) {
for obj != nil {
fmt.Println(reflect.TypeOf(obj))
if u, ok := obj.(common.WithUpstream); !ok {
break
} else {
obj = u.Upstream()
}
}
}

48
common/debugio/race.go Normal file
View File

@@ -0,0 +1,48 @@
package debugio
import (
"net"
"sync"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
N "github.com/sagernet/sing/common/network"
)
type RaceConn struct {
N.ExtendedConn
readAccess sync.Mutex
writeAccess sync.Mutex
}
func NewRaceConn(conn net.Conn) N.ExtendedConn {
return &RaceConn{ExtendedConn: bufio.NewExtendedConn(conn)}
}
func (c *RaceConn) Read(p []byte) (n int, err error) {
c.readAccess.Lock()
defer c.readAccess.Unlock()
return c.ExtendedConn.Read(p)
}
func (c *RaceConn) Write(p []byte) (n int, err error) {
c.writeAccess.Lock()
defer c.writeAccess.Unlock()
return c.ExtendedConn.Write(p)
}
func (c *RaceConn) ReadBuffer(buffer *buf.Buffer) error {
c.readAccess.Lock()
defer c.readAccess.Unlock()
return c.ExtendedConn.ReadBuffer(buffer)
}
func (c *RaceConn) WriteBuffer(buffer *buf.Buffer) error {
c.writeAccess.Lock()
defer c.writeAccess.Unlock()
return c.ExtendedConn.WriteBuffer(buffer)
}
func (c *RaceConn) Upstream() any {
return c.ExtendedConn
}

View File

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

View File

@@ -1,20 +1,20 @@
package conntrack package conntrack
import ( import (
"runtime"
runtimeDebug "runtime/debug" runtimeDebug "runtime/debug"
"time" "time"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/memory"
) )
var ( var (
KillerEnabled bool KillerEnabled bool
MemoryLimit uint64 MemoryLimit int64
killerLastCheck time.Time killerLastCheck time.Time
) )
func KillerCheck() error { func killerCheck() error {
if !KillerEnabled { if !KillerEnabled {
return nil return nil
} }
@@ -23,7 +23,10 @@ func KillerCheck() error {
return nil return nil
} }
killerLastCheck = nowTime 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() Close()
go func() { go func() {
time.Sleep(time.Second) time.Sleep(time.Second)

View File

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

View File

@@ -6,18 +6,19 @@ import (
"time" "time"
"github.com/sagernet/sing-box/adapter" "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" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/tfo-go"
) )
type DefaultDialer struct { type DefaultDialer struct {
dialer4 tcpDialer dialer4 tfo.Dialer
dialer6 tcpDialer dialer6 tfo.Dialer
udpDialer4 net.Dialer udpDialer4 net.Dialer
udpDialer6 net.Dialer udpDialer6 net.Dialer
udpListener net.ListenConfig udpListener net.ListenConfig
@@ -25,7 +26,7 @@ type DefaultDialer struct {
udpAddr6 string udpAddr6 string
} }
func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDialer, error) { func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDialer {
var dialer net.Dialer var dialer net.Dialer
var listener net.ListenConfig var listener net.ListenConfig
if options.BindInterface != "" { if options.BindInterface != "" {
@@ -92,29 +93,15 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()} udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String() udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()
} }
if options.TCPMultiPath {
if !go121Available {
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{ return &DefaultDialer{
tcpDialer4, tfo.Dialer{Dialer: dialer4, DisableTFO: !options.TCPFastOpen},
tcpDialer6, tfo.Dialer{Dialer: dialer6, DisableTFO: !options.TCPFastOpen},
udpDialer4, udpDialer4,
udpDialer6, udpDialer6,
listener, listener,
udpAddr4, udpAddr4,
udpAddr6, udpAddr6,
}, nil }
} }
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) { func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
@@ -137,12 +124,10 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address
} }
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if destination.IsIPv6() { 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 {
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4)) 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

@@ -1,11 +0,0 @@
//go:build go1.21
package dialer
import "net"
const go121Available = 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

@@ -1,12 +0,0 @@
//go:build !go1.21
package dialer
import (
"net"
)
const go121Available = false
func setMultiPathTCP(dialer *net.Dialer) {
}

View File

@@ -6,7 +6,6 @@ import (
"sync" "sync"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/bufio/deadline"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
@@ -45,14 +44,7 @@ func (d *DetourDialer) DialContext(ctx context.Context, network string, destinat
if err != nil { if err != nil {
return nil, err return nil, err
} }
conn, err := dialer.DialContext(ctx, network, destination) return dialer.DialContext(ctx, network, destination)
if err != nil {
return nil, err
}
if deadline.NeedAdditionalReadDeadline(conn) {
conn = deadline.NewConn(conn)
}
return conn, nil
} }
func (d *DetourDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { func (d *DetourDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {

View File

@@ -6,35 +6,19 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-dns" "github.com/sagernet/sing-dns"
"github.com/sagernet/sing/common"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
func MustNew(router adapter.Router, options option.DialerOptions) N.Dialer { func New(router adapter.Router, options option.DialerOptions) N.Dialer {
return common.Must1(New(router, options)) var dialer N.Dialer
}
func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error) {
var (
dialer N.Dialer
err error
)
if options.Detour == "" { if options.Detour == "" {
dialer, err = NewDefault(router, options) dialer = NewDefault(router, options)
if err != nil {
return nil, err
}
} else { } else {
dialer = NewDetour(router, options.Detour) dialer = NewDetour(router, options.Detour)
} }
domainStrategy := dns.DomainStrategy(options.DomainStrategy) domainStrategy := dns.DomainStrategy(options.DomainStrategy)
if domainStrategy != dns.DomainStrategyAsIS || options.Detour == "" { if domainStrategy != dns.DomainStrategyAsIS || options.Detour == "" {
dialer = NewResolveDialer( dialer = NewResolveDialer(router, dialer, domainStrategy, time.Duration(options.FallbackDelay))
router,
dialer,
options.Detour == "" && !options.TCPFastOpen,
domainStrategy,
time.Duration(options.FallbackDelay))
} }
return dialer, nil return dialer
} }

View File

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

View File

@@ -18,19 +18,11 @@ func NewRouter(router adapter.Router) N.Dialer {
} }
func (d *RouterDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { func (d *RouterDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
dialer, err := d.router.DefaultOutbound(network) return d.router.DefaultOutbound(network).DialContext(ctx, network, destination)
if err != nil {
return nil, err
}
return dialer.DialContext(ctx, network, destination)
} }
func (d *RouterDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { func (d *RouterDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
dialer, err := d.router.DefaultOutbound(N.NetworkUDP) return d.router.DefaultOutbound(N.NetworkUDP).ListenPacket(ctx, destination)
if err != nil {
return nil, err
}
return dialer.ListenPacket(ctx, destination)
} }
func (d *RouterDialer) Upstream() any { func (d *RouterDialer) Upstream() any {

View File

@@ -1,5 +1,3 @@
//go:build go1.20
package dialer package dialer
import ( import (
@@ -7,7 +5,6 @@ import (
"io" "io"
"net" "net"
"os" "os"
"sync"
"time" "time"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
@@ -25,11 +22,10 @@ type slowOpenConn struct {
destination M.Socksaddr destination M.Socksaddr
conn net.Conn conn net.Conn
create chan struct{} create chan struct{}
access sync.Mutex
err error 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 { if dialer.DisableTFO || N.NetworkName(network) != N.NetworkTCP {
switch N.NetworkName(network) { switch N.NetworkName(network) {
case N.NetworkTCP, N.NetworkUDP: 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) { func (c *slowOpenConn) Write(b []byte) (n int, err error) {
if c.conn != nil { if c.conn == nil {
return c.conn.Write(b) c.conn, err = c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
} if err != nil {
c.access.Lock() c.err = E.Cause(err, "dial tcp fast open")
defer c.access.Unlock()
select {
case <-c.create:
if c.err != nil {
return 0, c.err
} }
return c.conn.Write(b) close(c.create)
default: return
} }
c.conn, err = c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b) return c.conn.Write(b)
if err != nil {
c.conn = nil
c.err = E.Cause(err, "dial tcp fast open")
}
close(c.create)
return
} }
func (c *slowOpenConn) Close() error { func (c *slowOpenConn) Close() error {
@@ -143,6 +128,13 @@ func (c *slowOpenConn) NeedHandshake() bool {
return c.conn == nil return c.conn == nil
} }
func (c *slowOpenConn) ReadFrom(r io.Reader) (n int64, err error) {
if c.conn != nil {
return bufio.Copy(c.conn, r)
}
return bufio.ReadFrom0(c, r)
}
func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) { func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) {
if c.conn == nil { if c.conn == nil {
select { select {

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 package mux
import ( import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-mux" "github.com/sagernet/sing-mux"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
type Client = mux.Client func NewClientWithOptions(dialer N.Dialer, options option.MultiplexOptions) (*Client, error) {
func NewClientWithOptions(dialer N.Dialer, logger logger.Logger, options option.OutboundMultiplexOptions) (*Client, error) {
if !options.Enabled { if !options.Enabled {
return nil, nil return nil, nil
} }
var brutalOptions mux.BrutalOptions
if options.Brutal != nil && options.Brutal.Enabled {
brutalOptions = mux.BrutalOptions{
Enabled: true,
SendBPS: uint64(options.Brutal.UpMbps * C.MbpsToBps),
ReceiveBPS: uint64(options.Brutal.DownMbps * C.MbpsToBps),
}
if brutalOptions.SendBPS < mux.BrutalMinSpeedBPS {
return nil, E.New("brutal: invalid upload speed")
}
if brutalOptions.ReceiveBPS < mux.BrutalMinSpeedBPS {
return nil, E.New("brutal: invalid download speed")
}
}
return mux.NewClient(mux.Options{ return mux.NewClient(mux.Options{
Dialer: dialer, Dialer: dialer,
Logger: logger,
Protocol: options.Protocol, Protocol: options.Protocol,
MaxConnections: options.MaxConnections, MaxConnections: options.MaxConnections,
MinStreams: options.MinStreams, MinStreams: options.MinStreams,
MaxStreams: options.MaxStreams, MaxStreams: options.MaxStreams,
Padding: options.Padding, Padding: options.Padding,
Brutal: brutalOptions,
}) })
} }

14
common/mux/protocol.go Normal file
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

@@ -15,6 +15,7 @@ import (
"unicode" "unicode"
"unsafe" "unsafe"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
@@ -81,7 +82,9 @@ func resolveSocketByNetlink(network string, source netip.AddrPort, destination n
return 0, 0, E.Cause(err, "write netlink request") return 0, 0, E.Cause(err, "write netlink request")
} }
buffer := buf.New() _buffer := buf.StackNew()
defer common.KeepAlive(_buffer)
buffer := common.Dup(_buffer)
defer buffer.Release() defer buffer.Release()
n, err := syscall.Read(socket, buffer.FreeBytes()) n, err := syscall.Read(socket, buffer.FreeBytes())

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 package settings
import ( import (
"context"
"os" "os"
"strings" "strings"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/shell" "github.com/sagernet/sing/common/shell"
) )
type AndroidSystemProxy struct { var (
useRish bool useRish bool
rishPath string rishPath string
serverAddr M.Socksaddr )
supportSOCKS bool
isEnabled bool
}
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*AndroidSystemProxy, error) { func init() {
userId := os.Getuid() userId := os.Getuid()
var (
useRish bool
rishPath string
)
if userId == 0 || userId == 1000 || userId == 2000 { if userId == 0 || userId == 1000 || userId == 2000 {
useRish = false useRish = false
} else { } else {
rishPath, useRish = C.FindPath("rish") 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 { func runAndroidShell(name string, args ...string) error {
return p.isEnabled if !useRish {
}
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 {
return shell.Exec(name, args...).Attach().Run() return shell.Exec(name, args...).Attach().Run()
} else { } 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 package settings
import ( import (
"context"
"net/netip" "net/netip"
"strconv"
"strings" "strings"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
E "github.com/sagernet/sing/common/exceptions" 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/shell"
"github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/common/x/list"
) )
type DarwinSystemProxy struct { type systemProxy struct {
monitor tun.DefaultInterfaceMonitor monitor tun.DefaultInterfaceMonitor
interfaceName string interfaceName string
element *list.Element[tun.DefaultInterfaceUpdateCallback] element *list.Element[tun.DefaultInterfaceUpdateCallback]
serverAddr M.Socksaddr port uint16
supportSOCKS bool isMixed bool
isEnabled bool
} }
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*DarwinSystemProxy, error) { func (p *systemProxy) update(event int) error {
interfaceMonitor := adapter.RouterFromContext(ctx).InterfaceMonitor() newInterfaceName := p.monitor.DefaultInterfaceName(netip.IPv4Unspecified())
if interfaceMonitor == nil { if p.interfaceName == newInterfaceName {
return nil, E.New("missing interface monitor") return nil
} }
proxy := &DarwinSystemProxy{ if p.interfaceName != "" {
monitor: interfaceMonitor, _ = p.unset()
serverAddr: serverAddr,
supportSOCKS: supportSOCKS,
} }
proxy.element = interfaceMonitor.RegisterCallback(proxy.update) p.interfaceName = newInterfaceName
return proxy, nil
}
func (p *DarwinSystemProxy) IsEnabled() bool {
return p.isEnabled
}
func (p *DarwinSystemProxy) Enable() error {
return p.update0()
}
func (p *DarwinSystemProxy) Disable() error {
interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName) interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName)
if err != nil { if err != nil {
return err return err
} }
if p.supportSOCKS { 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 {
err = shell.Exec("networksetup", "-setsecurewebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
}
return err
}
func (p *systemProxy) unset() error {
interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName)
if err != nil {
return err
}
if p.isMixed {
err = shell.Exec("networksetup", "-setsocksfirewallproxystate", interfaceDisplayName, "off").Attach().Run() err = shell.Exec("networksetup", "-setsocksfirewallproxystate", interfaceDisplayName, "off").Attach().Run()
} }
if err == nil { if err == nil {
@@ -59,53 +59,9 @@ func (p *DarwinSystemProxy) Disable() error {
if err == nil { if err == nil {
err = shell.Exec("networksetup", "-setsecurewebproxystate", interfaceDisplayName, "off").Attach().Run() err = shell.Exec("networksetup", "-setsecurewebproxystate", interfaceDisplayName, "off").Attach().Run()
} }
if err == nil {
p.isEnabled = false
}
return err 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) { func getInterfaceDisplayName(name string) (string, error) {
content, err := shell.Exec("networksetup", "-listallhardwareports").ReadOutput() content, err := shell.Exec("networksetup", "-listallhardwareports").ReadOutput()
if err != nil { if err != nil {
@@ -121,3 +77,24 @@ func getInterfaceDisplayName(name string) (string, error) {
} }
return "", E.New(name, " not found in networksetup -listallhardwareports") 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,
}
err := proxy.update(tun.EventInterfaceUpdate)
if err != nil {
return nil, err
}
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 package settings
import ( import (
"context"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/shell" "github.com/sagernet/sing/common/shell"
) )
type LinuxSystemProxy struct { var (
hasGSettings bool hasGSettings bool
hasKWriteConfig5 bool sudoUser string
sudoUser string )
serverAddr M.Socksaddr
supportSOCKS bool
isEnabled bool
}
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*LinuxSystemProxy, error) { func init() {
hasGSettings := common.Error(exec.LookPath("gsettings")) == nil hasGSettings = common.Error(exec.LookPath("gsettings")) == nil
hasKWriteConfig5 := common.Error(exec.LookPath("kwriteconfig5")) == nil
var sudoUser string
if os.Getuid() == 0 { if os.Getuid() == 0 {
sudoUser = os.Getenv("SUDO_USER") 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 { func runAsUser(name string, args ...string) error {
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 {
if os.Getuid() != 0 { if os.Getuid() != 0 {
return shell.Exec(name, args...).Attach().Run() return shell.Exec(name, args...).Attach().Run()
} else if p.sudoUser != "" { } else if sudoUser != "" {
return shell.Exec("su", "-", p.sudoUser, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run() return shell.Exec("su", "-", sudoUser, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run()
} else { } else {
return E.New("set system proxy: unable to set as root") return E.New("set system proxy: unable to set as root")
} }
} }
func (p *LinuxSystemProxy) setGnomeProxy(proxyTypes ...string) error { func SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() error, error) {
for _, proxyType := range proxyTypes { if !hasGSettings {
err := p.runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "host", p.serverAddr.AddrString()) return nil, E.New("unsupported desktop environment")
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
}
} }
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 { for _, proxyType := range proxyTypes {
var proxyUrl string err := runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "host", "127.0.0.1")
if proxyType == "socks" { if err != nil {
proxyUrl = "socks://" + p.serverAddr.String() return err
} else {
proxyUrl = "http://" + p.serverAddr.String()
} }
err := p.runAsUser( err = runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "port", F.ToString(port))
"kwriteconfig5",
"--file",
"kioslaverc",
"--group",
"Proxy Settings",
"--key", proxyType+"Proxy",
proxyUrl,
)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -3,12 +3,11 @@
package settings package settings
import ( import (
"context"
"os" "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 return nil, os.ErrInvalid
} }

View File

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

@@ -26,7 +26,9 @@ func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.
if length == 0 { if length == 0 {
return nil, os.ErrInvalid return nil, os.ErrInvalid
} }
buffer := buf.NewSize(int(length)) _buffer := buf.StackNewSize(int(length))
defer common.KeepAlive(_buffer)
buffer := common.Dup(_buffer)
defer buffer.Release() defer buffer.Release()
readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100) readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100)

View File

@@ -182,52 +182,11 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
break break
} }
switch frameType { switch frameType {
case 0x00: // PADDING case 0x0:
continue continue
case 0x01: // PING case 0x1:
continue continue
case 0x02, 0x03: // ACK case 0x6:
_, err = qtls.ReadUvarint(decryptedReader) // Largest Acknowledged
if err != nil {
return nil, err
}
_, err = qtls.ReadUvarint(decryptedReader) // ACK Delay
if err != nil {
return nil, err
}
ackRangeCount, err := qtls.ReadUvarint(decryptedReader) // ACK Range Count
if err != nil {
return nil, err
}
_, err = qtls.ReadUvarint(decryptedReader) // First ACK Range
if err != nil {
return nil, err
}
for i := 0; i < int(ackRangeCount); i++ {
_, err = qtls.ReadUvarint(decryptedReader) // Gap
if err != nil {
return nil, err
}
_, err = qtls.ReadUvarint(decryptedReader) // ACK Range Length
if err != nil {
return nil, err
}
}
if frameType == 0x03 {
_, err = qtls.ReadUvarint(decryptedReader) // ECT0 Count
if err != nil {
return nil, err
}
_, err = qtls.ReadUvarint(decryptedReader) // ECT1 Count
if err != nil {
return nil, err
}
_, err = qtls.ReadUvarint(decryptedReader) // ECN-CE Count
if err != nil {
return nil, err
}
}
case 0x06: // CRYPTO
var offset uint64 var offset uint64
offset, err = qtls.ReadUvarint(decryptedReader) offset, err = qtls.ReadUvarint(decryptedReader)
if err != nil { if err != nil {
@@ -249,26 +208,8 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
if err != nil { if err != nil {
return nil, err return nil, err
} }
case 0x1c: // CONNECTION_CLOSE
_, err = qtls.ReadUvarint(decryptedReader) // Error Code
if err != nil {
return nil, err
}
_, err = qtls.ReadUvarint(decryptedReader) // Frame Type
if err != nil {
return nil, err
}
var length uint64
length, err = qtls.ReadUvarint(decryptedReader) // Reason Phrase Length
if err != nil {
return nil, err
}
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent) // Reason Phrase
if err != nil {
return nil, err
}
default: default:
return nil, os.ErrInvalid // ignore unknown frame type
} }
} }
tlsHdr := make([]byte, 5) tlsHdr := make([]byte, 5)

View File

@@ -22,29 +22,23 @@ func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout
if timeout == 0 { if timeout == 0 {
timeout = C.ReadPayloadTimeout 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 var errors []error
for _, sniffer := range sniffers {
for i := 0; i < 3; i++ { metadata, err = sniffer(ctx, bytes.NewReader(buffer.Bytes()))
err := conn.SetReadDeadline(deadline) if metadata != nil {
if err != nil { return metadata, 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)
} }
errors = append(errors, err)
} }
return nil, E.Errors(errors...) return nil, E.Errors(errors...)
} }

View File

@@ -1,485 +0,0 @@
package srs
import (
"compress/zlib"
"encoding/binary"
"io"
"net/netip"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/domain"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
"go4.org/netipx"
)
var MagicBytes = [3]byte{0x53, 0x52, 0x53} // SRS
const (
ruleItemQueryType uint8 = iota
ruleItemNetwork
ruleItemDomain
ruleItemDomainKeyword
ruleItemDomainRegex
ruleItemSourceIPCIDR
ruleItemIPCIDR
ruleItemSourcePort
ruleItemSourcePortRange
ruleItemPort
ruleItemPortRange
ruleItemProcessName
ruleItemProcessPath
ruleItemPackageName
ruleItemWIFISSID
ruleItemWIFIBSSID
ruleItemFinal uint8 = 0xFF
)
func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) {
var magicBytes [3]byte
_, err = io.ReadFull(reader, magicBytes[:])
if err != nil {
return
}
if magicBytes != MagicBytes {
err = E.New("invalid sing-box rule set file")
return
}
var version uint8
err = binary.Read(reader, binary.BigEndian, &version)
if err != nil {
return ruleSet, err
}
if version != 1 {
return ruleSet, E.New("unsupported version: ", version)
}
zReader, err := zlib.NewReader(reader)
if err != nil {
return
}
length, err := rw.ReadUVariant(zReader)
if err != nil {
return
}
ruleSet.Rules = make([]option.HeadlessRule, length)
for i := uint64(0); i < length; i++ {
ruleSet.Rules[i], err = readRule(zReader, recovery)
if err != nil {
err = E.Cause(err, "read rule[", i, "]")
return
}
}
return
}
func Write(writer io.Writer, ruleSet option.PlainRuleSet) error {
_, err := writer.Write(MagicBytes[:])
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, uint8(1))
if err != nil {
return err
}
zWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression)
if err != nil {
return err
}
err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules)))
if err != nil {
return err
}
for _, rule := range ruleSet.Rules {
err = writeRule(zWriter, rule)
if err != nil {
return err
}
}
return zWriter.Close()
}
func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) {
var ruleType uint8
err = binary.Read(reader, binary.BigEndian, &ruleType)
if err != nil {
return
}
switch ruleType {
case 0:
rule.DefaultOptions, err = readDefaultRule(reader, recovery)
case 1:
rule.LogicalOptions, err = readLogicalRule(reader, recovery)
default:
err = E.New("unknown rule type: ", ruleType)
}
return
}
func writeRule(writer io.Writer, rule option.HeadlessRule) error {
switch rule.Type {
case C.RuleTypeDefault:
return writeDefaultRule(writer, rule.DefaultOptions)
case C.RuleTypeLogical:
return writeLogicalRule(writer, rule.LogicalOptions)
default:
panic("unknown rule type: " + rule.Type)
}
}
func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) {
var lastItemType uint8
for {
var itemType uint8
err = binary.Read(reader, binary.BigEndian, &itemType)
if err != nil {
return
}
switch itemType {
case ruleItemQueryType:
var rawQueryType []uint16
rawQueryType, err = readRuleItemUint16(reader)
if err != nil {
return
}
rule.QueryType = common.Map(rawQueryType, func(it uint16) option.DNSQueryType {
return option.DNSQueryType(it)
})
case ruleItemNetwork:
rule.Network, err = readRuleItemString(reader)
case ruleItemDomain:
var matcher *domain.Matcher
matcher, err = domain.ReadMatcher(reader)
if err != nil {
return
}
rule.DomainMatcher = matcher
case ruleItemDomainKeyword:
rule.DomainKeyword, err = readRuleItemString(reader)
case ruleItemDomainRegex:
rule.DomainRegex, err = readRuleItemString(reader)
case ruleItemSourceIPCIDR:
rule.SourceIPSet, err = readIPSet(reader)
if err != nil {
return
}
if recovery {
rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String)
}
case ruleItemIPCIDR:
rule.IPSet, err = readIPSet(reader)
if err != nil {
return
}
if recovery {
rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String)
}
case ruleItemSourcePort:
rule.SourcePort, err = readRuleItemUint16(reader)
case ruleItemSourcePortRange:
rule.SourcePortRange, err = readRuleItemString(reader)
case ruleItemPort:
rule.Port, err = readRuleItemUint16(reader)
case ruleItemPortRange:
rule.PortRange, err = readRuleItemString(reader)
case ruleItemProcessName:
rule.ProcessName, err = readRuleItemString(reader)
case ruleItemProcessPath:
rule.ProcessPath, err = readRuleItemString(reader)
case ruleItemPackageName:
rule.PackageName, err = readRuleItemString(reader)
case ruleItemWIFISSID:
rule.WIFISSID, err = readRuleItemString(reader)
case ruleItemWIFIBSSID:
rule.WIFIBSSID, err = readRuleItemString(reader)
case ruleItemFinal:
err = binary.Read(reader, binary.BigEndian, &rule.Invert)
return
default:
err = E.New("unknown rule item type: ", itemType, ", last type: ", lastItemType)
}
if err != nil {
return
}
lastItemType = itemType
}
}
func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error {
err := binary.Write(writer, binary.BigEndian, uint8(0))
if err != nil {
return err
}
if len(rule.QueryType) > 0 {
err = writeRuleItemUint16(writer, ruleItemQueryType, common.Map(rule.QueryType, func(it option.DNSQueryType) uint16 {
return uint16(it)
}))
if err != nil {
return err
}
}
if len(rule.Network) > 0 {
err = writeRuleItemString(writer, ruleItemNetwork, rule.Network)
if err != nil {
return err
}
}
if len(rule.Domain) > 0 || len(rule.DomainSuffix) > 0 {
err = binary.Write(writer, binary.BigEndian, ruleItemDomain)
if err != nil {
return err
}
err = domain.NewMatcher(rule.Domain, rule.DomainSuffix).Write(writer)
if err != nil {
return err
}
}
if len(rule.DomainKeyword) > 0 {
err = writeRuleItemString(writer, ruleItemDomainKeyword, rule.DomainKeyword)
if err != nil {
return err
}
}
if len(rule.DomainRegex) > 0 {
err = writeRuleItemString(writer, ruleItemDomainRegex, rule.DomainRegex)
if err != nil {
return err
}
}
if len(rule.SourceIPCIDR) > 0 {
err = writeRuleItemCIDR(writer, ruleItemSourceIPCIDR, rule.SourceIPCIDR)
if err != nil {
return E.Cause(err, "source_ipcidr")
}
}
if len(rule.IPCIDR) > 0 {
err = writeRuleItemCIDR(writer, ruleItemIPCIDR, rule.IPCIDR)
if err != nil {
return E.Cause(err, "ipcidr")
}
}
if len(rule.SourcePort) > 0 {
err = writeRuleItemUint16(writer, ruleItemSourcePort, rule.SourcePort)
if err != nil {
return err
}
}
if len(rule.SourcePortRange) > 0 {
err = writeRuleItemString(writer, ruleItemSourcePortRange, rule.SourcePortRange)
if err != nil {
return err
}
}
if len(rule.Port) > 0 {
err = writeRuleItemUint16(writer, ruleItemPort, rule.Port)
if err != nil {
return err
}
}
if len(rule.PortRange) > 0 {
err = writeRuleItemString(writer, ruleItemPortRange, rule.PortRange)
if err != nil {
return err
}
}
if len(rule.ProcessName) > 0 {
err = writeRuleItemString(writer, ruleItemProcessName, rule.ProcessName)
if err != nil {
return err
}
}
if len(rule.ProcessPath) > 0 {
err = writeRuleItemString(writer, ruleItemProcessPath, rule.ProcessPath)
if err != nil {
return err
}
}
if len(rule.PackageName) > 0 {
err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName)
if err != nil {
return err
}
}
if len(rule.WIFISSID) > 0 {
err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID)
if err != nil {
return err
}
}
if len(rule.WIFIBSSID) > 0 {
err = writeRuleItemString(writer, ruleItemWIFIBSSID, rule.WIFIBSSID)
if err != nil {
return err
}
}
err = binary.Write(writer, binary.BigEndian, ruleItemFinal)
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, rule.Invert)
if err != nil {
return err
}
return nil
}
func readRuleItemString(reader io.Reader) ([]string, error) {
length, err := rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
value := make([]string, length)
for i := uint64(0); i < length; i++ {
value[i], err = rw.ReadVString(reader)
if err != nil {
return nil, err
}
}
return value, nil
}
func writeRuleItemString(writer io.Writer, itemType uint8, value []string) error {
err := binary.Write(writer, binary.BigEndian, itemType)
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(value)))
if err != nil {
return err
}
for _, item := range value {
err = rw.WriteVString(writer, item)
if err != nil {
return err
}
}
return nil
}
func readRuleItemUint16(reader io.Reader) ([]uint16, error) {
length, err := rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
value := make([]uint16, length)
for i := uint64(0); i < length; i++ {
err = binary.Read(reader, binary.BigEndian, &value[i])
if err != nil {
return nil, err
}
}
return value, nil
}
func writeRuleItemUint16(writer io.Writer, itemType uint8, value []uint16) error {
err := binary.Write(writer, binary.BigEndian, itemType)
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(value)))
if err != nil {
return err
}
for _, item := range value {
err = binary.Write(writer, binary.BigEndian, item)
if err != nil {
return err
}
}
return nil
}
func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error {
var builder netipx.IPSetBuilder
for i, prefixString := range value {
prefix, err := netip.ParsePrefix(prefixString)
if err == nil {
builder.AddPrefix(prefix)
continue
}
addr, addrErr := netip.ParseAddr(prefixString)
if addrErr == nil {
builder.Add(addr)
continue
}
return E.Cause(err, "parse [", i, "]")
}
ipSet, err := builder.IPSet()
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, itemType)
if err != nil {
return err
}
return writeIPSet(writer, ipSet)
}
func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) {
var mode uint8
err = binary.Read(reader, binary.BigEndian, &mode)
if err != nil {
return
}
switch mode {
case 0:
logicalRule.Mode = C.LogicalTypeAnd
case 1:
logicalRule.Mode = C.LogicalTypeOr
default:
err = E.New("unknown logical mode: ", mode)
return
}
length, err := rw.ReadUVariant(reader)
if err != nil {
return
}
logicalRule.Rules = make([]option.HeadlessRule, length)
for i := uint64(0); i < length; i++ {
logicalRule.Rules[i], err = readRule(reader, recovery)
if err != nil {
err = E.Cause(err, "read logical rule [", i, "]")
return
}
}
err = binary.Read(reader, binary.BigEndian, &logicalRule.Invert)
if err != nil {
return
}
return
}
func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error {
err := binary.Write(writer, binary.BigEndian, uint8(1))
if err != nil {
return err
}
switch logicalRule.Mode {
case C.LogicalTypeAnd:
err = binary.Write(writer, binary.BigEndian, uint8(0))
case C.LogicalTypeOr:
err = binary.Write(writer, binary.BigEndian, uint8(1))
default:
panic("unknown logical mode: " + logicalRule.Mode)
}
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules)))
if err != nil {
return err
}
for _, rule := range logicalRule.Rules {
err = writeRule(writer, rule)
if err != nil {
return err
}
}
err = binary.Write(writer, binary.BigEndian, logicalRule.Invert)
if err != nil {
return err
}
return nil
}

View File

@@ -1,116 +0,0 @@
package srs
import (
"encoding/binary"
"io"
"net/netip"
"unsafe"
"github.com/sagernet/sing/common/rw"
"go4.org/netipx"
)
type myIPSet struct {
rr []myIPRange
}
type myIPRange struct {
from netip.Addr
to netip.Addr
}
func readIPSet(reader io.Reader) (*netipx.IPSet, error) {
var version uint8
err := binary.Read(reader, binary.BigEndian, &version)
if err != nil {
return nil, err
}
var length uint64
err = binary.Read(reader, binary.BigEndian, &length)
if err != nil {
return nil, err
}
mySet := &myIPSet{
rr: make([]myIPRange, length),
}
for i := uint64(0); i < length; i++ {
var (
fromLen uint64
toLen uint64
fromAddr netip.Addr
toAddr netip.Addr
)
fromLen, err = rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
fromBytes := make([]byte, fromLen)
_, err = io.ReadFull(reader, fromBytes)
if err != nil {
return nil, err
}
err = fromAddr.UnmarshalBinary(fromBytes)
if err != nil {
return nil, err
}
toLen, err = rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
toBytes := make([]byte, toLen)
_, err = io.ReadFull(reader, toBytes)
if err != nil {
return nil, err
}
err = toAddr.UnmarshalBinary(toBytes)
if err != nil {
return nil, err
}
mySet.rr[i] = myIPRange{fromAddr, toAddr}
}
return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil
}
func writeIPSet(writer io.Writer, set *netipx.IPSet) error {
err := binary.Write(writer, binary.BigEndian, uint8(1))
if err != nil {
return err
}
mySet := (*myIPSet)(unsafe.Pointer(set))
err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr)))
if err != nil {
return err
}
for _, rr := range mySet.rr {
var (
fromBinary []byte
toBinary []byte
)
fromBinary, err = rr.from.MarshalBinary()
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(fromBinary)))
if err != nil {
return err
}
_, err = writer.Write(fromBinary)
if err != nil {
return err
}
toBinary, err = rr.to.MarshalBinary()
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(toBinary)))
if err != nil {
return err
}
_, err = writer.Write(toBinary)
if err != nil {
return err
}
}
return nil
}

View File

@@ -9,13 +9,10 @@ import (
"strings" "strings"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/libdns/alidns"
"github.com/libdns/cloudflare"
"github.com/mholt/acmez/acme" "github.com/mholt/acmez/acme"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
@@ -24,7 +21,6 @@ import (
type acmeWrapper struct { type acmeWrapper struct {
ctx context.Context ctx context.Context
cfg *certmagic.Config cfg *certmagic.Config
cache *certmagic.Cache
domain []string domain []string
} }
@@ -33,7 +29,7 @@ func (w *acmeWrapper) Start() error {
} }
func (w *acmeWrapper) Close() error { func (w *acmeWrapper) Close() error {
w.cache.Stop() w.cfg.Unmanage(w.domain)
return nil return nil
} }
@@ -77,33 +73,14 @@ func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Con
AltTLSALPNPort: int(options.AlternativeTLSPort), AltTLSALPNPort: int(options.AlternativeTLSPort),
Logger: config.Logger, 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 != "" { if options.ExternalAccount != nil && options.ExternalAccount.KeyID != "" {
acmeConfig.ExternalAccount = (*acme.EAB)(options.ExternalAccount) acmeConfig.ExternalAccount = (*acme.EAB)(options.ExternalAccount)
} }
config.Issuers = []certmagic.Issuer{certmagic.NewACMEIssuer(config, acmeConfig)} config.Issuers = []certmagic.Issuer{certmagic.NewACMEIssuer(config, acmeConfig)}
cache := certmagic.NewCache(certmagic.CacheOptions{ config = certmagic.New(certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) { GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) {
return config, nil return config, nil
}, },
}) }), *config)
config = certmagic.New(cache, *config) return config.TLSConfig(), &acmeWrapper{ctx, config, options.Domain}, nil
return config.TLSConfig(), &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil
} }

View File

@@ -13,29 +13,29 @@ import (
aTLS "github.com/sagernet/sing/common/tls" 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 { if !options.Enabled {
return dialer, nil return dialer, nil
} }
config, err := NewClient(ctx, serverAddress, options) config, err := NewClient(router, serverAddress, options)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewDialer(dialer, config), nil 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 { if !options.Enabled {
return nil, nil return nil, nil
} }
if options.ECH != nil && options.ECH.Enabled { 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 { } 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 { } else if options.UTLS != nil && options.UTLS.Enabled {
return NewUTLSClient(ctx, serverAddress, options) return NewUTLSClient(router, serverAddress, options)
} else { } else {
return NewSTDClient(ctx, serverAddress, options) return NewSTDClient(router, serverAddress, options)
} }
} }

View File

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

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