Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ca7d15955 |
24
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
24
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,5 +1,6 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: "Create a report to help us improve."
|
description: "Create a report to help us improve."
|
||||||
|
labels: [ bug ]
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: terms
|
id: terms
|
||||||
@@ -8,11 +9,9 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: Yes, I'm using the latest major release. Only such installations are supported.
|
- label: Yes, I'm using the latest major release. Only such installations are supported.
|
||||||
required: true
|
required: true
|
||||||
- label: Yes, I'm using the latest Golang release. Only such installations are supported.
|
|
||||||
required: true
|
|
||||||
- label: Yes, I've searched similar issues on GitHub and didn't find any.
|
- label: Yes, I've searched similar issues on GitHub and didn't find any.
|
||||||
required: true
|
required: true
|
||||||
- label: Yes, I've included all information below (version, **FULL** config, **FULL** log, etc).
|
- label: Yes, I've included all information below (version, config, etc).
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -31,7 +30,7 @@ body:
|
|||||||
<details>
|
<details>
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ sing-box version
|
$ sing-box --version
|
||||||
# Paste output here
|
# Paste output here
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -52,19 +51,4 @@ body:
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: log
|
|
||||||
attributes:
|
|
||||||
label: Server and client log file
|
|
||||||
value: |-
|
|
||||||
<details>
|
|
||||||
|
|
||||||
```console
|
|
||||||
# paste log here
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
10
.github/update_dependencies.sh
vendored
10
.github/update_dependencies.sh
vendored
@@ -1,5 +1,13 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
PROJECTS=$(dirname "$0")/../..
|
PROJECTS=$(dirname "$0")/../..
|
||||||
go get -x github.com/sagernet/$1@$(git -C $PROJECTS/$1 rev-parse HEAD)
|
|
||||||
|
go get -x github.com/sagernet/sing@$(git -C $PROJECTS/sing rev-parse HEAD)
|
||||||
|
go get -x github.com/sagernet/sing-dns@$(git -C $PROJECTS/sing-dns rev-parse HEAD)
|
||||||
|
go get -x github.com/sagernet/sing-tun@$(git -C $PROJECTS/sing-tun rev-parse HEAD)
|
||||||
|
go get -x github.com/sagernet/sing-shadowsocks@$(git -C $PROJECTS/sing-shadowsocks rev-parse HEAD)
|
||||||
|
go get -x github.com/sagernet/sing-vmess@$(git -C $PROJECTS/sing-vmess rev-parse HEAD)
|
||||||
go mod tidy
|
go mod tidy
|
||||||
|
pushd test
|
||||||
|
go mod tidy
|
||||||
|
popd
|
||||||
|
|||||||
32
.github/workflows/debug.yml
vendored
32
.github/workflows/debug.yml
vendored
@@ -3,18 +3,14 @@ name: Debug build
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
|
||||||
- dev
|
- dev
|
||||||
- dev-next
|
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
- '.github/**'
|
- '.github/**'
|
||||||
- '!.github/workflows/debug.yml'
|
- '!.github/workflows/debug.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
|
||||||
- dev
|
- dev
|
||||||
- dev-next
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -51,28 +47,6 @@ jobs:
|
|||||||
go mod init build
|
go mod init build
|
||||||
go get -v github.com/sagernet/sing-box@$version
|
go get -v github.com/sagernet/sing-box@$version
|
||||||
popd
|
popd
|
||||||
continue-on-error: true
|
|
||||||
- name: Run Test
|
|
||||||
run: |
|
|
||||||
go test -v ./...
|
|
||||||
build_go118:
|
|
||||||
name: Debug build (Go 1.18)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v2
|
|
||||||
with:
|
|
||||||
go-version: 1.18.7
|
|
||||||
- name: Cache go module
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/go/pkg/mod
|
|
||||||
key: go118-${{ hashFiles('**/go.sum') }}
|
|
||||||
- name: Run Test
|
- name: Run Test
|
||||||
run: |
|
run: |
|
||||||
go test -v ./...
|
go test -v ./...
|
||||||
@@ -149,9 +123,6 @@ jobs:
|
|||||||
- name: linux-mips64el
|
- name: linux-mips64el
|
||||||
goos: linux
|
goos: linux
|
||||||
goarch: mips64le
|
goarch: mips64le
|
||||||
- name: linux-s390x
|
|
||||||
goos: linux
|
|
||||||
goarch: s390x
|
|
||||||
# darwin
|
# darwin
|
||||||
- name: darwin-amd64
|
- name: darwin-amd64
|
||||||
goos: darwin
|
goos: darwin
|
||||||
@@ -189,7 +160,6 @@ jobs:
|
|||||||
GOARM: ${{ matrix.goarm }}
|
GOARM: ${{ matrix.goarm }}
|
||||||
GOMIPS: ${{ matrix.gomips }}
|
GOMIPS: ${{ matrix.gomips }}
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
TAGS: with_clash_api,with_quic
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -216,4 +186,4 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: sing-box-${{ matrix.name }}
|
name: sing-box-${{ matrix.name }}
|
||||||
path: sing-box*
|
path: sing-box*
|
||||||
48
.github/workflows/docker.yml
vendored
48
.github/workflows/docker.yml
vendored
@@ -1,48 +0,0 @@
|
|||||||
name: Build Docker Images
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
tag:
|
|
||||||
description: "The tag version you want to build"
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Setup Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
- name: Setup QEMU for Docker Buildx
|
|
||||||
uses: docker/setup-qemu-action@v2
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Docker metadata
|
|
||||||
id: metadata
|
|
||||||
uses: docker/metadata-action@v3
|
|
||||||
with:
|
|
||||||
images: ghcr.io/sagernet/sing-box
|
|
||||||
- name: Get tag to build
|
|
||||||
id: tag
|
|
||||||
run: |
|
|
||||||
echo "latest=ghcr.io/sagernet/sing-box:latest" >> $GITHUB_OUTPUT
|
|
||||||
if [[ -z "${{ github.event.inputs.tag }}" ]]; then
|
|
||||||
echo "versioned=ghcr.io/sagernet/sing-box:${{ github.ref_name }}" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "versioned=ghcr.io/sagernet/sing-box:${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
- name: Build and release Docker images
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
platforms: linux/386,linux/amd64,linux/arm64,linux/s390x
|
|
||||||
target: dist
|
|
||||||
tags: |
|
|
||||||
${{ steps.tag.outputs.latest }}
|
|
||||||
${{ steps.tag.outputs.versioned }}
|
|
||||||
push: true
|
|
||||||
2
.github/workflows/mkdocs.yml
vendored
2
.github/workflows/mkdocs.yml
vendored
@@ -14,5 +14,5 @@ jobs:
|
|||||||
- uses: actions/setup-python@v2
|
- uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.x
|
python-version: 3.x
|
||||||
- run: pip install mkdocs-material mkdocs-static-i18n
|
- run: pip install mkdocs-material
|
||||||
- run: mkdocs gh-deploy -m "{sha}" --force --ignore-version --no-history
|
- run: mkdocs gh-deploy -m "{sha}" --force --ignore-version --no-history
|
||||||
15
.github/workflows/stale.yml
vendored
15
.github/workflows/stale.yml
vendored
@@ -1,15 +0,0 @@
|
|||||||
name: Mark stale issues and pull requests
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "30 1 * * *"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
stale:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/stale@v5
|
|
||||||
with:
|
|
||||||
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
|
|
||||||
days-before-stale: 60
|
|
||||||
days-before-close: 5
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,6 +3,4 @@
|
|||||||
/*.json
|
/*.json
|
||||||
/*.db
|
/*.db
|
||||||
/site/
|
/site/
|
||||||
/bin/
|
/bin/
|
||||||
/dist/
|
|
||||||
/sing-box
|
|
||||||
@@ -3,19 +3,18 @@ linters:
|
|||||||
enable:
|
enable:
|
||||||
- gofumpt
|
- gofumpt
|
||||||
- govet
|
- govet
|
||||||
# - gci
|
- gci
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- paralleltest
|
- paralleltest
|
||||||
|
|
||||||
run:
|
issues:
|
||||||
skip-dirs:
|
fix: true
|
||||||
- transport/cloudflaretls
|
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
# gci:
|
gci:
|
||||||
# sections:
|
sections:
|
||||||
# - standard
|
- standard
|
||||||
# - prefix(github.com/sagernet/)
|
- prefix(github.com/sagernet/)
|
||||||
# - default
|
- default
|
||||||
staticcheck:
|
staticcheck:
|
||||||
go: '1.19'
|
go: '1.19'
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
project_name: sing-box
|
|
||||||
builds:
|
|
||||||
- main: ./cmd/sing-box
|
|
||||||
flags:
|
|
||||||
- -v
|
|
||||||
- -trimpath
|
|
||||||
asmflags:
|
|
||||||
- all=-trimpath={{.Env.GOPATH}}
|
|
||||||
gcflags:
|
|
||||||
- all=-trimpath={{.Env.GOPATH}}
|
|
||||||
ldflags:
|
|
||||||
- -s -w -buildid=
|
|
||||||
tags:
|
|
||||||
- with_gvisor
|
|
||||||
- with_quic
|
|
||||||
- with_wireguard
|
|
||||||
- with_utls
|
|
||||||
- with_clash_api
|
|
||||||
env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
targets:
|
|
||||||
- android_arm64
|
|
||||||
- android_amd64
|
|
||||||
- android_amd64_v3
|
|
||||||
- linux_amd64_v1
|
|
||||||
- linux_amd64_v3
|
|
||||||
- linux_arm64
|
|
||||||
- linux_arm_7
|
|
||||||
- linux_s390x
|
|
||||||
- windows_amd64_v1
|
|
||||||
- windows_amd64_v3
|
|
||||||
- windows_386
|
|
||||||
- windows_arm64
|
|
||||||
- darwin_amd64_v1
|
|
||||||
- darwin_amd64_v3
|
|
||||||
- darwin_arm64
|
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
|
||||||
snapshot:
|
|
||||||
name_template: "{{ .Version }}.{{ .ShortCommit }}"
|
|
||||||
archives:
|
|
||||||
- id: archive
|
|
||||||
format: tar.gz
|
|
||||||
format_overrides:
|
|
||||||
- goos: windows
|
|
||||||
format: zip
|
|
||||||
wrap_in_directory: true
|
|
||||||
files:
|
|
||||||
- LICENSE
|
|
||||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
|
||||||
nfpms:
|
|
||||||
- id: package
|
|
||||||
package_name: sing-box
|
|
||||||
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
|
||||||
vendor: sagernet
|
|
||||||
homepage: https://sing-box.sagernet.org/
|
|
||||||
maintainer: nekohasekai <contact-git@sekai.icu>
|
|
||||||
description: The universal proxy platform.
|
|
||||||
license: GPLv3 or later
|
|
||||||
formats:
|
|
||||||
- deb
|
|
||||||
- rpm
|
|
||||||
priority: extra
|
|
||||||
contents:
|
|
||||||
- src: release/config/config.json
|
|
||||||
dst: /etc/sing-box/config.json
|
|
||||||
type: config
|
|
||||||
- src: release/config/sing-box.service
|
|
||||||
dst: /etc/systemd/system/sing-box.service
|
|
||||||
- src: release/config/sing-box@.service
|
|
||||||
dst: /etc/systemd/system/sing-box@.service
|
|
||||||
- src: LICENSE
|
|
||||||
dst: /usr/share/licenses/sing-box/LICENSE
|
|
||||||
source:
|
|
||||||
enabled: false
|
|
||||||
name_template: '{{ .ProjectName }}-{{ .Version }}.source'
|
|
||||||
prefix_template: '{{ .ProjectName }}-{{ .Version }}/'
|
|
||||||
checksum:
|
|
||||||
disable: true
|
|
||||||
name_template: '{{ .ProjectName }}-{{ .Version }}.checksum'
|
|
||||||
signs:
|
|
||||||
- artifacts: checksum
|
|
||||||
release:
|
|
||||||
github:
|
|
||||||
owner: SagerNet
|
|
||||||
name: sing-box
|
|
||||||
name_template: '{{ if .IsSnapshot }}{{ nightly }}{{ else }}{{ .Version }}{{ end }}'
|
|
||||||
draft: true
|
|
||||||
mode: replace
|
|
||||||
22
Dockerfile
22
Dockerfile
@@ -1,22 +0,0 @@
|
|||||||
FROM golang:1.19-alpine AS builder
|
|
||||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
|
||||||
COPY . /go/src/github.com/sagernet/sing-box
|
|
||||||
WORKDIR /go/src/github.com/sagernet/sing-box
|
|
||||||
ARG GOPROXY=""
|
|
||||||
ENV GOPROXY ${GOPROXY}
|
|
||||||
ENV CGO_ENABLED=0
|
|
||||||
RUN set -ex \
|
|
||||||
&& apk add git build-base \
|
|
||||||
&& export COMMIT=$(git rev-parse --short HEAD) \
|
|
||||||
&& go build -v -trimpath -tags with_quic,with_wireguard,with_acme \
|
|
||||||
-o /go/bin/sing-box \
|
|
||||||
-ldflags "-s -w -buildid=" \
|
|
||||||
./cmd/sing-box
|
|
||||||
FROM alpine AS dist
|
|
||||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
|
||||||
RUN set -ex \
|
|
||||||
&& apk upgrade \
|
|
||||||
&& apk add bash tzdata ca-certificates \
|
|
||||||
&& rm -rf /var/cache/apk/*
|
|
||||||
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
|
|
||||||
ENTRYPOINT ["sing-box"]
|
|
||||||
76
Makefile
76
Makefile
@@ -1,78 +1,46 @@
|
|||||||
NAME = sing-box
|
NAME=sing-box
|
||||||
COMMIT = $(shell git rev-parse --short HEAD)
|
COMMIT=$(shell git rev-parse --short HEAD)
|
||||||
TAGS ?= with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api
|
PARAMS=-trimpath -tags '$(TAGS)' -ldflags \
|
||||||
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_shadowsocksr
|
'-X "github.com/sagernet/sing-box/constant.Commit=$(COMMIT)" \
|
||||||
PARAMS = -v -trimpath -tags "$(TAGS)" -ldflags "-s -w -buildid="
|
-w -s -buildid='
|
||||||
MAIN = ./cmd/sing-box
|
MAIN=./cmd/sing-box
|
||||||
|
|
||||||
.PHONY: test release
|
.PHONY: test
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go build $(PARAMS) $(MAIN)
|
go build $(PARAMS) $(MAIN)
|
||||||
|
|
||||||
|
action_version: build
|
||||||
|
echo "::set-output name=VERSION::`./sing-box version -n`"
|
||||||
|
|
||||||
install:
|
install:
|
||||||
go install $(PARAMS) $(MAIN)
|
go install $(PARAMS) $(MAIN)
|
||||||
|
|
||||||
fmt:
|
|
||||||
@gofumpt -l -w .
|
|
||||||
@gofmt -s -w .
|
|
||||||
@gci write -s "standard,prefix(github.com/sagernet/),default" .
|
|
||||||
|
|
||||||
fmt_install:
|
fmt_install:
|
||||||
go install -v mvdan.cc/gofumpt@latest
|
go install -v mvdan.cc/gofumpt@latest
|
||||||
go install -v github.com/daixiang0/gci@v0.4.0
|
go install -v github.com/daixiang0/gci@v0.4.0
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
gofumpt -l -w .
|
||||||
|
gofmt -s -w .
|
||||||
|
gci write -s "standard,prefix(github.com/sagernet/),default" .
|
||||||
|
|
||||||
|
lint_install:
|
||||||
|
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
GOOS=linux golangci-lint run ./...
|
GOOS=linux golangci-lint run ./...
|
||||||
GOOS=android golangci-lint run ./...
|
|
||||||
GOOS=windows golangci-lint run ./...
|
GOOS=windows golangci-lint run ./...
|
||||||
GOOS=darwin golangci-lint run ./...
|
GOOS=darwin golangci-lint run ./...
|
||||||
GOOS=freebsd golangci-lint run ./...
|
GOOS=freebsd golangci-lint run ./...
|
||||||
|
|
||||||
lint_install:
|
|
||||||
go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
|
||||||
|
|
||||||
proto:
|
|
||||||
@go run ./cmd/internal/protogen
|
|
||||||
@gofumpt -l -w .
|
|
||||||
@gofumpt -l -w .
|
|
||||||
|
|
||||||
proto_install:
|
|
||||||
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
|
||||||
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
|
||||||
|
|
||||||
snapshot:
|
|
||||||
goreleaser release --rm-dist --snapshot
|
|
||||||
mkdir dist/release
|
|
||||||
mv dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/release
|
|
||||||
ghr --delete --draft --prerelease -p 1 nightly dist/release
|
|
||||||
rm -r dist
|
|
||||||
|
|
||||||
release:
|
|
||||||
goreleaser release --rm-dist --skip-publish
|
|
||||||
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:
|
|
||||||
go install -v github.com/goreleaser/goreleaser@latest
|
|
||||||
go install -v github.com/tcnksm/ghr@latest
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@go test -v ./... && \
|
go test -v . && \
|
||||||
cd test && \
|
pushd test && \
|
||||||
go mod tidy && \
|
go test -v . && \
|
||||||
go test -v -tags "$(TAGS_TEST)" .
|
popd
|
||||||
|
|
||||||
test_stdio:
|
|
||||||
@go test -v ./... && \
|
|
||||||
cd test && \
|
|
||||||
go mod tidy && \
|
|
||||||
go test -v -tags "$(TAGS_TEST),force_stdio" .
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf bin dist sing-box
|
|
||||||
rm -f $(shell go env GOPATH)/sing-box
|
rm -f $(shell go env GOPATH)/sing-box
|
||||||
|
|
||||||
update:
|
update:
|
||||||
|
|||||||
@@ -4,29 +4,23 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/urltest"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClashServer interface {
|
type ClashServer interface {
|
||||||
Service
|
Service
|
||||||
Mode() string
|
TrafficController
|
||||||
StoreSelected() bool
|
|
||||||
CacheFile() ClashCacheFile
|
|
||||||
HistoryStorage() *urltest.HistoryStorage
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClashCacheFile interface {
|
|
||||||
LoadSelected(group string) string
|
|
||||||
StoreSelected(group string, selected string) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tracker interface {
|
type Tracker interface {
|
||||||
Leave()
|
Leave()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TrafficController interface {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
type OutboundGroup interface {
|
type OutboundGroup interface {
|
||||||
Now() string
|
Now() string
|
||||||
All() []string
|
All() []string
|
||||||
@@ -38,13 +32,3 @@ func OutboundTag(detour Outbound) string {
|
|||||||
}
|
}
|
||||||
return detour.Tag()
|
return detour.Tag()
|
||||||
}
|
}
|
||||||
|
|
||||||
type V2RayServer interface {
|
|
||||||
Service
|
|
||||||
StatsService() V2RayStatsService
|
|
||||||
}
|
|
||||||
|
|
||||||
type V2RayStatsService interface {
|
|
||||||
RoutedConnection(inbound string, outbound string, conn net.Conn) net.Conn
|
|
||||||
RoutedPacketConnection(inbound string, outbound string, conn N.PacketConn) N.PacketConn
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/process"
|
"github.com/sagernet/sing-box/common/process"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-dns"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Inbound interface {
|
type Inbound interface {
|
||||||
@@ -17,17 +15,9 @@ type Inbound interface {
|
|||||||
Tag() string
|
Tag() string
|
||||||
}
|
}
|
||||||
|
|
||||||
type InjectableInbound interface {
|
|
||||||
Inbound
|
|
||||||
Network() []string
|
|
||||||
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
|
||||||
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type InboundContext struct {
|
type InboundContext struct {
|
||||||
Inbound string
|
Inbound string
|
||||||
InboundType string
|
InboundType string
|
||||||
IPVersion int
|
|
||||||
Network string
|
Network string
|
||||||
Source M.Socksaddr
|
Source M.Socksaddr
|
||||||
Destination M.Socksaddr
|
Destination M.Socksaddr
|
||||||
@@ -38,14 +28,14 @@ type InboundContext struct {
|
|||||||
|
|
||||||
// cache
|
// cache
|
||||||
|
|
||||||
InboundDetour string
|
DomainStrategy dns.DomainStrategy
|
||||||
LastInbound string
|
SniffEnabled bool
|
||||||
OriginDestination M.Socksaddr
|
SniffOverrideDestination bool
|
||||||
InboundOptions option.InboundOptions
|
DestinationAddresses []netip.Addr
|
||||||
DestinationAddresses []netip.Addr
|
|
||||||
SourceGeoIPCode string
|
SourceGeoIPCode string
|
||||||
GeoIPCode string
|
GeoIPCode string
|
||||||
ProcessInfo *process.Info
|
ProcessInfo *process.Info
|
||||||
}
|
}
|
||||||
|
|
||||||
type inboundContextKey struct{}
|
type inboundContextKey struct{}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/sagernet/sing/common/control"
|
"github.com/sagernet/sing/common/control"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
mdns "github.com/miekg/dns"
|
"golang.org/x/net/dns/dnsmessage"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Router interface {
|
type Router interface {
|
||||||
@@ -27,24 +27,18 @@ type Router interface {
|
|||||||
GeoIPReader() *geoip.Reader
|
GeoIPReader() *geoip.Reader
|
||||||
LoadGeosite(code string) (Rule, error)
|
LoadGeosite(code string) (Rule, error)
|
||||||
|
|
||||||
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
|
Exchange(ctx context.Context, message *dnsmessage.Message) (*dnsmessage.Message, 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)
|
||||||
|
|
||||||
InterfaceFinder() control.InterfaceFinder
|
InterfaceBindManager() control.BindManager
|
||||||
DefaultInterface() string
|
DefaultInterface() string
|
||||||
AutoDetectInterface() bool
|
AutoDetectInterface() bool
|
||||||
DefaultMark() int
|
DefaultMark() int
|
||||||
NetworkMonitor() tun.NetworkUpdateMonitor
|
NetworkMonitor() tun.NetworkUpdateMonitor
|
||||||
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
||||||
PackageManager() tun.PackageManager
|
|
||||||
Rules() []Rule
|
Rules() []Rule
|
||||||
|
SetTrafficController(controller TrafficController)
|
||||||
ClashServer() ClashServer
|
|
||||||
SetClashServer(server ClashServer)
|
|
||||||
|
|
||||||
V2RayServer() V2RayServer
|
|
||||||
SetV2RayServer(server V2RayServer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Rule interface {
|
type Rule interface {
|
||||||
@@ -60,7 +54,3 @@ type DNSRule interface {
|
|||||||
Rule
|
Rule
|
||||||
DisableCache() bool
|
DisableCache() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterfaceUpdateListener interface {
|
|
||||||
InterfaceUpdated() error
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
package adapter
|
package adapter
|
||||||
|
|
||||||
type Service interface {
|
import "io"
|
||||||
|
|
||||||
|
type Starter interface {
|
||||||
Start() error
|
Start() error
|
||||||
Close() error
|
}
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
Starter
|
||||||
|
io.Closer
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,25 +38,13 @@ type myUpstreamHandlerWrapper struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||||
myMetadata := w.metadata
|
w.metadata.Destination = metadata.Destination
|
||||||
if metadata.Source.IsValid() {
|
return w.connectionHandler(ctx, conn, w.metadata)
|
||||||
myMetadata.Source = metadata.Source
|
|
||||||
}
|
|
||||||
if metadata.Destination.IsValid() {
|
|
||||||
myMetadata.Destination = metadata.Destination
|
|
||||||
}
|
|
||||||
return w.connectionHandler(ctx, conn, myMetadata)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||||
myMetadata := w.metadata
|
w.metadata.Destination = metadata.Destination
|
||||||
if metadata.Source.IsValid() {
|
return w.packetHandler(ctx, conn, w.metadata)
|
||||||
myMetadata.Source = metadata.Source
|
|
||||||
}
|
|
||||||
if metadata.Destination.IsValid() {
|
|
||||||
myMetadata.Destination = metadata.Destination
|
|
||||||
}
|
|
||||||
return w.packetHandler(ctx, conn, myMetadata)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
|
func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||||
@@ -90,23 +78,13 @@ func NewUpstreamContextHandler(
|
|||||||
|
|
||||||
func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||||
myMetadata := ContextFrom(ctx)
|
myMetadata := ContextFrom(ctx)
|
||||||
if metadata.Source.IsValid() {
|
myMetadata.Destination = metadata.Destination
|
||||||
myMetadata.Source = metadata.Source
|
|
||||||
}
|
|
||||||
if metadata.Destination.IsValid() {
|
|
||||||
myMetadata.Destination = metadata.Destination
|
|
||||||
}
|
|
||||||
return w.connectionHandler(ctx, conn, *myMetadata)
|
return w.connectionHandler(ctx, conn, *myMetadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||||
myMetadata := ContextFrom(ctx)
|
myMetadata := ContextFrom(ctx)
|
||||||
if metadata.Source.IsValid() {
|
myMetadata.Destination = metadata.Destination
|
||||||
myMetadata.Source = metadata.Source
|
|
||||||
}
|
|
||||||
if metadata.Destination.IsValid() {
|
|
||||||
myMetadata.Destination = metadata.Destination
|
|
||||||
}
|
|
||||||
return w.packetHandler(ctx, conn, *myMetadata)
|
return w.packetHandler(ctx, conn, *myMetadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
package adapter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
type V2RayServerTransport interface {
|
|
||||||
Network() []string
|
|
||||||
Serve(listener net.Listener) error
|
|
||||||
ServePacket(listener net.PacketConn) error
|
|
||||||
Close() error
|
|
||||||
}
|
|
||||||
|
|
||||||
type V2RayClientTransport interface {
|
|
||||||
DialContext(ctx context.Context) (net.Conn, error)
|
|
||||||
}
|
|
||||||
83
box.go
83
box.go
@@ -2,10 +2,8 @@ package box
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"runtime/debug"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
@@ -31,7 +29,6 @@ type Box struct {
|
|||||||
logger log.ContextLogger
|
logger log.ContextLogger
|
||||||
logFile *os.File
|
logFile *os.File
|
||||||
clashServer adapter.ClashServer
|
clashServer adapter.ClashServer
|
||||||
v2rayServer adapter.V2RayServer
|
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,14 +37,8 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
|
|||||||
logOptions := common.PtrValueOrDefault(options.Log)
|
logOptions := common.PtrValueOrDefault(options.Log)
|
||||||
|
|
||||||
var needClashAPI bool
|
var needClashAPI bool
|
||||||
var needV2RayAPI bool
|
if options.Experimental != nil && options.Experimental.ClashAPI != nil && options.Experimental.ClashAPI.ExternalController != "" {
|
||||||
if options.Experimental != nil {
|
needClashAPI = true
|
||||||
if options.Experimental.ClashAPI != nil && options.Experimental.ClashAPI.ExternalController != "" {
|
|
||||||
needClashAPI = true
|
|
||||||
}
|
|
||||||
if options.Experimental.V2RayAPI != nil && options.Experimental.V2RayAPI.Listen != "" {
|
|
||||||
needV2RayAPI = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var logFactory log.Factory
|
var logFactory log.Factory
|
||||||
@@ -69,7 +60,6 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
logWriter = logFile
|
|
||||||
}
|
}
|
||||||
logFormatter := log.Formatter{
|
logFormatter := log.Formatter{
|
||||||
BaseTime: createdAt,
|
BaseTime: createdAt,
|
||||||
@@ -97,7 +87,8 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
|
|||||||
|
|
||||||
router, err := route.NewRouter(
|
router, err := route.NewRouter(
|
||||||
ctx,
|
ctx,
|
||||||
logFactory,
|
logFactory.NewLogger("router"),
|
||||||
|
logFactory.NewLogger("dns"),
|
||||||
common.PtrValueOrDefault(options.Route),
|
common.PtrValueOrDefault(options.Route),
|
||||||
common.PtrValueOrDefault(options.DNS),
|
common.PtrValueOrDefault(options.DNS),
|
||||||
options.Inbounds,
|
options.Inbounds,
|
||||||
@@ -144,7 +135,7 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
|
|||||||
}
|
}
|
||||||
outbounds = append(outbounds, out)
|
outbounds = append(outbounds, out)
|
||||||
}
|
}
|
||||||
err = router.Initialize(inbounds, outbounds, func() adapter.Outbound {
|
err = router.Initialize(outbounds, func() adapter.Outbound {
|
||||||
out, oErr := outbound.New(ctx, router, logFactory.NewLogger("outbound/direct"), option.Outbound{Type: "direct", Tag: "default"})
|
out, oErr := outbound.New(ctx, router, logFactory.NewLogger("outbound/direct"), option.Outbound{Type: "direct", Tag: "default"})
|
||||||
common.Must(oErr)
|
common.Must(oErr)
|
||||||
outbounds = append(outbounds, out)
|
outbounds = append(outbounds, out)
|
||||||
@@ -155,20 +146,12 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var clashServer adapter.ClashServer
|
var clashServer adapter.ClashServer
|
||||||
var v2rayServer adapter.V2RayServer
|
|
||||||
if needClashAPI {
|
if needClashAPI {
|
||||||
clashServer, err = experimental.NewClashServer(router, observableLogFactory, common.PtrValueOrDefault(options.Experimental.ClashAPI))
|
clashServer, err = experimental.NewClashServer(router, observableLogFactory, common.PtrValueOrDefault(options.Experimental.ClashAPI))
|
||||||
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.SetTrafficController(clashServer)
|
||||||
}
|
|
||||||
if needV2RayAPI {
|
|
||||||
v2rayServer, err = experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(options.Experimental.V2RayAPI))
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "create v2ray api server")
|
|
||||||
}
|
|
||||||
router.SetV2RayServer(v2rayServer)
|
|
||||||
}
|
}
|
||||||
return &Box{
|
return &Box{
|
||||||
router: router,
|
router: router,
|
||||||
@@ -176,46 +159,14 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
|
|||||||
outbounds: outbounds,
|
outbounds: outbounds,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
logFactory: logFactory,
|
logFactory: logFactory,
|
||||||
logger: logFactory.Logger(),
|
logger: logFactory.NewLogger(""),
|
||||||
logFile: logFile,
|
logFile: logFile,
|
||||||
clashServer: clashServer,
|
clashServer: clashServer,
|
||||||
v2rayServer: v2rayServer,
|
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Box) Start() error {
|
func (s *Box) Start() error {
|
||||||
err := s.start()
|
|
||||||
if err != nil {
|
|
||||||
// TODO: remove catch error
|
|
||||||
defer func() {
|
|
||||||
v := recover()
|
|
||||||
if v != nil {
|
|
||||||
log.Error(E.Cause(err, "origin error"))
|
|
||||||
debug.PrintStack()
|
|
||||||
panic("panic on early close: " + fmt.Sprint(v))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
s.Close()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Box) start() error {
|
|
||||||
for i, out := range s.outbounds {
|
|
||||||
if starter, isStarter := out.(common.Starter); isStarter {
|
|
||||||
err := starter.Start()
|
|
||||||
if err != nil {
|
|
||||||
var tag string
|
|
||||||
if out.Tag() == "" {
|
|
||||||
tag = F.ToString(i)
|
|
||||||
} else {
|
|
||||||
tag = out.Tag()
|
|
||||||
}
|
|
||||||
return E.Cause(err, "initialize outbound/", out.Type(), "[", tag, "]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err := s.router.Start()
|
err := s.router.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -223,13 +174,10 @@ func (s *Box) start() error {
|
|||||||
for i, in := range s.inbounds {
|
for i, in := range s.inbounds {
|
||||||
err = in.Start()
|
err = in.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var tag string
|
for g := 0; g < i; g++ {
|
||||||
if in.Tag() == "" {
|
s.inbounds[g].Close()
|
||||||
tag = F.ToString(i)
|
|
||||||
} else {
|
|
||||||
tag = in.Tag()
|
|
||||||
}
|
}
|
||||||
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if s.clashServer != nil {
|
if s.clashServer != nil {
|
||||||
@@ -238,12 +186,6 @@ func (s *Box) start() error {
|
|||||||
return E.Cause(err, "start clash api server")
|
return E.Cause(err, "start clash api server")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if s.v2rayServer != nil {
|
|
||||||
err = s.v2rayServer.Start()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "start v2ray api server")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.logger.Info("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
s.logger.Info("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -265,11 +207,6 @@ func (s *Box) Close() error {
|
|||||||
s.router,
|
s.router,
|
||||||
s.logFactory,
|
s.logFactory,
|
||||||
s.clashServer,
|
s.clashServer,
|
||||||
s.v2rayServer,
|
|
||||||
common.PtrOrNil(s.logFile),
|
common.PtrOrNil(s.logFile),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Box) Router() adapter.Router {
|
|
||||||
return s.router
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,218 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"go/build"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// envFile returns the name of the Go environment configuration file.
|
|
||||||
// Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166
|
|
||||||
func envFile() (string, error) {
|
|
||||||
if file := os.Getenv("GOENV"); file != "" {
|
|
||||||
if file == "off" {
|
|
||||||
return "", fmt.Errorf("GOENV=off")
|
|
||||||
}
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
dir, err := os.UserConfigDir()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if dir == "" {
|
|
||||||
return "", fmt.Errorf("missing user-config dir")
|
|
||||||
}
|
|
||||||
return filepath.Join(dir, "go", "env"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRuntimeEnv returns the value of runtime environment variable,
|
|
||||||
// that is set by running following command: `go env -w key=value`.
|
|
||||||
func GetRuntimeEnv(key string) (string, error) {
|
|
||||||
file, err := envFile()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if file == "" {
|
|
||||||
return "", fmt.Errorf("missing runtime env file")
|
|
||||||
}
|
|
||||||
var data []byte
|
|
||||||
var runtimeEnv string
|
|
||||||
data, readErr := os.ReadFile(file)
|
|
||||||
if readErr != nil {
|
|
||||||
return "", readErr
|
|
||||||
}
|
|
||||||
envStrings := strings.Split(string(data), "\n")
|
|
||||||
for _, envItem := range envStrings {
|
|
||||||
envItem = strings.TrimSuffix(envItem, "\r")
|
|
||||||
envKeyValue := strings.Split(envItem, "=")
|
|
||||||
if strings.EqualFold(strings.TrimSpace(envKeyValue[0]), key) {
|
|
||||||
runtimeEnv = strings.TrimSpace(envKeyValue[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return runtimeEnv, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetGOBIN returns GOBIN environment variable as a string. It will NOT be empty.
|
|
||||||
func GetGOBIN() string {
|
|
||||||
// The one set by user explicitly by `export GOBIN=/path` or `env GOBIN=/path command`
|
|
||||||
GOBIN := os.Getenv("GOBIN")
|
|
||||||
if GOBIN == "" {
|
|
||||||
var err error
|
|
||||||
// The one set by user by running `go env -w GOBIN=/path`
|
|
||||||
GOBIN, err = GetRuntimeEnv("GOBIN")
|
|
||||||
if err != nil {
|
|
||||||
// The default one that Golang uses
|
|
||||||
return filepath.Join(build.Default.GOPATH, "bin")
|
|
||||||
}
|
|
||||||
if GOBIN == "" {
|
|
||||||
return filepath.Join(build.Default.GOPATH, "bin")
|
|
||||||
}
|
|
||||||
return GOBIN
|
|
||||||
}
|
|
||||||
return GOBIN
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
pwd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Can not get current working directory.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
GOBIN := GetGOBIN()
|
|
||||||
binPath := os.Getenv("PATH")
|
|
||||||
pathSlice := []string{pwd, GOBIN, binPath}
|
|
||||||
binPath = strings.Join(pathSlice, string(os.PathListSeparator))
|
|
||||||
os.Setenv("PATH", binPath)
|
|
||||||
|
|
||||||
suffix := ""
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
suffix = ".exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
protoc := "protoc"
|
|
||||||
|
|
||||||
if linkPath, err := os.Readlink(protoc); err == nil {
|
|
||||||
protoc = linkPath
|
|
||||||
}
|
|
||||||
|
|
||||||
protoFilesMap := make(map[string][]string)
|
|
||||||
walkErr := filepath.Walk("./", func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
dir := filepath.Dir(path)
|
|
||||||
filename := filepath.Base(path)
|
|
||||||
if strings.HasSuffix(filename, ".proto") &&
|
|
||||||
filename != "typed_message.proto" &&
|
|
||||||
filename != "descriptor.proto" {
|
|
||||||
protoFilesMap[dir] = append(protoFilesMap[dir], path)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if walkErr != nil {
|
|
||||||
fmt.Println(walkErr)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, files := range protoFilesMap {
|
|
||||||
for _, relProtoFile := range files {
|
|
||||||
args := []string{
|
|
||||||
"-I", ".",
|
|
||||||
"--go_out", pwd,
|
|
||||||
"--go_opt", "paths=source_relative",
|
|
||||||
"--go-grpc_out", pwd,
|
|
||||||
"--go-grpc_opt", "paths=source_relative",
|
|
||||||
"--plugin", "protoc-gen-go=" + filepath.Join(GOBIN, "protoc-gen-go"+suffix),
|
|
||||||
"--plugin", "protoc-gen-go-grpc=" + filepath.Join(GOBIN, "protoc-gen-go-grpc"+suffix),
|
|
||||||
}
|
|
||||||
args = append(args, relProtoFile)
|
|
||||||
cmd := exec.Command(protoc, args...)
|
|
||||||
cmd.Env = append(cmd.Env, os.Environ()...)
|
|
||||||
output, cmdErr := cmd.CombinedOutput()
|
|
||||||
if len(output) > 0 {
|
|
||||||
fmt.Println(string(output))
|
|
||||||
}
|
|
||||||
if cmdErr != nil {
|
|
||||||
fmt.Println(cmdErr)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
normalizeWalkErr := filepath.Walk("./", func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
filename := filepath.Base(path)
|
|
||||||
if strings.HasSuffix(filename, ".pb.go") &&
|
|
||||||
path != "config.pb.go" {
|
|
||||||
if err := NormalizeGeneratedProtoFile(path); err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if normalizeWalkErr != nil {
|
|
||||||
fmt.Println(normalizeWalkErr)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NormalizeGeneratedProtoFile(path string) error {
|
|
||||||
fd, err := os.OpenFile(path, os.O_RDWR, 0o644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = fd.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
out := bytes.NewBuffer(nil)
|
|
||||||
scanner := bufio.NewScanner(fd)
|
|
||||||
valid := false
|
|
||||||
for scanner.Scan() {
|
|
||||||
if !valid && !strings.HasPrefix(scanner.Text(), "package ") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
valid = true
|
|
||||||
out.Write(scanner.Bytes())
|
|
||||||
out.Write([]byte("\n"))
|
|
||||||
}
|
|
||||||
_, err = fd.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = fd.Truncate(0)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = io.Copy(fd, bytes.NewReader(out.Bytes()))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,12 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box"
|
"github.com/sagernet/sing-box"
|
||||||
|
"github.com/sagernet/sing-box/common/json"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -12,26 +15,24 @@ import (
|
|||||||
var commandCheck = &cobra.Command{
|
var commandCheck = &cobra.Command{
|
||||||
Use: "check",
|
Use: "check",
|
||||||
Short: "Check configuration",
|
Short: "Check configuration",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: checkConfiguration,
|
||||||
err := check()
|
Args: cobra.NoArgs,
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func checkConfiguration(cmd *cobra.Command, args []string) {
|
||||||
mainCommand.AddCommand(commandCheck)
|
configContent, err := os.ReadFile(configPath)
|
||||||
}
|
|
||||||
|
|
||||||
func check() error {
|
|
||||||
options, err := readConfig()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
log.Fatal("read config: ", err)
|
||||||
|
}
|
||||||
|
var options option.Options
|
||||||
|
err = json.Unmarshal(configContent, &options)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("decode config: ", err)
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
_, err = box.New(ctx, options)
|
_, err = box.New(ctx, options)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("create service: ", err)
|
||||||
|
}
|
||||||
cancel()
|
cancel()
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ 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"
|
"github.com/sagernet/sing-box/option"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -18,54 +17,47 @@ var commandFormatFlagWrite bool
|
|||||||
var commandFormat = &cobra.Command{
|
var commandFormat = &cobra.Command{
|
||||||
Use: "format",
|
Use: "format",
|
||||||
Short: "Format configuration",
|
Short: "Format configuration",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: formatConfiguration,
|
||||||
err := format()
|
Args: cobra.NoArgs,
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
commandFormat.Flags().BoolVarP(&commandFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout")
|
commandFormat.Flags().BoolVarP(&commandFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout")
|
||||||
mainCommand.AddCommand(commandFormat)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func format() error {
|
func formatConfiguration(cmd *cobra.Command, args []string) {
|
||||||
configContent, err := os.ReadFile(configPath)
|
configContent, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "read config")
|
log.Fatal("read config: ", err)
|
||||||
}
|
}
|
||||||
var options option.Options
|
var options option.Options
|
||||||
err = options.UnmarshalJSON(configContent)
|
err = json.Unmarshal(configContent, &options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "decode config")
|
log.Fatal("decode config: ", err)
|
||||||
}
|
}
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
encoder := json.NewEncoder(buffer)
|
encoder := json.NewEncoder(buffer)
|
||||||
encoder.SetIndent("", " ")
|
encoder.SetIndent("", " ")
|
||||||
err = encoder.Encode(options)
|
err = encoder.Encode(options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "encode config")
|
log.Fatal("encode config: ", err)
|
||||||
}
|
}
|
||||||
if !commandFormatFlagWrite {
|
if !commandFormatFlagWrite {
|
||||||
os.Stdout.WriteString(buffer.String() + "\n")
|
os.Stdout.WriteString(buffer.String() + "\n")
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
if bytes.Equal(configContent, buffer.Bytes()) {
|
if bytes.Equal(configContent, buffer.Bytes()) {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
output, err := os.Create(configPath)
|
output, err := os.Create(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "open output")
|
log.Fatal("open output: ", err)
|
||||||
}
|
}
|
||||||
_, err = output.Write(buffer.Bytes())
|
_, err = output.Write(buffer.Bytes())
|
||||||
output.Close()
|
output.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "write output")
|
log.Fatal("write output: ", err)
|
||||||
}
|
}
|
||||||
outputPath, _ := filepath.Abs(configPath)
|
outputPath, _ := filepath.Abs(configPath)
|
||||||
os.Stderr.WriteString(outputPath + "\n")
|
os.Stderr.WriteString(outputPath + "\n")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
runtimeDebug "runtime/debug"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box"
|
"github.com/sagernet/sing-box"
|
||||||
|
"github.com/sagernet/sing-box/common/json"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common/debug"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -19,43 +20,25 @@ import (
|
|||||||
var commandRun = &cobra.Command{
|
var commandRun = &cobra.Command{
|
||||||
Use: "run",
|
Use: "run",
|
||||||
Short: "Run service",
|
Short: "Run service",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: run,
|
||||||
err := run()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func run(cmd *cobra.Command, args []string) {
|
||||||
mainCommand.AddCommand(commandRun)
|
err := run0()
|
||||||
}
|
|
||||||
|
|
||||||
func readConfig() (option.Options, error) {
|
|
||||||
var (
|
|
||||||
configContent []byte
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if configPath == "stdin" {
|
|
||||||
configContent, err = io.ReadAll(os.Stdin)
|
|
||||||
} else {
|
|
||||||
configContent, err = os.ReadFile(configPath)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return option.Options{}, E.Cause(err, "read config")
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run0() error {
|
||||||
|
configContent, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "read config")
|
||||||
}
|
}
|
||||||
var options option.Options
|
var options option.Options
|
||||||
err = options.UnmarshalJSON(configContent)
|
err = json.Unmarshal(configContent, &options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return option.Options{}, E.Cause(err, "decode config")
|
return E.Cause(err, "decode config")
|
||||||
}
|
|
||||||
return options, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func create() (*box.Box, context.CancelFunc, error) {
|
|
||||||
options, err := readConfig()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
}
|
||||||
if disableColor {
|
if disableColor {
|
||||||
if options.Log == nil {
|
if options.Log == nil {
|
||||||
@@ -67,55 +50,23 @@ func create() (*box.Box, context.CancelFunc, error) {
|
|||||||
instance, err := box.New(ctx, options)
|
instance, err := box.New(ctx, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
return nil, nil, E.Cause(err, "create service")
|
return E.Cause(err, "create service")
|
||||||
}
|
}
|
||||||
|
|
||||||
osSignals := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
|
|
||||||
defer func() {
|
|
||||||
signal.Stop(osSignals)
|
|
||||||
close(osSignals)
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
_, loaded := <-osSignals
|
|
||||||
if loaded {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
err = instance.Start()
|
err = instance.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
return nil, nil, E.Cause(err, "start service")
|
return E.Cause(err, "start service")
|
||||||
}
|
}
|
||||||
return instance, cancel, nil
|
if debug.Enabled {
|
||||||
}
|
http.HandleFunc("/debug/close", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
|
||||||
func run() error {
|
|
||||||
osSignals := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
|
|
||||||
defer signal.Stop(osSignals)
|
|
||||||
for {
|
|
||||||
instance, cancel, err := create()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
runtimeDebug.FreeOSMemory()
|
|
||||||
for {
|
|
||||||
osSignal := <-osSignals
|
|
||||||
if osSignal == syscall.SIGHUP {
|
|
||||||
err = check()
|
|
||||||
if err != nil {
|
|
||||||
log.Error(E.Cause(err, "reload service"))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cancel()
|
cancel()
|
||||||
instance.Close()
|
instance.Close()
|
||||||
if osSignal != syscall.SIGHUP {
|
})
|
||||||
return nil
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
osSignals := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM)
|
||||||
|
<-osSignals
|
||||||
|
cancel()
|
||||||
|
instance.Close()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
|
||||||
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -21,44 +21,33 @@ var nameOnly bool
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
commandVersion.Flags().BoolVarP(&nameOnly, "name", "n", false, "print version name only")
|
commandVersion.Flags().BoolVarP(&nameOnly, "name", "n", false, "print version name only")
|
||||||
mainCommand.AddCommand(commandVersion)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func printVersion(cmd *cobra.Command, args []string) {
|
func printVersion(cmd *cobra.Command, args []string) {
|
||||||
if nameOnly {
|
var version string
|
||||||
os.Stdout.WriteString(C.Version + "\n")
|
if !nameOnly {
|
||||||
return
|
version = "sing-box "
|
||||||
}
|
}
|
||||||
version := "sing-box version " + C.Version + "\n\n"
|
version += F.ToString(C.Version)
|
||||||
version += "Environment: " + runtime.Version() + " " + runtime.GOOS + "/" + runtime.GOARCH + "\n"
|
if C.Commit != "" {
|
||||||
|
version += "." + C.Commit
|
||||||
var tags string
|
}
|
||||||
var revision string
|
if !nameOnly {
|
||||||
|
version += " ("
|
||||||
debugInfo, loaded := debug.ReadBuildInfo()
|
version += runtime.Version()
|
||||||
if loaded {
|
version += ", "
|
||||||
for _, setting := range debugInfo.Settings {
|
version += runtime.GOOS
|
||||||
switch setting.Key {
|
version += ", "
|
||||||
case "-tags":
|
version += runtime.GOARCH
|
||||||
tags = setting.Value
|
version += ", "
|
||||||
case "vcs.revision":
|
version += "CGO "
|
||||||
revision = setting.Value
|
if C.CGO_ENABLED {
|
||||||
}
|
version += "enabled"
|
||||||
|
} else {
|
||||||
|
version += "disabled"
|
||||||
}
|
}
|
||||||
|
version += ")"
|
||||||
}
|
}
|
||||||
|
version += "\n"
|
||||||
if tags != "" {
|
|
||||||
version += "Tags: " + tags + "\n"
|
|
||||||
}
|
|
||||||
if revision != "" {
|
|
||||||
version += "Revision: " + revision + "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
if C.CGO_ENABLED {
|
|
||||||
version += "CGO: enabled\n"
|
|
||||||
} else {
|
|
||||||
version += "CGO: disabled\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Stdout.WriteString(version)
|
os.Stdout.WriteString(version)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
_ "github.com/sagernet/sing-box/include"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -24,6 +23,11 @@ func init() {
|
|||||||
mainCommand.PersistentFlags().StringVarP(&configPath, "config", "c", "config.json", "set configuration file path")
|
mainCommand.PersistentFlags().StringVarP(&configPath, "config", "c", "config.json", "set configuration file path")
|
||||||
mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
|
mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
|
||||||
mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
|
mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
|
||||||
|
|
||||||
|
mainCommand.AddCommand(commandRun)
|
||||||
|
mainCommand.AddCommand(commandCheck)
|
||||||
|
mainCommand.AddCommand(commandFormat)
|
||||||
|
mainCommand.AddCommand(commandVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
//go:build go1.19 && !go1.20
|
|
||||||
|
|
||||||
package badtls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/binary"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"reflect"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
|
||||||
"github.com/sagernet/sing/common/bufio"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Conn struct {
|
|
||||||
*tls.Conn
|
|
||||||
writer N.ExtendedWriter
|
|
||||||
activeCall *int32
|
|
||||||
closeNotifySent *bool
|
|
||||||
version *uint16
|
|
||||||
rand io.Reader
|
|
||||||
halfAccess *sync.Mutex
|
|
||||||
halfError *error
|
|
||||||
cipher cipher.AEAD
|
|
||||||
explicitNonceLen int
|
|
||||||
halfPtr uintptr
|
|
||||||
halfSeq []byte
|
|
||||||
halfScratchBuf []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func Create(conn *tls.Conn) (TLSConn, error) {
|
|
||||||
if !handshakeComplete(conn) {
|
|
||||||
return nil, E.New("handshake not finished")
|
|
||||||
}
|
|
||||||
rawConn := reflect.Indirect(reflect.ValueOf(conn))
|
|
||||||
rawActiveCall := rawConn.FieldByName("activeCall")
|
|
||||||
if !rawActiveCall.IsValid() || rawActiveCall.Kind() != reflect.Int32 {
|
|
||||||
return nil, E.New("badtls: invalid active call")
|
|
||||||
}
|
|
||||||
activeCall := (*int32)(unsafe.Pointer(rawActiveCall.UnsafeAddr()))
|
|
||||||
rawHalfConn := rawConn.FieldByName("out")
|
|
||||||
if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct {
|
|
||||||
return nil, E.New("badtls: invalid half conn")
|
|
||||||
}
|
|
||||||
rawVersion := rawConn.FieldByName("vers")
|
|
||||||
if !rawVersion.IsValid() || rawVersion.Kind() != reflect.Uint16 {
|
|
||||||
return nil, E.New("badtls: invalid version")
|
|
||||||
}
|
|
||||||
version := (*uint16)(unsafe.Pointer(rawVersion.UnsafeAddr()))
|
|
||||||
rawCloseNotifySent := rawConn.FieldByName("closeNotifySent")
|
|
||||||
if !rawCloseNotifySent.IsValid() || rawCloseNotifySent.Kind() != reflect.Bool {
|
|
||||||
return nil, E.New("badtls: invalid notify")
|
|
||||||
}
|
|
||||||
closeNotifySent := (*bool)(unsafe.Pointer(rawCloseNotifySent.UnsafeAddr()))
|
|
||||||
rawConfig := reflect.Indirect(rawConn.FieldByName("config"))
|
|
||||||
if !rawConfig.IsValid() || rawConfig.Kind() != reflect.Struct {
|
|
||||||
return nil, E.New("badtls: bad config")
|
|
||||||
}
|
|
||||||
config := (*tls.Config)(unsafe.Pointer(rawConfig.UnsafeAddr()))
|
|
||||||
randReader := config.Rand
|
|
||||||
if randReader == nil {
|
|
||||||
randReader = rand.Reader
|
|
||||||
}
|
|
||||||
rawHalfMutex := rawHalfConn.FieldByName("Mutex")
|
|
||||||
if !rawHalfMutex.IsValid() || rawHalfMutex.Kind() != reflect.Struct {
|
|
||||||
return nil, E.New("badtls: invalid half mutex")
|
|
||||||
}
|
|
||||||
halfAccess := (*sync.Mutex)(unsafe.Pointer(rawHalfMutex.UnsafeAddr()))
|
|
||||||
rawHalfError := rawHalfConn.FieldByName("err")
|
|
||||||
if !rawHalfError.IsValid() || rawHalfError.Kind() != reflect.Interface {
|
|
||||||
return nil, E.New("badtls: invalid half error")
|
|
||||||
}
|
|
||||||
halfError := (*error)(unsafe.Pointer(rawHalfError.UnsafeAddr()))
|
|
||||||
rawHalfCipherInterface := rawHalfConn.FieldByName("cipher")
|
|
||||||
if !rawHalfCipherInterface.IsValid() || rawHalfCipherInterface.Kind() != reflect.Interface {
|
|
||||||
return nil, E.New("badtls: invalid cipher interface")
|
|
||||||
}
|
|
||||||
rawHalfCipher := rawHalfCipherInterface.Elem()
|
|
||||||
aeadCipher, loaded := valueInterface(rawHalfCipher, false).(cipher.AEAD)
|
|
||||||
if !loaded {
|
|
||||||
return nil, E.New("badtls: invalid AEAD cipher")
|
|
||||||
}
|
|
||||||
var explicitNonceLen int
|
|
||||||
switch cipherName := reflect.Indirect(rawHalfCipher).Type().String(); cipherName {
|
|
||||||
case "tls.prefixNonceAEAD":
|
|
||||||
explicitNonceLen = aeadCipher.NonceSize()
|
|
||||||
case "tls.xorNonceAEAD":
|
|
||||||
default:
|
|
||||||
return nil, E.New("badtls: unknown cipher type: ", cipherName)
|
|
||||||
}
|
|
||||||
rawHalfSeq := rawHalfConn.FieldByName("seq")
|
|
||||||
if !rawHalfSeq.IsValid() || rawHalfSeq.Kind() != reflect.Array {
|
|
||||||
return nil, E.New("badtls: invalid seq")
|
|
||||||
}
|
|
||||||
halfSeq := rawHalfSeq.Bytes()
|
|
||||||
rawHalfScratchBuf := rawHalfConn.FieldByName("scratchBuf")
|
|
||||||
if !rawHalfScratchBuf.IsValid() || rawHalfScratchBuf.Kind() != reflect.Array {
|
|
||||||
return nil, E.New("badtls: invalid scratchBuf")
|
|
||||||
}
|
|
||||||
halfScratchBuf := rawHalfScratchBuf.Bytes()
|
|
||||||
return &Conn{
|
|
||||||
Conn: conn,
|
|
||||||
writer: bufio.NewExtendedWriter(conn.NetConn()),
|
|
||||||
activeCall: activeCall,
|
|
||||||
closeNotifySent: closeNotifySent,
|
|
||||||
version: version,
|
|
||||||
halfAccess: halfAccess,
|
|
||||||
halfError: halfError,
|
|
||||||
cipher: aeadCipher,
|
|
||||||
explicitNonceLen: explicitNonceLen,
|
|
||||||
rand: randReader,
|
|
||||||
halfPtr: rawHalfConn.UnsafeAddr(),
|
|
||||||
halfSeq: halfSeq,
|
|
||||||
halfScratchBuf: halfScratchBuf,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) WriteBuffer(buffer *buf.Buffer) error {
|
|
||||||
if buffer.Len() > maxPlaintext {
|
|
||||||
defer buffer.Release()
|
|
||||||
return common.Error(c.Write(buffer.Bytes()))
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
x := atomic.LoadInt32(c.activeCall)
|
|
||||||
if x&1 != 0 {
|
|
||||||
return net.ErrClosed
|
|
||||||
}
|
|
||||||
if atomic.CompareAndSwapInt32(c.activeCall, x, x+2) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer atomic.AddInt32(c.activeCall, -2)
|
|
||||||
c.halfAccess.Lock()
|
|
||||||
defer c.halfAccess.Unlock()
|
|
||||||
if err := *c.halfError; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if *c.closeNotifySent {
|
|
||||||
return errShutdown
|
|
||||||
}
|
|
||||||
dataLen := buffer.Len()
|
|
||||||
dataBytes := buffer.Bytes()
|
|
||||||
outBuf := buffer.ExtendHeader(recordHeaderLen + c.explicitNonceLen)
|
|
||||||
outBuf[0] = 23
|
|
||||||
version := *c.version
|
|
||||||
if version == 0 {
|
|
||||||
version = tls.VersionTLS10
|
|
||||||
} else if version == tls.VersionTLS13 {
|
|
||||||
version = tls.VersionTLS12
|
|
||||||
}
|
|
||||||
binary.BigEndian.PutUint16(outBuf[1:], version)
|
|
||||||
var nonce []byte
|
|
||||||
if c.explicitNonceLen > 0 {
|
|
||||||
nonce = outBuf[5 : 5+c.explicitNonceLen]
|
|
||||||
if c.explicitNonceLen < 16 {
|
|
||||||
copy(nonce, c.halfSeq)
|
|
||||||
} else {
|
|
||||||
if _, err := io.ReadFull(c.rand, nonce); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(nonce) == 0 {
|
|
||||||
nonce = c.halfSeq
|
|
||||||
}
|
|
||||||
if *c.version == tls.VersionTLS13 {
|
|
||||||
buffer.FreeBytes()[0] = 23
|
|
||||||
binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen+1+c.cipher.Overhead()))
|
|
||||||
c.cipher.Seal(outBuf, nonce, outBuf[recordHeaderLen:recordHeaderLen+c.explicitNonceLen+dataLen+1], outBuf[:recordHeaderLen])
|
|
||||||
buffer.Extend(1 + c.cipher.Overhead())
|
|
||||||
} else {
|
|
||||||
binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen))
|
|
||||||
additionalData := append(c.halfScratchBuf[:0], c.halfSeq...)
|
|
||||||
additionalData = append(additionalData, outBuf[:recordHeaderLen]...)
|
|
||||||
c.cipher.Seal(outBuf, nonce, dataBytes, additionalData)
|
|
||||||
buffer.Extend(c.cipher.Overhead())
|
|
||||||
binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen+c.explicitNonceLen+c.cipher.Overhead()))
|
|
||||||
}
|
|
||||||
incSeq(c.halfPtr)
|
|
||||||
return c.writer.WriteBuffer(buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) FrontHeadroom() int {
|
|
||||||
return recordHeaderLen + c.explicitNonceLen
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) RearHeadroom() int {
|
|
||||||
return 1 + c.cipher.Overhead()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) WriterMTU() int {
|
|
||||||
return maxPlaintext
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) Upstream() any {
|
|
||||||
return c.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) UpstreamWriter() any {
|
|
||||||
return c.NetConn()
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
//go:build !go1.19 || go1.20
|
|
||||||
|
|
||||||
package badtls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Create(conn *tls.Conn) (TLSConn, error) {
|
|
||||||
return nil, os.ErrInvalid
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package badtls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TLSConn interface {
|
|
||||||
net.Conn
|
|
||||||
HandshakeContext(ctx context.Context) error
|
|
||||||
ConnectionState() tls.ConnectionState
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
//go:build go1.19 && !go.1.20
|
|
||||||
|
|
||||||
package badtls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"reflect"
|
|
||||||
_ "unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
maxPlaintext = 16384 // maximum plaintext payload length
|
|
||||||
recordHeaderLen = 5 // record header length
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:linkname errShutdown crypto/tls.errShutdown
|
|
||||||
var errShutdown error
|
|
||||||
|
|
||||||
//go:linkname handshakeComplete crypto/tls.(*Conn).handshakeComplete
|
|
||||||
func handshakeComplete(conn *tls.Conn) bool
|
|
||||||
|
|
||||||
//go:linkname incSeq crypto/tls.(*halfConn).incSeq
|
|
||||||
func incSeq(conn uintptr)
|
|
||||||
|
|
||||||
//go:linkname valueInterface reflect.valueInterface
|
|
||||||
func valueInterface(v reflect.Value, safe bool) any
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,11 +10,10 @@ import (
|
|||||||
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"
|
|
||||||
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/database64128/tfo-go/v2"
|
"github.com/database64128/tfo-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
var warnBindInterfaceOnUnsupportedPlatform = warning.New(
|
var warnBindInterfaceOnUnsupportedPlatform = warning.New(
|
||||||
@@ -53,13 +52,8 @@ var warnTFOOnUnsupportedPlatform = warning.New(
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DefaultDialer struct {
|
type DefaultDialer struct {
|
||||||
dialer4 tfo.Dialer
|
tfo.Dialer
|
||||||
dialer6 tfo.Dialer
|
net.ListenConfig
|
||||||
udpDialer4 net.Dialer
|
|
||||||
udpDialer6 net.Dialer
|
|
||||||
udpListener net.ListenConfig
|
|
||||||
udpAddr4 string
|
|
||||||
udpAddr6 string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDialer {
|
func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDialer {
|
||||||
@@ -67,23 +61,25 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
|
|||||||
var listener net.ListenConfig
|
var listener net.ListenConfig
|
||||||
if options.BindInterface != "" {
|
if options.BindInterface != "" {
|
||||||
warnBindInterfaceOnUnsupportedPlatform.Check()
|
warnBindInterfaceOnUnsupportedPlatform.Check()
|
||||||
bindFunc := control.BindToInterface(router.InterfaceFinder(), options.BindInterface, -1)
|
bindFunc := control.BindToInterface(router.InterfaceBindManager(), options.BindInterface)
|
||||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
listener.Control = control.Append(listener.Control, bindFunc)
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
} else if router.AutoDetectInterface() {
|
} else if router.AutoDetectInterface() {
|
||||||
const useInterfaceName = C.IsLinux
|
if C.IsWindows {
|
||||||
bindFunc := control.BindToInterfaceFunc(router.InterfaceFinder(), func(network string, address string) (interfaceName string, interfaceIndex int) {
|
bindFunc := control.BindToInterfaceIndexFunc(func() int {
|
||||||
remoteAddr := M.ParseSocksaddr(address).Addr
|
return router.InterfaceMonitor().DefaultInterfaceIndex()
|
||||||
if C.IsLinux {
|
})
|
||||||
return router.InterfaceMonitor().DefaultInterfaceName(remoteAddr), -1
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
} else {
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
return "", router.InterfaceMonitor().DefaultInterfaceIndex(remoteAddr)
|
} else {
|
||||||
}
|
bindFunc := control.BindToInterfaceFunc(router.InterfaceBindManager(), func() string {
|
||||||
})
|
return router.InterfaceMonitor().DefaultInterfaceName()
|
||||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
})
|
||||||
listener.Control = control.Append(listener.Control, bindFunc)
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
|
}
|
||||||
} else if router.DefaultInterface() != "" {
|
} else if router.DefaultInterface() != "" {
|
||||||
bindFunc := control.BindToInterface(router.InterfaceFinder(), router.DefaultInterface(), -1)
|
bindFunc := control.BindToInterface(router.InterfaceBindManager(), router.DefaultInterface())
|
||||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
listener.Control = control.Append(listener.Control, bindFunc)
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
}
|
}
|
||||||
@@ -112,72 +108,17 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
|
|||||||
if options.TCPFastOpen {
|
if options.TCPFastOpen {
|
||||||
warnTFOOnUnsupportedPlatform.Check()
|
warnTFOOnUnsupportedPlatform.Check()
|
||||||
}
|
}
|
||||||
var udpFragment bool
|
return &DefaultDialer{tfo.Dialer{Dialer: dialer, DisableTFO: !options.TCPFastOpen}, listener}
|
||||||
if options.UDPFragment != nil {
|
|
||||||
udpFragment = *options.UDPFragment
|
|
||||||
} else {
|
|
||||||
udpFragment = options.UDPFragmentDefault
|
|
||||||
}
|
|
||||||
if !udpFragment {
|
|
||||||
dialer.Control = control.Append(dialer.Control, control.DisableUDPFragment())
|
|
||||||
listener.Control = control.Append(listener.Control, control.DisableUDPFragment())
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
dialer4 = dialer
|
|
||||||
udpDialer4 = dialer
|
|
||||||
udpAddr4 string
|
|
||||||
)
|
|
||||||
if options.Inet4BindAddress != nil {
|
|
||||||
bindAddr := options.Inet4BindAddress.Build()
|
|
||||||
dialer4.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
|
|
||||||
udpDialer4.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
|
|
||||||
udpAddr4 = M.SocksaddrFrom(bindAddr, 0).String()
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
dialer6 = dialer
|
|
||||||
udpDialer6 = dialer
|
|
||||||
udpAddr6 string
|
|
||||||
)
|
|
||||||
if options.Inet6BindAddress != nil {
|
|
||||||
bindAddr := options.Inet6BindAddress.Build()
|
|
||||||
dialer6.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
|
|
||||||
udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
|
|
||||||
udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()
|
|
||||||
}
|
|
||||||
return &DefaultDialer{
|
|
||||||
tfo.Dialer{Dialer: dialer4, DisableTFO: !options.TCPFastOpen},
|
|
||||||
tfo.Dialer{Dialer: dialer6, DisableTFO: !options.TCPFastOpen},
|
|
||||||
udpDialer4,
|
|
||||||
udpDialer6,
|
|
||||||
listener,
|
|
||||||
udpAddr4,
|
|
||||||
udpAddr6,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
if !address.IsValid() {
|
return d.Dialer.DialContext(ctx, network, address.Unwrap().String())
|
||||||
return nil, E.New("invalid address")
|
|
||||||
}
|
|
||||||
switch N.NetworkName(network) {
|
|
||||||
case N.NetworkUDP:
|
|
||||||
if !address.IsIPv6() {
|
|
||||||
return d.udpDialer4.DialContext(ctx, network, address.String())
|
|
||||||
} else {
|
|
||||||
return d.udpDialer6.DialContext(ctx, network, address.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !address.IsIPv6() {
|
|
||||||
return DialSlowContext(&d.dialer4, ctx, network, address)
|
|
||||||
} else {
|
|
||||||
return DialSlowContext(&d.dialer6, ctx, network, 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() {
|
return d.ListenConfig.ListenPacket(ctx, N.NetworkUDP, "")
|
||||||
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4)
|
}
|
||||||
} else {
|
|
||||||
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6)
|
func (d *DefaultDialer) Upstream() any {
|
||||||
}
|
return &d.Dialer
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func New(router adapter.Router, options option.DialerOptions) N.Dialer {
|
func New(router adapter.Router, options option.DialerOptions) N.Dialer {
|
||||||
var dialer N.Dialer
|
|
||||||
if options.Detour == "" {
|
if options.Detour == "" {
|
||||||
dialer = NewDefault(router, options)
|
return NewDefault(router, options)
|
||||||
} else {
|
} else {
|
||||||
dialer = NewDetour(router, options.Detour)
|
return NewDetour(router, options.Detour)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOutbound(router adapter.Router, options option.OutboundDialerOptions) N.Dialer {
|
||||||
|
dialer := New(router, options.DialerOptions)
|
||||||
domainStrategy := dns.DomainStrategy(options.DomainStrategy)
|
domainStrategy := dns.DomainStrategy(options.DomainStrategy)
|
||||||
if domainStrategy != dns.DomainStrategyAsIS || options.Detour == "" {
|
if domainStrategy != dns.DomainStrategyAsIS || options.Detour == "" {
|
||||||
dialer = NewResolveDialer(router, dialer, domainStrategy, time.Duration(options.FallbackDelay))
|
dialer = NewResolveDialer(router, dialer, domainStrategy, time.Duration(options.FallbackDelay))
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func (d *ResolveDialer) DialContext(ctx context.Context, network string, destina
|
|||||||
}
|
}
|
||||||
|
|
||||||
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() || destination.Fqdn == "" {
|
||||||
return d.dialer.ListenPacket(ctx, destination)
|
return d.dialer.ListenPacket(ctx, destination)
|
||||||
}
|
}
|
||||||
ctx, metadata := adapter.AppendContext(ctx)
|
ctx, metadata := adapter.AppendContext(ctx)
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
package dialer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing/common/bufio"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
|
|
||||||
"github.com/database64128/tfo-go/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type slowOpenConn struct {
|
|
||||||
dialer *tfo.Dialer
|
|
||||||
ctx context.Context
|
|
||||||
network string
|
|
||||||
destination M.Socksaddr
|
|
||||||
conn net.Conn
|
|
||||||
create chan struct{}
|
|
||||||
err 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 {
|
|
||||||
return dialer.DialContext(ctx, network, destination.String(), nil)
|
|
||||||
}
|
|
||||||
return &slowOpenConn{
|
|
||||||
dialer: dialer,
|
|
||||||
ctx: ctx,
|
|
||||||
network: network,
|
|
||||||
destination: destination,
|
|
||||||
create: make(chan struct{}),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *slowOpenConn) Read(b []byte) (n int, err error) {
|
|
||||||
if c.conn == nil {
|
|
||||||
select {
|
|
||||||
case <-c.create:
|
|
||||||
if c.err != nil {
|
|
||||||
return 0, c.err
|
|
||||||
}
|
|
||||||
case <-c.ctx.Done():
|
|
||||||
return 0, c.ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c.conn.Read(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *slowOpenConn) Write(b []byte) (n int, err error) {
|
|
||||||
if c.conn == nil {
|
|
||||||
c.conn, err = c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
|
|
||||||
if err != nil {
|
|
||||||
c.err = E.Cause(err, "dial tcp fast open")
|
|
||||||
}
|
|
||||||
close(c.create)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return c.conn.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *slowOpenConn) Close() error {
|
|
||||||
return common.Close(c.conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *slowOpenConn) LocalAddr() net.Addr {
|
|
||||||
if c.conn == nil {
|
|
||||||
return M.Socksaddr{}
|
|
||||||
}
|
|
||||||
return c.conn.LocalAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *slowOpenConn) RemoteAddr() net.Addr {
|
|
||||||
if c.conn == nil {
|
|
||||||
return M.Socksaddr{}
|
|
||||||
}
|
|
||||||
return c.conn.RemoteAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *slowOpenConn) SetDeadline(t time.Time) error {
|
|
||||||
if c.conn == nil {
|
|
||||||
return os.ErrInvalid
|
|
||||||
}
|
|
||||||
return c.conn.SetDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *slowOpenConn) SetReadDeadline(t time.Time) error {
|
|
||||||
if c.conn == nil {
|
|
||||||
return os.ErrInvalid
|
|
||||||
}
|
|
||||||
return c.conn.SetReadDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *slowOpenConn) SetWriteDeadline(t time.Time) error {
|
|
||||||
if c.conn == nil {
|
|
||||||
return os.ErrInvalid
|
|
||||||
}
|
|
||||||
return c.conn.SetWriteDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *slowOpenConn) Upstream() any {
|
|
||||||
return c.conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *slowOpenConn) ReaderReplaceable() bool {
|
|
||||||
return c.conn != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *slowOpenConn) WriterReplaceable() bool {
|
|
||||||
return c.conn != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *slowOpenConn) LazyHeadroom() bool {
|
|
||||||
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) {
|
|
||||||
if c.conn == nil {
|
|
||||||
select {
|
|
||||||
case <-c.create:
|
|
||||||
if c.err != nil {
|
|
||||||
return 0, c.err
|
|
||||||
}
|
|
||||||
case <-c.ctx.Done():
|
|
||||||
return 0, c.ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bufio.Copy(w, c.conn)
|
|
||||||
}
|
|
||||||
@@ -1,49 +1,30 @@
|
|||||||
package tls
|
package dialer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
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"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
type STDClientConfig struct {
|
type TLSDialer struct {
|
||||||
|
dialer N.Dialer
|
||||||
config *tls.Config
|
config *tls.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *STDClientConfig) ServerName() string {
|
func NewTLS(dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
|
||||||
return s.config.ServerName
|
if !options.Enabled {
|
||||||
}
|
return dialer, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *STDClientConfig) SetServerName(serverName string) {
|
|
||||||
s.config.ServerName = serverName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *STDClientConfig) NextProtos() []string {
|
|
||||||
return s.config.NextProtos
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *STDClientConfig) SetNextProtos(nextProto []string) {
|
|
||||||
s.config.NextProtos = nextProto
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *STDClientConfig) Config() (*STDConfig, error) {
|
|
||||||
return s.config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *STDClientConfig) Client(conn net.Conn) Conn {
|
|
||||||
return tls.Client(conn, s.config)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *STDClientConfig) Clone() Config {
|
|
||||||
return &STDClientConfig{s.config.Clone()}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSTDClient(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
|
||||||
@@ -52,7 +33,7 @@ func NewSTDClient(serverAddress string, options option.OutboundTLSOptions) (Conf
|
|||||||
serverName = serverAddress
|
serverName = serverAddress
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if serverName == "" && !options.Insecure {
|
if serverName == "" && options.Insecure {
|
||||||
return nil, E.New("missing server_name or insecure=true")
|
return nil, E.New("missing server_name or insecure=true")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,14 +63,14 @@ func NewSTDClient(serverAddress string, options option.OutboundTLSOptions) (Conf
|
|||||||
tlsConfig.NextProtos = options.ALPN
|
tlsConfig.NextProtos = options.ALPN
|
||||||
}
|
}
|
||||||
if options.MinVersion != "" {
|
if options.MinVersion != "" {
|
||||||
minVersion, err := ParseTLSVersion(options.MinVersion)
|
minVersion, err := option.ParseTLSVersion(options.MinVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "parse min_version")
|
return nil, E.Cause(err, "parse min_version")
|
||||||
}
|
}
|
||||||
tlsConfig.MinVersion = minVersion
|
tlsConfig.MinVersion = minVersion
|
||||||
}
|
}
|
||||||
if options.MaxVersion != "" {
|
if options.MaxVersion != "" {
|
||||||
maxVersion, err := ParseTLSVersion(options.MaxVersion)
|
maxVersion, err := option.ParseTLSVersion(options.MaxVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "parse max_version")
|
return nil, E.Cause(err, "parse max_version")
|
||||||
}
|
}
|
||||||
@@ -124,5 +105,27 @@ func NewSTDClient(serverAddress string, options option.OutboundTLSOptions) (Conf
|
|||||||
}
|
}
|
||||||
tlsConfig.RootCAs = certPool
|
tlsConfig.RootCAs = certPool
|
||||||
}
|
}
|
||||||
return &STDClientConfig{&tlsConfig}, nil
|
return &TLSDialer{
|
||||||
|
dialer: dialer,
|
||||||
|
config: &tlsConfig,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *TLSDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
|
if network != N.NetworkTCP {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
conn, err := d.dialer.DialContext(ctx, network, destination)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsConn := tls.Client(conn, d.config)
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
||||||
|
defer cancel()
|
||||||
|
err = tlsConn.HandshakeContext(ctx)
|
||||||
|
return tlsConn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *TLSDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
}
|
}
|
||||||
@@ -20,11 +20,13 @@ func Write(writer io.Writer, domains map[string][]Item) error {
|
|||||||
for _, code := range keys {
|
for _, code := range keys {
|
||||||
index[code] = content.Len()
|
index[code] = content.Len()
|
||||||
for _, domain := range domains[code] {
|
for _, domain := range domains[code] {
|
||||||
content.WriteByte(domain.Type)
|
err := rw.WriteByte(content, domain.Type)
|
||||||
err := rw.WriteVString(content, domain.Value)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err = rw.WriteVString(content, domain.Value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
package json
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// kanged from v2ray
|
|
||||||
|
|
||||||
type commentFilterState = byte
|
|
||||||
|
|
||||||
const (
|
|
||||||
commentFilterStateContent commentFilterState = iota
|
|
||||||
commentFilterStateEscape
|
|
||||||
commentFilterStateDoubleQuote
|
|
||||||
commentFilterStateDoubleQuoteEscape
|
|
||||||
commentFilterStateSingleQuote
|
|
||||||
commentFilterStateSingleQuoteEscape
|
|
||||||
commentFilterStateComment
|
|
||||||
commentFilterStateSlash
|
|
||||||
commentFilterStateMultilineComment
|
|
||||||
commentFilterStateMultilineCommentStar
|
|
||||||
)
|
|
||||||
|
|
||||||
type CommentFilter struct {
|
|
||||||
br *bufio.Reader
|
|
||||||
state commentFilterState
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCommentFilter(reader io.Reader) io.Reader {
|
|
||||||
return &CommentFilter{br: bufio.NewReader(reader)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *CommentFilter) Read(b []byte) (int, error) {
|
|
||||||
p := b[:0]
|
|
||||||
for len(p) < len(b)-2 {
|
|
||||||
x, err := v.br.ReadByte()
|
|
||||||
if err != nil {
|
|
||||||
if len(p) == 0 {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return len(p), nil
|
|
||||||
}
|
|
||||||
switch v.state {
|
|
||||||
case commentFilterStateContent:
|
|
||||||
switch x {
|
|
||||||
case '"':
|
|
||||||
v.state = commentFilterStateDoubleQuote
|
|
||||||
p = append(p, x)
|
|
||||||
case '\'':
|
|
||||||
v.state = commentFilterStateSingleQuote
|
|
||||||
p = append(p, x)
|
|
||||||
case '\\':
|
|
||||||
v.state = commentFilterStateEscape
|
|
||||||
case '#':
|
|
||||||
v.state = commentFilterStateComment
|
|
||||||
case '/':
|
|
||||||
v.state = commentFilterStateSlash
|
|
||||||
default:
|
|
||||||
p = append(p, x)
|
|
||||||
}
|
|
||||||
case commentFilterStateEscape:
|
|
||||||
p = append(p, '\\', x)
|
|
||||||
v.state = commentFilterStateContent
|
|
||||||
case commentFilterStateDoubleQuote:
|
|
||||||
switch x {
|
|
||||||
case '"':
|
|
||||||
v.state = commentFilterStateContent
|
|
||||||
p = append(p, x)
|
|
||||||
case '\\':
|
|
||||||
v.state = commentFilterStateDoubleQuoteEscape
|
|
||||||
default:
|
|
||||||
p = append(p, x)
|
|
||||||
}
|
|
||||||
case commentFilterStateDoubleQuoteEscape:
|
|
||||||
p = append(p, '\\', x)
|
|
||||||
v.state = commentFilterStateDoubleQuote
|
|
||||||
case commentFilterStateSingleQuote:
|
|
||||||
switch x {
|
|
||||||
case '\'':
|
|
||||||
v.state = commentFilterStateContent
|
|
||||||
p = append(p, x)
|
|
||||||
case '\\':
|
|
||||||
v.state = commentFilterStateSingleQuoteEscape
|
|
||||||
default:
|
|
||||||
p = append(p, x)
|
|
||||||
}
|
|
||||||
case commentFilterStateSingleQuoteEscape:
|
|
||||||
p = append(p, '\\', x)
|
|
||||||
v.state = commentFilterStateSingleQuote
|
|
||||||
case commentFilterStateComment:
|
|
||||||
if x == '\n' {
|
|
||||||
v.state = commentFilterStateContent
|
|
||||||
p = append(p, '\n')
|
|
||||||
}
|
|
||||||
case commentFilterStateSlash:
|
|
||||||
switch x {
|
|
||||||
case '/':
|
|
||||||
v.state = commentFilterStateComment
|
|
||||||
case '*':
|
|
||||||
v.state = commentFilterStateMultilineComment
|
|
||||||
default:
|
|
||||||
p = append(p, '/', x)
|
|
||||||
}
|
|
||||||
case commentFilterStateMultilineComment:
|
|
||||||
switch x {
|
|
||||||
case '*':
|
|
||||||
v.state = commentFilterStateMultilineCommentStar
|
|
||||||
case '\n':
|
|
||||||
p = append(p, '\n')
|
|
||||||
}
|
|
||||||
case commentFilterStateMultilineCommentStar:
|
|
||||||
switch x {
|
|
||||||
case '/':
|
|
||||||
v.state = commentFilterStateContent
|
|
||||||
case '*':
|
|
||||||
// Stay
|
|
||||||
case '\n':
|
|
||||||
p = append(p, '\n')
|
|
||||||
default:
|
|
||||||
v.state = commentFilterStateMultilineComment
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
panic("Unknown state.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return len(p), nil
|
|
||||||
}
|
|
||||||
@@ -329,23 +329,6 @@ func (c *ClientPacketConn) Write(b []byte) (n int, err error) {
|
|||||||
return c.ExtendedConn.Write(b)
|
return c.ExtendedConn.Write(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClientPacketConn) ReadBuffer(buffer *buf.Buffer) (err error) {
|
|
||||||
if !c.responseRead {
|
|
||||||
err = c.readResponse()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.responseRead = true
|
|
||||||
}
|
|
||||||
var length uint16
|
|
||||||
err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientPacketConn) WriteBuffer(buffer *buf.Buffer) error {
|
func (c *ClientPacketConn) WriteBuffer(buffer *buf.Buffer) error {
|
||||||
if !c.requestWrite {
|
if !c.requestWrite {
|
||||||
defer buffer.Release()
|
defer buffer.Release()
|
||||||
@@ -360,11 +343,6 @@ func (c *ClientPacketConn) FrontHeadroom() int {
|
|||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClientPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
|
||||||
err = c.ReadBuffer(buffer)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
func (c *ClientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||||
return c.WriteBuffer(buffer)
|
return c.WriteBuffer(buffer)
|
||||||
}
|
}
|
||||||
@@ -488,7 +466,10 @@ func (c *ClientPacketAddrConn) ReadPacket(buffer *buf.Buffer) (destination M.Soc
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
|
if buffer.FreeLen() < int(length) {
|
||||||
|
return destination, io.ErrShortBuffer
|
||||||
|
}
|
||||||
|
_, err = io.ReadFull(c.ExtendedConn, buffer.Extend(int(length)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func ParseProtocol(name string) (Protocol, error) {
|
|||||||
func (p Protocol) newServer(conn net.Conn) (abstractSession, error) {
|
func (p Protocol) newServer(conn net.Conn) (abstractSession, error) {
|
||||||
switch p {
|
switch p {
|
||||||
case ProtocolSMux:
|
case ProtocolSMux:
|
||||||
session, err := smux.Server(conn, smuxConfig())
|
session, err := smux.Server(conn, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ func (p Protocol) newServer(conn net.Conn) (abstractSession, error) {
|
|||||||
func (p Protocol) newClient(conn net.Conn) (abstractSession, error) {
|
func (p Protocol) newClient(conn net.Conn) (abstractSession, error) {
|
||||||
switch p {
|
switch p {
|
||||||
case ProtocolSMux:
|
case ProtocolSMux:
|
||||||
session, err := smux.Client(conn, smuxConfig())
|
session, err := smux.Client(conn, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -70,12 +70,6 @@ func (p Protocol) newClient(conn net.Conn) (abstractSession, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func smuxConfig() *smux.Config {
|
|
||||||
config := smux.DefaultConfig()
|
|
||||||
config.KeepAliveDisabled = true
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func yaMuxConfig() *yamux.Config {
|
func yaMuxConfig() *yamux.Config {
|
||||||
config := yamux.DefaultConfig()
|
config := yamux.DefaultConfig()
|
||||||
config.LogOutput = io.Discard
|
config.LogOutput = io.Discard
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package mux
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
@@ -14,7 +15,6 @@ import (
|
|||||||
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/sing/common/rw"
|
"github.com/sagernet/sing/common/rw"
|
||||||
"github.com/sagernet/sing/common/task"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewConnection(ctx context.Context, router adapter.Router, errorHandler E.Handler, logger log.ContextLogger, conn net.Conn, metadata adapter.InboundContext) error {
|
func NewConnection(ctx context.Context, router adapter.Router, errorHandler E.Handler, logger log.ContextLogger, conn net.Conn, metadata adapter.InboundContext) error {
|
||||||
@@ -26,21 +26,14 @@ func NewConnection(ctx context.Context, router adapter.Router, errorHandler E.Ha
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var group task.Group
|
var stream net.Conn
|
||||||
group.Append0(func(ctx context.Context) error {
|
for {
|
||||||
var stream net.Conn
|
stream, err = session.Accept()
|
||||||
for {
|
if err != nil {
|
||||||
stream, err = session.Accept()
|
return err
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
go newConnection(ctx, router, errorHandler, logger, stream, metadata)
|
|
||||||
}
|
}
|
||||||
})
|
go newConnection(ctx, router, errorHandler, logger, stream, metadata)
|
||||||
group.Cleanup(func() {
|
}
|
||||||
session.Close()
|
|
||||||
})
|
|
||||||
return group.Run(ctx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newConnection(ctx context.Context, router adapter.Router, errorHandler E.Handler, logger log.ContextLogger, stream net.Conn, metadata adapter.InboundContext) {
|
func newConnection(ctx context.Context, router adapter.Router, errorHandler E.Handler, logger log.ContextLogger, stream net.Conn, metadata adapter.InboundContext) {
|
||||||
@@ -165,6 +158,9 @@ func (c *ServerPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksad
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if buffer.FreeLen() < int(length) {
|
||||||
|
return destination, io.ErrShortBuffer
|
||||||
|
}
|
||||||
_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
|
_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -227,6 +223,9 @@ func (c *ServerPacketAddrConn) ReadPacket(buffer *buf.Buffer) (destination M.Soc
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if buffer.FreeLen() < int(length) {
|
||||||
|
return destination, io.ErrShortBuffer
|
||||||
|
}
|
||||||
_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
|
_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -4,29 +4,18 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-tun"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Searcher interface {
|
type Searcher interface {
|
||||||
FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error)
|
FindProcessInfo(ctx context.Context, network string, srcIP netip.Addr, srcPort int) (*Info, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrNotFound = E.New("process not found")
|
var ErrNotFound = E.New("process not found")
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Logger log.ContextLogger
|
|
||||||
PackageManager tun.PackageManager
|
|
||||||
}
|
|
||||||
|
|
||||||
type Info struct {
|
type Info struct {
|
||||||
ProcessPath string
|
ProcessPath string
|
||||||
PackageName string
|
PackageName string
|
||||||
User string
|
User string
|
||||||
UserId int32
|
UserId int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
|
||||||
return findProcessInfo(searcher, ctx, network, source, destination)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,37 +2,170 @@ package process
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"io"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/sagernet/sing-tun"
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Searcher = (*androidSearcher)(nil)
|
var _ Searcher = (*androidSearcher)(nil)
|
||||||
|
|
||||||
type androidSearcher struct {
|
type androidSearcher struct {
|
||||||
packageManager tun.PackageManager
|
logger log.ContextLogger
|
||||||
|
watcher *fsnotify.Watcher
|
||||||
|
userMap map[string]int32
|
||||||
|
packageMap map[int32]string
|
||||||
|
sharedUserMap map[int32]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSearcher(config Config) (Searcher, error) {
|
func NewSearcher(logger log.ContextLogger) (Searcher, error) {
|
||||||
return &androidSearcher{config.PackageManager}, nil
|
return &androidSearcher{logger: logger}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
func (s *androidSearcher) Start() error {
|
||||||
_, uid, err := resolveSocketByNetlink(network, source, destination)
|
err := s.updatePackages()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "read packages list")
|
||||||
|
}
|
||||||
|
err = s.startWatcher()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("create fsnotify watcher: ", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *androidSearcher) startWatcher() error {
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = watcher.Add("/data/system/packages.xml")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.watcher = watcher
|
||||||
|
go s.loopUpdate()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *androidSearcher) loopUpdate() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case _, ok := <-s.watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := s.updatePackages()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error(E.Cause(err, "update packages list"))
|
||||||
|
}
|
||||||
|
case err, ok := <-s.watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.logger.Error(E.Cause(err, "fsnotify error"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *androidSearcher) Close() error {
|
||||||
|
return common.Close(common.PtrOrNil(s.watcher))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, srcIP netip.Addr, srcPort int) (*Info, error) {
|
||||||
|
_, uid, err := resolveSocketByNetlink(network, srcIP, srcPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if sharedPackage, loaded := s.packageManager.SharedPackageByID(uid % 100000); loaded {
|
if sharedUser, loaded := s.sharedUserMap[uid]; loaded {
|
||||||
return &Info{
|
return &Info{
|
||||||
UserId: int32(uid),
|
UserId: uid,
|
||||||
PackageName: sharedPackage,
|
PackageName: sharedUser,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded {
|
if packageName, loaded := s.packageMap[uid]; loaded {
|
||||||
return &Info{
|
return &Info{
|
||||||
UserId: int32(uid),
|
UserId: uid,
|
||||||
PackageName: packageName,
|
PackageName: packageName,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
return &Info{UserId: int32(uid)}, nil
|
return &Info{UserId: uid}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *androidSearcher) updatePackages() error {
|
||||||
|
userMap := make(map[string]int32)
|
||||||
|
packageMap := make(map[int32]string)
|
||||||
|
sharedUserMap := make(map[int32]string)
|
||||||
|
packagesData, err := os.Open("/data/system/packages.xml")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
decoder := xml.NewDecoder(packagesData)
|
||||||
|
var token xml.Token
|
||||||
|
for {
|
||||||
|
token, err = decoder.Token()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
element, isStart := token.(xml.StartElement)
|
||||||
|
if !isStart {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch element.Name.Local {
|
||||||
|
case "package":
|
||||||
|
var name string
|
||||||
|
var userID int64
|
||||||
|
for _, attr := range element.Attr {
|
||||||
|
switch attr.Name.Local {
|
||||||
|
case "name":
|
||||||
|
name = attr.Value
|
||||||
|
case "userId", "sharedUserId":
|
||||||
|
userID, err = strconv.ParseInt(attr.Value, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if userID == 0 && name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
userMap[name] = int32(userID)
|
||||||
|
packageMap[int32(userID)] = name
|
||||||
|
case "shared-user":
|
||||||
|
var name string
|
||||||
|
var userID int64
|
||||||
|
for _, attr := range element.Attr {
|
||||||
|
switch attr.Name.Local {
|
||||||
|
case "name":
|
||||||
|
name = attr.Value
|
||||||
|
case "userId":
|
||||||
|
userID, err = strconv.ParseInt(attr.Value, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
packageMap[int32(userID)] = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if userID == 0 && name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sharedUserMap[int32(userID)] = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.logger.Info("updated packages list: ", len(packageMap), " packages, ", len(sharedUserMap), " shared users")
|
||||||
|
s.userMap = userMap
|
||||||
|
s.packageMap = packageMap
|
||||||
|
s.sharedUserMap = sharedUserMap
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
@@ -19,34 +18,18 @@ var _ Searcher = (*darwinSearcher)(nil)
|
|||||||
|
|
||||||
type darwinSearcher struct{}
|
type darwinSearcher struct{}
|
||||||
|
|
||||||
func NewSearcher(_ Config) (Searcher, error) {
|
func NewSearcher(logger log.ContextLogger) (Searcher, error) {
|
||||||
return &darwinSearcher{}, nil
|
return &darwinSearcher{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, srcIP netip.Addr, srcPort int) (*Info, error) {
|
||||||
processName, err := findProcessName(network, source.Addr(), int(source.Port()))
|
processName, err := findProcessName(network, srcIP, srcPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &Info{ProcessPath: processName, UserId: -1}, nil
|
return &Info{ProcessPath: processName, UserId: -1}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var structSize = func() int {
|
|
||||||
value, _ := syscall.Sysctl("kern.osrelease")
|
|
||||||
major, _, _ := strings.Cut(value, ".")
|
|
||||||
n, _ := strconv.ParseInt(major, 10, 64)
|
|
||||||
switch true {
|
|
||||||
case n >= 22:
|
|
||||||
return 408
|
|
||||||
default:
|
|
||||||
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
|
|
||||||
// size/offset are round up (aligned) to 8 bytes in darwin
|
|
||||||
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
|
|
||||||
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
|
|
||||||
return 384
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
func findProcessName(network string, ip netip.Addr, port int) (string, error) {
|
func findProcessName(network string, ip netip.Addr, port int) (string, error) {
|
||||||
var spath string
|
var spath string
|
||||||
switch network {
|
switch network {
|
||||||
@@ -71,7 +54,7 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
|
|||||||
// size/offset are round up (aligned) to 8 bytes in darwin
|
// size/offset are round up (aligned) to 8 bytes in darwin
|
||||||
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
|
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
|
||||||
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
|
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
|
||||||
itemSize := structSize
|
itemSize := 384
|
||||||
if network == N.NetworkTCP {
|
if network == N.NetworkTCP {
|
||||||
// rup8(sizeof(xtcpcb_n))
|
// rup8(sizeof(xtcpcb_n))
|
||||||
itemSize += 208
|
itemSize += 208
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ type linuxSearcher struct {
|
|||||||
logger log.ContextLogger
|
logger log.ContextLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSearcher(config Config) (Searcher, error) {
|
func NewSearcher(logger log.ContextLogger) (Searcher, error) {
|
||||||
return &linuxSearcher{config.Logger}, nil
|
return &linuxSearcher{logger}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, srcIP netip.Addr, srcPort int) (*Info, error) {
|
||||||
inode, uid, err := resolveSocketByNetlink(network, source, destination)
|
inode, uid, err := resolveSocketByNetlink(network, srcIP, srcPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, sou
|
|||||||
s.logger.DebugContext(ctx, "find process path: ", err)
|
s.logger.DebugContext(ctx, "find process path: ", err)
|
||||||
}
|
}
|
||||||
return &Info{
|
return &Info{
|
||||||
UserId: int32(uid),
|
UserId: uid,
|
||||||
ProcessPath: processPath,
|
ProcessPath: processPath,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,19 @@ const (
|
|||||||
pathProc = "/proc"
|
pathProc = "/proc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (inode int32, uid int32, err error) {
|
||||||
var family uint8
|
for attempts := 0; attempts < 3; attempts++ {
|
||||||
var protocol uint8
|
inode, uid, err = resolveSocketByNetlink0(network, ip, srcPort)
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveSocketByNetlink0(network string, ip netip.Addr, srcPort int) (inode int32, uid int32, err error) {
|
||||||
|
var family byte
|
||||||
|
var protocol byte
|
||||||
|
|
||||||
switch network {
|
switch network {
|
||||||
case N.NetworkTCP:
|
case N.NetworkTCP:
|
||||||
@@ -50,13 +60,13 @@ func resolveSocketByNetlink(network string, source netip.AddrPort, destination n
|
|||||||
return 0, 0, os.ErrInvalid
|
return 0, 0, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
if source.Addr().Is4() {
|
if ip.Is4() {
|
||||||
family = syscall.AF_INET
|
family = syscall.AF_INET
|
||||||
} else {
|
} else {
|
||||||
family = syscall.AF_INET6
|
family = syscall.AF_INET6
|
||||||
}
|
}
|
||||||
|
|
||||||
req := packSocketDiagRequest(family, protocol, source)
|
req := packSocketDiagRequest(family, protocol, ip, uint16(srcPort))
|
||||||
|
|
||||||
socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG)
|
socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -67,18 +77,16 @@ func resolveSocketByNetlink(network string, source netip.AddrPort, destination n
|
|||||||
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 100})
|
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 100})
|
||||||
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 100})
|
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 100})
|
||||||
|
|
||||||
err = syscall.Connect(socket, &syscall.SockaddrNetlink{
|
if err = syscall.Connect(socket, &syscall.SockaddrNetlink{
|
||||||
Family: syscall.AF_NETLINK,
|
Family: syscall.AF_NETLINK,
|
||||||
Pad: 0,
|
Pad: 0,
|
||||||
Pid: 0,
|
Pid: 0,
|
||||||
Groups: 0,
|
Groups: 0,
|
||||||
})
|
}); err != nil {
|
||||||
if err != nil {
|
return 0, 0, err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = syscall.Write(socket, req)
|
if _, err = syscall.Write(socket, req); err != nil {
|
||||||
if err != nil {
|
|
||||||
return 0, 0, E.Cause(err, "write netlink request")
|
return 0, 0, E.Cause(err, "write netlink request")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,12 +115,15 @@ func resolveSocketByNetlink(network string, source netip.AddrPort, destination n
|
|||||||
}
|
}
|
||||||
|
|
||||||
inode, uid = unpackSocketDiagResponse(&messages[0])
|
inode, uid = unpackSocketDiagResponse(&messages[0])
|
||||||
|
if inode < 0 || uid < 0 {
|
||||||
|
return 0, 0, E.New("invalid inode(", inode, ") or uid(", uid, ")")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func packSocketDiagRequest(family, protocol byte, source netip.AddrPort) []byte {
|
func packSocketDiagRequest(family, protocol byte, source netip.Addr, sourcePort uint16) []byte {
|
||||||
s := make([]byte, 16)
|
s := make([]byte, 16)
|
||||||
copy(s, source.Addr().AsSlice())
|
copy(s, source.AsSlice())
|
||||||
|
|
||||||
buf := make([]byte, sizeOfSocketDiagRequest)
|
buf := make([]byte, sizeOfSocketDiagRequest)
|
||||||
|
|
||||||
@@ -128,7 +139,7 @@ func packSocketDiagRequest(family, protocol byte, source netip.AddrPort) []byte
|
|||||||
buf[19] = 0
|
buf[19] = 0
|
||||||
nativeEndian.PutUint32(buf[20:24], 0xFFFFFFFF)
|
nativeEndian.PutUint32(buf[20:24], 0xFFFFFFFF)
|
||||||
|
|
||||||
binary.BigEndian.PutUint16(buf[24:26], source.Port())
|
binary.BigEndian.PutUint16(buf[24:26], sourcePort)
|
||||||
binary.BigEndian.PutUint16(buf[26:28], 0)
|
binary.BigEndian.PutUint16(buf[26:28], 0)
|
||||||
|
|
||||||
copy(buf[28:44], s)
|
copy(buf[28:44], s)
|
||||||
@@ -140,20 +151,20 @@ func packSocketDiagRequest(family, protocol byte, source netip.AddrPort) []byte
|
|||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
|
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid int32) {
|
||||||
if len(msg.Data) < 72 {
|
if len(msg.Data) < 72 {
|
||||||
return 0, 0
|
return 0, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
data := msg.Data
|
data := msg.Data
|
||||||
|
|
||||||
uid = nativeEndian.Uint32(data[64:68])
|
uid = int32(nativeEndian.Uint32(data[64:68]))
|
||||||
inode = nativeEndian.Uint32(data[68:72])
|
inode = int32(nativeEndian.Uint32(data[68:72]))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) {
|
func resolveProcessNameByProcSearch(inode, uid int32) (string, error) {
|
||||||
files, err := os.ReadDir(pathProc)
|
files, err := os.ReadDir(pathProc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -171,7 +182,7 @@ func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if info.Sys().(*syscall.Stat_t).Uid != uid {
|
if info.Sys().(*syscall.Stat_t).Uid != uint32(uid) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ package process
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewSearcher(_ Config) (Searcher, error) {
|
func NewSearcher(logger log.ContextLogger) (Searcher, error) {
|
||||||
return nil, os.ErrInvalid
|
return nil, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
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"
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ var _ Searcher = (*windowsSearcher)(nil)
|
|||||||
|
|
||||||
type windowsSearcher struct{}
|
type windowsSearcher struct{}
|
||||||
|
|
||||||
func NewSearcher(_ Config) (Searcher, error) {
|
func NewSearcher(logger log.ContextLogger) (Searcher, error) {
|
||||||
err := initWin32API()
|
err := initWin32API()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "init win32 api")
|
return nil, E.Cause(err, "init win32 api")
|
||||||
@@ -63,8 +64,8 @@ func initWin32API() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, srcIP netip.Addr, srcPort int) (*Info, error) {
|
||||||
processName, err := findProcessName(network, source.Addr(), int(source.Port()))
|
processName, err := findProcessName(network, srcIP, srcPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build linux && !android
|
//go:build cgo && linux && !android
|
||||||
|
|
||||||
package process
|
package process
|
||||||
|
|
||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
func findProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
func FindProcessInfo(searcher Searcher, ctx context.Context, network string, srcIP netip.Addr, srcPort int) (*Info, error) {
|
||||||
info, err := searcher.FindProcessInfo(ctx, network, source, destination)
|
info, err := searcher.FindProcessInfo(ctx, network, srcIP, srcPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !linux || android
|
//go:build !(cgo && linux && !android)
|
||||||
|
|
||||||
package process
|
package process
|
||||||
|
|
||||||
@@ -7,6 +7,6 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
)
|
)
|
||||||
|
|
||||||
func findProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
func FindProcessInfo(searcher Searcher, ctx context.Context, network string, srcIP netip.Addr, srcPort int) (*Info, error) {
|
||||||
return searcher.FindProcessInfo(ctx, network, source, destination)
|
return searcher.FindProcessInfo(ctx, network, srcIP, srcPort)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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, err
|
|
||||||
}
|
|
||||||
if bufReader.Buffered() > 0 {
|
|
||||||
cache := buf.NewSize(bufReader.Buffered())
|
|
||||||
_, err = cache.ReadFullFrom(bufReader, cache.FreeLen())
|
|
||||||
if err != nil {
|
|
||||||
return nil, 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
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package redir
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
PF_OUT = 0x2
|
|
||||||
DIOCNATLOOK = 0xc0544417
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetOriginalDestination(conn net.Conn) (destination netip.AddrPort, err error) {
|
|
||||||
fd, err := syscall.Open("/dev/pf", 0, syscall.O_RDONLY)
|
|
||||||
if err != nil {
|
|
||||||
return netip.AddrPort{}, err
|
|
||||||
}
|
|
||||||
defer syscall.Close(fd)
|
|
||||||
nl := struct {
|
|
||||||
saddr, daddr, rsaddr, rdaddr [16]byte
|
|
||||||
sxport, dxport, rsxport, rdxport [4]byte
|
|
||||||
af, proto, protoVariant, direction uint8
|
|
||||||
}{
|
|
||||||
af: syscall.AF_INET,
|
|
||||||
proto: syscall.IPPROTO_TCP,
|
|
||||||
direction: PF_OUT,
|
|
||||||
}
|
|
||||||
la := conn.LocalAddr().(*net.TCPAddr)
|
|
||||||
ra := conn.RemoteAddr().(*net.TCPAddr)
|
|
||||||
raIP, laIP := ra.IP, la.IP
|
|
||||||
raPort, laPort := ra.Port, la.Port
|
|
||||||
switch {
|
|
||||||
case raIP.To4() != nil:
|
|
||||||
copy(nl.saddr[:net.IPv4len], raIP.To4())
|
|
||||||
copy(nl.daddr[:net.IPv4len], laIP.To4())
|
|
||||||
nl.af = syscall.AF_INET
|
|
||||||
default:
|
|
||||||
copy(nl.saddr[:], raIP.To16())
|
|
||||||
copy(nl.daddr[:], laIP.To16())
|
|
||||||
nl.af = syscall.AF_INET6
|
|
||||||
}
|
|
||||||
nl.sxport[0], nl.sxport[1] = byte(raPort>>8), byte(raPort)
|
|
||||||
nl.dxport[0], nl.dxport[1] = byte(laPort>>8), byte(laPort)
|
|
||||||
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), DIOCNATLOOK, uintptr(unsafe.Pointer(&nl))); errno != 0 {
|
|
||||||
return netip.AddrPort{}, errno
|
|
||||||
}
|
|
||||||
|
|
||||||
var ip net.IP
|
|
||||||
switch nl.af {
|
|
||||||
case syscall.AF_INET:
|
|
||||||
ip = make(net.IP, net.IPv4len)
|
|
||||||
copy(ip, nl.rdaddr[:net.IPv4len])
|
|
||||||
case syscall.AF_INET6:
|
|
||||||
ip = make(net.IP, net.IPv6len)
|
|
||||||
copy(ip, nl.rdaddr[:])
|
|
||||||
}
|
|
||||||
port := uint16(nl.rdxport[0])<<8 | uint16(nl.rdxport[1])
|
|
||||||
destination = netip.AddrPortFrom(M.AddrFromIP(ip), port)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -3,35 +3,35 @@ package redir
|
|||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing/common/control"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetOriginalDestination(conn net.Conn) (destination netip.AddrPort, err error) {
|
func GetOriginalDestination(conn net.Conn) (destination netip.AddrPort, err error) {
|
||||||
syscallConn, ok := common.Cast[syscall.Conn](conn)
|
rawConn, err := conn.(syscall.Conn).SyscallConn()
|
||||||
if !ok {
|
if err != nil {
|
||||||
return netip.AddrPort{}, os.ErrInvalid
|
return
|
||||||
}
|
}
|
||||||
err = control.Conn(syscallConn, func(fd uintptr) error {
|
var rawFd uintptr
|
||||||
const SO_ORIGINAL_DST = 80
|
err = rawConn.Control(func(fd uintptr) {
|
||||||
if conn.RemoteAddr().(*net.TCPAddr).IP.To4() != nil {
|
rawFd = fd
|
||||||
raw, err := syscall.GetsockoptIPv6Mreq(int(fd), syscall.IPPROTO_IP, SO_ORIGINAL_DST)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
destination = netip.AddrPortFrom(M.AddrFromIP(raw.Multiaddr[4:8]), uint16(raw.Multiaddr[2])<<8+uint16(raw.Multiaddr[3]))
|
|
||||||
} else {
|
|
||||||
raw, err := syscall.GetsockoptIPv6MTUInfo(int(fd), syscall.IPPROTO_IPV6, SO_ORIGINAL_DST)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
destination = netip.AddrPortFrom(M.AddrFromIP(raw.Addr.Addr[:]), raw.Addr.Port)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
return
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const SO_ORIGINAL_DST = 80
|
||||||
|
if conn.RemoteAddr().(*net.TCPAddr).IP.To4() != nil {
|
||||||
|
raw, err := syscall.GetsockoptIPv6Mreq(int(rawFd), syscall.IPPROTO_IP, SO_ORIGINAL_DST)
|
||||||
|
if err != nil {
|
||||||
|
return netip.AddrPort{}, err
|
||||||
|
}
|
||||||
|
return netip.AddrPortFrom(M.AddrFromIP(raw.Multiaddr[4:8]), uint16(raw.Multiaddr[2])<<8+uint16(raw.Multiaddr[3])), nil
|
||||||
|
} else {
|
||||||
|
raw, err := syscall.GetsockoptIPv6MTUInfo(int(rawFd), syscall.IPPROTO_IPV6, SO_ORIGINAL_DST)
|
||||||
|
if err != nil {
|
||||||
|
return netip.AddrPort{}, err
|
||||||
|
}
|
||||||
|
return netip.AddrPortFrom(M.AddrFromIP(raw.Addr.Addr[:]), raw.Addr.Port), nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !linux && !darwin
|
//go:build !linux
|
||||||
|
|
||||||
package redir
|
package redir
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ package redir
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/control"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
@@ -29,18 +32,6 @@ func TProxy(fd uintptr, isIPv6 bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func TProxyWriteBack() control.Func {
|
|
||||||
return func(network, address string, conn syscall.RawConn) error {
|
|
||||||
return control.Raw(conn, func(fd uintptr) error {
|
|
||||||
if M.ParseSocksaddr(address).Addr.Is6() {
|
|
||||||
return syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_TRANSPARENT, 1)
|
|
||||||
} else {
|
|
||||||
return syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetOriginalDestinationFromOOB(oob []byte) (netip.AddrPort, error) {
|
func GetOriginalDestinationFromOOB(oob []byte) (netip.AddrPort, error) {
|
||||||
controlMessages, err := unix.ParseSocketControlMessage(oob)
|
controlMessages, err := unix.ParseSocketControlMessage(oob)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -55,3 +46,79 @@ func GetOriginalDestinationFromOOB(oob []byte) (netip.AddrPort, error) {
|
|||||||
}
|
}
|
||||||
return netip.AddrPort{}, E.New("not found")
|
return netip.AddrPort{}, E.New("not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DialUDP(lAddr *net.UDPAddr, rAddr *net.UDPAddr) (*net.UDPConn, error) {
|
||||||
|
rSockAddr, err := udpAddrToSockAddr(rAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lSockAddr, err := udpAddrToSockAddr(lAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fd, err := syscall.Socket(udpAddrFamily(lAddr, rAddr), syscall.SOCK_DGRAM, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil {
|
||||||
|
syscall.Close(fd)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = syscall.SetsockoptInt(fd, syscall.SOL_IP, syscall.IP_TRANSPARENT, 1); err != nil {
|
||||||
|
syscall.Close(fd)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = syscall.Bind(fd, lSockAddr); err != nil {
|
||||||
|
syscall.Close(fd)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = syscall.Connect(fd, rSockAddr); err != nil {
|
||||||
|
syscall.Close(fd)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fdFile := os.NewFile(uintptr(fd), F.ToString("net-udp-dial-", rAddr))
|
||||||
|
defer fdFile.Close()
|
||||||
|
|
||||||
|
c, err := net.FileConn(fdFile)
|
||||||
|
if err != nil {
|
||||||
|
syscall.Close(fd)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.(*net.UDPConn), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func udpAddrToSockAddr(addr *net.UDPAddr) (syscall.Sockaddr, error) {
|
||||||
|
switch {
|
||||||
|
case addr.IP.To4() != nil:
|
||||||
|
ip := [4]byte{}
|
||||||
|
copy(ip[:], addr.IP.To4())
|
||||||
|
|
||||||
|
return &syscall.SockaddrInet4{Addr: ip, Port: addr.Port}, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
ip := [16]byte{}
|
||||||
|
copy(ip[:], addr.IP.To16())
|
||||||
|
|
||||||
|
zoneID, err := strconv.ParseUint(addr.Zone, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
zoneID = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return &syscall.SockaddrInet6{Addr: ip, Port: addr.Port, ZoneId: uint32(zoneID)}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func udpAddrFamily(lAddr, rAddr *net.UDPAddr) int {
|
||||||
|
if (lAddr == nil || lAddr.IP.To4() != nil) && (rAddr == nil || lAddr.IP.To4() != nil) {
|
||||||
|
return syscall.AF_INET
|
||||||
|
}
|
||||||
|
return syscall.AF_INET6
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,20 +3,19 @@
|
|||||||
package redir
|
package redir
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/control"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TProxy(fd uintptr, isIPv6 bool) error {
|
func TProxy(fd uintptr, isIPv6 bool) error {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
func TProxyWriteBack() control.Func {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetOriginalDestinationFromOOB(oob []byte) (netip.AddrPort, error) {
|
func GetOriginalDestinationFromOOB(oob []byte) (netip.AddrPort, error) {
|
||||||
return netip.AddrPort{}, os.ErrInvalid
|
return netip.AddrPort{}, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DialUDP(lAddr *net.UDPAddr, rAddr *net.UDPAddr) (*net.UDPConn, error) {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
|||||||
21
common/settings/command.go
Normal file
21
common/settings/command.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runCommand(name string, args ...string) error {
|
||||||
|
command := exec.Command(name, args...)
|
||||||
|
command.Env = os.Environ()
|
||||||
|
command.Stdin = os.Stdin
|
||||||
|
command.Stdout = os.Stderr
|
||||||
|
command.Stderr = os.Stderr
|
||||||
|
return command.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func readCommand(name string, args ...string) ([]byte, error) {
|
||||||
|
command := exec.Command(name, args...)
|
||||||
|
command.Env = os.Environ()
|
||||||
|
return command.CombinedOutput()
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,9 +25,9 @@ func init() {
|
|||||||
|
|
||||||
func runAndroidShell(name string, args ...string) error {
|
func runAndroidShell(name string, args ...string) error {
|
||||||
if !useRish {
|
if !useRish {
|
||||||
return common.Exec(name, args...).Attach().Run()
|
return runCommand(name, args...)
|
||||||
} else {
|
} else {
|
||||||
return common.Exec("sh", rishPath, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run()
|
return runCommand("sh", rishPath, "-c", F.ToString(name, " ", strings.Join(args, " ")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/netip"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-tun"
|
"github.com/sagernet/sing-tun"
|
||||||
"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/common/x/list"
|
"github.com/sagernet/sing/common/x/list"
|
||||||
@@ -20,8 +18,8 @@ type systemProxy struct {
|
|||||||
isMixed bool
|
isMixed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *systemProxy) update(event int) error {
|
func (p *systemProxy) update() error {
|
||||||
newInterfaceName := p.monitor.DefaultInterfaceName(netip.IPv4Unspecified())
|
newInterfaceName := p.monitor.DefaultInterfaceName()
|
||||||
if p.interfaceName == newInterfaceName {
|
if p.interfaceName == newInterfaceName {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -34,13 +32,13 @@ func (p *systemProxy) update(event int) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if p.isMixed {
|
if p.isMixed {
|
||||||
err = common.Exec("networksetup", "-setsocksfirewallproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
|
err = runCommand("networksetup", "-setsocksfirewallproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port))
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = common.Exec("networksetup", "-setwebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
|
err = runCommand("networksetup", "-setwebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port))
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = common.Exec("networksetup", "-setsecurewebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
|
err = runCommand("networksetup", "-setsecurewebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port))
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -51,19 +49,19 @@ func (p *systemProxy) unset() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if p.isMixed {
|
if p.isMixed {
|
||||||
err = common.Exec("networksetup", "-setsocksfirewallproxystate", interfaceDisplayName, "off").Attach().Run()
|
err = runCommand("networksetup", "-setsocksfirewallproxystate", interfaceDisplayName, "off")
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = common.Exec("networksetup", "-setwebproxystate", interfaceDisplayName, "off").Attach().Run()
|
err = runCommand("networksetup", "-setwebproxystate", interfaceDisplayName, "off")
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = common.Exec("networksetup", "-setsecurewebproxystate", interfaceDisplayName, "off").Attach().Run()
|
err = runCommand("networksetup", "-setsecurewebproxystate", interfaceDisplayName, "off")
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func getInterfaceDisplayName(name string) (string, error) {
|
func getInterfaceDisplayName(name string) (string, error) {
|
||||||
content, err := common.Exec("networksetup", "-listallhardwareports").Read()
|
content, err := readCommand("networksetup", "-listallhardwareports")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -88,7 +86,7 @@ func SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() er
|
|||||||
port: port,
|
port: port,
|
||||||
isMixed: isMixed,
|
isMixed: isMixed,
|
||||||
}
|
}
|
||||||
err := proxy.update(tun.EventInterfaceUpdate)
|
err := proxy.update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ func init() {
|
|||||||
|
|
||||||
func runAsUser(name string, args ...string) error {
|
func runAsUser(name string, args ...string) error {
|
||||||
if os.Getuid() != 0 {
|
if os.Getuid() != 0 {
|
||||||
return common.Exec(name, args...).Attach().Run()
|
return runCommand(name, args...)
|
||||||
} else if sudoUser != "" {
|
} else if sudoUser != "" {
|
||||||
return common.Exec("su", "-", sudoUser, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run()
|
return runCommand("su", "-", sudoUser, "-c", F.ToString(name, " ", strings.Join(args, " ")))
|
||||||
} else {
|
} else {
|
||||||
return E.New("set system proxy: unable to set as root")
|
return E.New("set system proxy: unable to set as root")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ import (
|
|||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
"github.com/sagernet/sing/common/task"
|
"github.com/sagernet/sing/common/task"
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
"golang.org/x/net/dns/dnsmessage"
|
||||||
)
|
)
|
||||||
|
|
||||||
func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.InboundContext, error) {
|
func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.InboundContext, error) {
|
||||||
@@ -23,7 +22,7 @@ func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if length == 0 {
|
if length > 512 {
|
||||||
return nil, os.ErrInvalid
|
return nil, os.ErrInvalid
|
||||||
}
|
}
|
||||||
_buffer := buf.StackNewSize(int(length))
|
_buffer := buf.StackNewSize(int(length))
|
||||||
@@ -45,13 +44,18 @@ func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DomainNameQuery(ctx context.Context, packet []byte) (*adapter.InboundContext, error) {
|
func DomainNameQuery(ctx context.Context, packet []byte) (*adapter.InboundContext, error) {
|
||||||
var msg mDNS.Msg
|
var parser dnsmessage.Parser
|
||||||
err := msg.Unpack(packet)
|
_, err := parser.Start(packet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) {
|
question, err := parser.Question()
|
||||||
|
if err != nil {
|
||||||
return nil, os.ErrInvalid
|
return nil, os.ErrInvalid
|
||||||
}
|
}
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolDNS}, nil
|
domain := question.Name.String()
|
||||||
|
if question.Class == dnsmessage.ClassINET && IsDomainName(domain) {
|
||||||
|
return &adapter.InboundContext{Protocol: C.ProtocolDNS /*, Domain: domain*/}, nil
|
||||||
|
}
|
||||||
|
return nil, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|||||||
6
common/sniff/domain.go
Normal file
6
common/sniff/domain.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package sniff
|
||||||
|
|
||||||
|
import _ "unsafe" // for linkname
|
||||||
|
|
||||||
|
//go:linkname IsDomainName net.isDomainName
|
||||||
|
func IsDomainName(domain string) bool
|
||||||
@@ -24,7 +24,8 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if typeByte&0x40 == 0 {
|
|
||||||
|
if typeByte&0x80 == 0 || typeByte&0x40 == 0 {
|
||||||
return nil, E.New("bad type byte")
|
return nil, E.New("bad type byte")
|
||||||
}
|
}
|
||||||
var versionNumber uint32
|
var versionNumber uint32
|
||||||
@@ -144,6 +145,9 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
|
|||||||
default:
|
default:
|
||||||
return nil, E.New("bad packet number length")
|
return nil, E.New("bad packet number length")
|
||||||
}
|
}
|
||||||
|
if packetNumber != 0 {
|
||||||
|
return nil, E.New("bad packet number: ", packetNumber)
|
||||||
|
}
|
||||||
extHdrLen := hdrLen + int(packetNumberLength)
|
extHdrLen := hdrLen + int(packetNumberLength)
|
||||||
copy(newPacket[extHdrLen:hdrLen+4], packet[extHdrLen:])
|
copy(newPacket[extHdrLen:hdrLen+4], packet[extHdrLen:])
|
||||||
data := newPacket[extHdrLen : int(packetLen)+hdrLen]
|
data := newPacket[extHdrLen : int(packetLen)+hdrLen]
|
||||||
@@ -168,76 +172,37 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var frameType byte
|
|
||||||
var frameLen uint64
|
|
||||||
var fragments []struct {
|
|
||||||
offset uint64
|
|
||||||
length uint64
|
|
||||||
payload []byte
|
|
||||||
}
|
|
||||||
decryptedReader := bytes.NewReader(decrypted)
|
decryptedReader := bytes.NewReader(decrypted)
|
||||||
for {
|
frameType, err := decryptedReader.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for frameType == 0x0 {
|
||||||
|
// skip padding
|
||||||
frameType, err = decryptedReader.ReadByte()
|
frameType, err = decryptedReader.ReadByte()
|
||||||
if err == io.EOF {
|
if err != nil {
|
||||||
break
|
return nil, err
|
||||||
}
|
|
||||||
switch frameType {
|
|
||||||
case 0x0:
|
|
||||||
continue
|
|
||||||
case 0x1:
|
|
||||||
continue
|
|
||||||
case 0x6:
|
|
||||||
var offset uint64
|
|
||||||
offset, err = qtls.ReadUvarint(decryptedReader)
|
|
||||||
if err != nil {
|
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err
|
|
||||||
}
|
|
||||||
var length uint64
|
|
||||||
length, err = qtls.ReadUvarint(decryptedReader)
|
|
||||||
if err != nil {
|
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err
|
|
||||||
}
|
|
||||||
index := len(decrypted) - decryptedReader.Len()
|
|
||||||
fragments = append(fragments, struct {
|
|
||||||
offset uint64
|
|
||||||
length uint64
|
|
||||||
payload []byte
|
|
||||||
}{offset, length, decrypted[index : index+int(length)]})
|
|
||||||
frameLen += length
|
|
||||||
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// ignore unknown frame type
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if frameType != 0x6 {
|
||||||
|
// not crypto frame
|
||||||
|
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, nil
|
||||||
|
}
|
||||||
|
_, err = qtls.ReadUvarint(decryptedReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = qtls.ReadUvarint(decryptedReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
tlsHdr := make([]byte, 5)
|
tlsHdr := make([]byte, 5)
|
||||||
tlsHdr[0] = 0x16
|
tlsHdr[0] = 0x16
|
||||||
binary.BigEndian.PutUint16(tlsHdr[1:], uint16(0x0303))
|
binary.BigEndian.PutUint16(tlsHdr[1:], uint16(0x0303))
|
||||||
binary.BigEndian.PutUint16(tlsHdr[3:], uint16(frameLen))
|
binary.BigEndian.PutUint16(tlsHdr[3:], uint16(decryptedReader.Len()))
|
||||||
var index uint64
|
metadata, err := TLSClientHello(ctx, io.MultiReader(bytes.NewReader(tlsHdr), decryptedReader))
|
||||||
var length int
|
|
||||||
var readers []io.Reader
|
|
||||||
readers = append(readers, bytes.NewReader(tlsHdr))
|
|
||||||
find:
|
|
||||||
for {
|
|
||||||
for _, fragment := range fragments {
|
|
||||||
if fragment.offset == index {
|
|
||||||
readers = append(readers, bytes.NewReader(fragment.payload))
|
|
||||||
index = fragment.offset + fragment.length
|
|
||||||
length++
|
|
||||||
continue find
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if length == len(fragments) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, E.New("bad fragments")
|
|
||||||
}
|
|
||||||
metadata, err := TLSClientHello(ctx, io.MultiReader(readers...))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
metadata.Protocol = C.ProtocolQUIC
|
metadata.Protocol = C.ProtocolQUIC
|
||||||
return metadata, nil
|
return metadata, nil
|
||||||
|
|||||||
@@ -19,15 +19,6 @@ func TestSniffQUICv1(t *testing.T) {
|
|||||||
require.Equal(t, metadata.Domain, "cloudflare-quic.com")
|
require.Equal(t, metadata.Domain, "cloudflare-quic.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSniffQUICFragment(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
pkt, err := hex.DecodeString("cc00000001082e3d5d1b64040c55000044d0ccea69e773f6631c1d18b04ae9ee75fcfc34ef74fa62533c93534338a86f101a05d70e0697fb483063fa85db1c59ccfbda5c35234931d8524d8aac37eaaad649470a67794cd754b23c98695238b8363452333bc8c4858376b4166e001da2006e35cf98a91e11a56419b2786775284942d0f7163982f7c248867d12dd374957481dbc564013ff785e1916195eef671f725908f761099d992d69231336ba81d9e25fe2fa3a6eff4318a6ccf10176fc841a1b315f7b35c5b292266fc869d76ca533e7d14e86d82db2e22eacd350977e47d2e012d8a5891c5aaf2a0f4c2b2dae897c161e5b68cbb4dee952472bdc1e21504b8f02534ec4366ce3f8bf86efc78e0232778fbd554457567112abdcafcf6d4d8fcf35083c25d9495679614aba21696e338c62b585046cc55ba8c09c844361d889a47c3ea703b4e23545a9ab2c0bb369693a9ddfb5daffa85cf80fdd6ad66738664e5b0a551729b4955cff7255afcb04dee88c2f072c9de7400947a1bd9327ac5d012a33000ada021d4c03d249fb017d6ac9200b2f9436beab8183ddfbe2d8aee31ffb7df9e1cc181c1af80c39a89965d18ed12da8e3ebe2ae1fbe4b348f83ba19e3e3d1c9b22bcf03ab6ad9b30fe180623faa291ebad83bcd71d7b57f2f5e2f3b8e81d24fb70b2f2159239e8f21ffafef2747aba47d97ab4081e603c018b10678cf99cab1fb42156a14486fa435153979d7279fd22cd40af7088bfc7eff41af2f4b3c0c8864d0040d74dff427f7bffdb8c278474ea00311326cf4925471a8cf596cb92119f19e0f789490ba9cb77b98015a987d93e0324cf1a38b55109f00c3e6ddc5180fb107bf468323afec9bb49fd6a86418569789d66cafe3b8253c2aebb3af3782c1c54dd560487d031d28e6a6e23e159581bb1d47efc4da3fe1d169f9ffb0ca9ba61af0a38a92fde5bc5e6ec026e8378a6315a7b95abf1d2da790a391306ce74d0baf8e2ce648ca74c487f2c0a76a28a80cdf5bd34316eb607684fe7e6d9e83824a00e07660d0b90e3cddd61ebf10748263474afa88c300549e64ce2e90560bb1a12dee7e9484f729a8a4ee7c5651adb5194b3b3ae38e501567c7dbf36e7bb37a2c20b74655f47f2d9af18e52e9d4c9c9eee8e63745779b8f0b06f3a09d846ba62eb978ad77c85de1ee2fee3fbb4c2d283c73e1ccba56a4658e48a2665d200f7f9342f8e84c2ba490094a4f94feec89e42d2f654f564c2beb2997bafa1fc2c68ad8e160b63587d49abc31b834878d52acfb05fb73d0e059b206162e3c90b40c4bc08407ffcb3c08431895b691a3fea923f1f3b48db75d3e6b91fd319ffe4d486e0e14bd5c6affc838dee63d9e0b80f169b5e6c02c7321dcb20deb2b8e707b60e345a308d505bbf26a93d8f18b39d62632e9a77cbe48b3b32eb8819d6311a49820d40f5acbf0273c91c36b2269a03e72ee64df3dfb10ddefe73c64ef60870b2b77bd99dea655f5fe791b538a929a14d99f6d69685d72431ea5f0f4b27a044f2f575ab474fcc3857895934de1ca2581798eaef2c17fe5aaf2e6add97fa32997c7026f15c1b1ad0e6043ae506027a7c0242546fdc851cca39a204e56879f2cef838be8ec66e0f2292f8c862e06f810eb9b80c7a467ce6e90155206352c7f82b1173ba3b98d35bb72c259a60db20dd1a43fe6d7aef0265e6eaa5caafd9b64b448ff745a2046acbdb65cf2a5007809808a4828dc99097feedc734c236260c584")
|
|
||||||
require.NoError(t, err)
|
|
||||||
metadata, err := sniff.QUICClientHello(context.Background(), pkt)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, metadata.Domain, "cloudflare-quic.com")
|
|
||||||
}
|
|
||||||
|
|
||||||
func FuzzSniffQUIC(f *testing.F) {
|
func FuzzSniffQUIC(f *testing.F) {
|
||||||
f.Fuzz(func(t *testing.T, data []byte) {
|
f.Fuzz(func(t *testing.T, data []byte) {
|
||||||
sniff.QUICClientHello(context.Background(), data)
|
sniff.QUICClientHello(context.Background(), data)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
@@ -18,11 +19,8 @@ type (
|
|||||||
PacketSniffer = func(ctx context.Context, packet []byte) (*adapter.InboundContext, error)
|
PacketSniffer = func(ctx context.Context, packet []byte) (*adapter.InboundContext, error)
|
||||||
)
|
)
|
||||||
|
|
||||||
func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) (*adapter.InboundContext, error) {
|
func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, sniffers ...StreamSniffer) (*adapter.InboundContext, error) {
|
||||||
if timeout == 0 {
|
err := conn.SetReadDeadline(time.Now().Add(C.ReadPayloadTimeout))
|
||||||
timeout = C.ReadPayloadTimeout
|
|
||||||
}
|
|
||||||
err := conn.SetReadDeadline(time.Now().Add(timeout))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -32,25 +30,23 @@ func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var metadata *adapter.InboundContext
|
var metadata *adapter.InboundContext
|
||||||
var errors []error
|
|
||||||
for _, sniffer := range sniffers {
|
for _, sniffer := range sniffers {
|
||||||
metadata, err = sniffer(ctx, bytes.NewReader(buffer.Bytes()))
|
metadata, err = sniffer(ctx, bytes.NewReader(buffer.Bytes()))
|
||||||
if metadata != nil {
|
if err != nil {
|
||||||
return metadata, nil
|
continue
|
||||||
}
|
}
|
||||||
errors = append(errors, err)
|
return metadata, nil
|
||||||
}
|
}
|
||||||
return nil, E.Errors(errors...)
|
return nil, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
func PeekPacket(ctx context.Context, packet []byte, sniffers ...PacketSniffer) (*adapter.InboundContext, error) {
|
func PeekPacket(ctx context.Context, packet []byte, sniffers ...PacketSniffer) (*adapter.InboundContext, error) {
|
||||||
var errors []error
|
|
||||||
for _, sniffer := range sniffers {
|
for _, sniffer := range sniffers {
|
||||||
metadata, err := sniffer(ctx, packet)
|
sniffMetadata, err := sniffer(ctx, packet)
|
||||||
if metadata != nil {
|
if err != nil {
|
||||||
return metadata, nil
|
continue
|
||||||
}
|
}
|
||||||
errors = append(errors, err)
|
return sniffMetadata, nil
|
||||||
}
|
}
|
||||||
return nil, E.Errors(errors...)
|
return nil, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
//go:build with_acme
|
|
||||||
|
|
||||||
package tls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
|
||||||
"github.com/mholt/acmez/acme"
|
|
||||||
)
|
|
||||||
|
|
||||||
type acmeWrapper struct {
|
|
||||||
ctx context.Context
|
|
||||||
cfg *certmagic.Config
|
|
||||||
domain []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *acmeWrapper) Start() error {
|
|
||||||
return w.cfg.ManageSync(w.ctx, w.domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *acmeWrapper) Close() error {
|
|
||||||
w.cfg.Unmanage(w.domain)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) {
|
|
||||||
var acmeServer string
|
|
||||||
switch options.Provider {
|
|
||||||
case "", "letsencrypt":
|
|
||||||
acmeServer = certmagic.LetsEncryptProductionCA
|
|
||||||
case "zerossl":
|
|
||||||
acmeServer = certmagic.ZeroSSLProductionCA
|
|
||||||
default:
|
|
||||||
if !strings.HasPrefix(options.Provider, "https://") {
|
|
||||||
return nil, nil, E.New("unsupported acme provider: " + options.Provider)
|
|
||||||
}
|
|
||||||
acmeServer = options.Provider
|
|
||||||
}
|
|
||||||
var storage certmagic.Storage
|
|
||||||
if options.DataDirectory != "" {
|
|
||||||
storage = &certmagic.FileStorage{
|
|
||||||
Path: options.DataDirectory,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
storage = certmagic.Default.Storage
|
|
||||||
}
|
|
||||||
config := &certmagic.Config{
|
|
||||||
DefaultServerName: options.DefaultServerName,
|
|
||||||
Storage: storage,
|
|
||||||
}
|
|
||||||
acmeConfig := certmagic.ACMEIssuer{
|
|
||||||
CA: acmeServer,
|
|
||||||
Email: options.Email,
|
|
||||||
Agreed: true,
|
|
||||||
DisableHTTPChallenge: options.DisableHTTPChallenge,
|
|
||||||
DisableTLSALPNChallenge: options.DisableTLSALPNChallenge,
|
|
||||||
AltHTTPPort: int(options.AlternativeHTTPPort),
|
|
||||||
AltTLSALPNPort: int(options.AlternativeTLSPort),
|
|
||||||
}
|
|
||||||
if options.ExternalAccount != nil {
|
|
||||||
acmeConfig.ExternalAccount = (*acme.EAB)(options.ExternalAccount)
|
|
||||||
}
|
|
||||||
config.Issuers = []certmagic.Issuer{certmagic.NewACMEIssuer(config, acmeConfig)}
|
|
||||||
config = certmagic.New(certmagic.NewCache(certmagic.CacheOptions{
|
|
||||||
GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) {
|
|
||||||
return config, nil
|
|
||||||
},
|
|
||||||
}), *config)
|
|
||||||
return config.TLSConfig(), &acmeWrapper{ctx, config, options.Domain}, nil
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
//go:build !with_acme
|
|
||||||
|
|
||||||
package tls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) {
|
|
||||||
return nil, nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`)
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package tls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/badtls"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewDialerFromOptions(router adapter.Router, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
|
|
||||||
if !options.Enabled {
|
|
||||||
return dialer, nil
|
|
||||||
}
|
|
||||||
config, err := NewClient(router, serverAddress, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return NewDialer(dialer, config), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
|
||||||
if !options.Enabled {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
if options.ECH != nil && options.ECH.Enabled {
|
|
||||||
return NewECHClient(router, serverAddress, options)
|
|
||||||
} else if options.UTLS != nil && options.UTLS.Enabled {
|
|
||||||
return NewUTLSClient(router, serverAddress, options)
|
|
||||||
} else {
|
|
||||||
return NewSTDClient(serverAddress, options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
|
|
||||||
tlsConn := config.Client(conn)
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
|
||||||
defer cancel()
|
|
||||||
err := tlsConn.HandshakeContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if stdConn, isSTD := tlsConn.(*tls.Conn); isSTD {
|
|
||||||
var badConn badtls.TLSConn
|
|
||||||
badConn, err = badtls.Create(stdConn)
|
|
||||||
if err == nil {
|
|
||||||
return badConn, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tlsConn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Dialer struct {
|
|
||||||
dialer N.Dialer
|
|
||||||
config Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDialer(dialer N.Dialer, config Config) N.Dialer {
|
|
||||||
return &Dialer{dialer, config}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Dialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
|
||||||
if network != N.NetworkTCP {
|
|
||||||
return nil, os.ErrInvalid
|
|
||||||
}
|
|
||||||
conn, err := d.dialer.DialContext(ctx, network, destination)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return ClientHandshake(ctx, conn, d.config)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Dialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
|
||||||
return nil, os.ErrInvalid
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package tls
|
|
||||||
|
|
||||||
const (
|
|
||||||
VersionTLS10 = 0x0301
|
|
||||||
VersionTLS11 = 0x0302
|
|
||||||
VersionTLS12 = 0x0303
|
|
||||||
VersionTLS13 = 0x0304
|
|
||||||
|
|
||||||
// Deprecated: SSLv3 is cryptographically broken, and is no longer
|
|
||||||
// supported by this package. See golang.org/issue/32716.
|
|
||||||
VersionSSL30 = 0x0300
|
|
||||||
)
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package tls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
STDConfig = tls.Config
|
|
||||||
STDConn = tls.Conn
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config interface {
|
|
||||||
ServerName() string
|
|
||||||
SetServerName(serverName string)
|
|
||||||
NextProtos() []string
|
|
||||||
SetNextProtos(nextProto []string)
|
|
||||||
Config() (*STDConfig, error)
|
|
||||||
Client(conn net.Conn) Conn
|
|
||||||
Clone() Config
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServerConfig interface {
|
|
||||||
Config
|
|
||||||
adapter.Service
|
|
||||||
Server(conn net.Conn) Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
type Conn interface {
|
|
||||||
net.Conn
|
|
||||||
HandshakeContext(ctx context.Context) error
|
|
||||||
ConnectionState() tls.ConnectionState
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseTLSVersion(version string) (uint16, error) {
|
|
||||||
switch version {
|
|
||||||
case "1.0":
|
|
||||||
return tls.VersionTLS10, nil
|
|
||||||
case "1.1":
|
|
||||||
return tls.VersionTLS11, nil
|
|
||||||
case "1.2":
|
|
||||||
return tls.VersionTLS12, nil
|
|
||||||
case "1.3":
|
|
||||||
return tls.VersionTLS13, nil
|
|
||||||
default:
|
|
||||||
return 0, E.New("unknown tls version:", version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
//go:build with_ech
|
|
||||||
|
|
||||||
package tls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/base64"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
cftls "github.com/sagernet/cloudflare-tls"
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing-dns"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ECHClientConfig struct {
|
|
||||||
config *cftls.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ECHClientConfig) ServerName() string {
|
|
||||||
return e.config.ServerName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ECHClientConfig) SetServerName(serverName string) {
|
|
||||||
e.config.ServerName = serverName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ECHClientConfig) NextProtos() []string {
|
|
||||||
return e.config.NextProtos
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ECHClientConfig) SetNextProtos(nextProto []string) {
|
|
||||||
e.config.NextProtos = nextProto
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ECHClientConfig) Config() (*STDConfig, error) {
|
|
||||||
return nil, E.New("unsupported usage for ECH")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ECHClientConfig) Client(conn net.Conn) Conn {
|
|
||||||
return &echConnWrapper{cftls.Client(conn, e.config)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ECHClientConfig) Clone() Config {
|
|
||||||
return &ECHClientConfig{
|
|
||||||
config: e.config.Clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type echConnWrapper struct {
|
|
||||||
*cftls.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echConnWrapper) ConnectionState() tls.ConnectionState {
|
|
||||||
state := c.Conn.ConnectionState()
|
|
||||||
return tls.ConnectionState{
|
|
||||||
Version: state.Version,
|
|
||||||
HandshakeComplete: state.HandshakeComplete,
|
|
||||||
DidResume: state.DidResume,
|
|
||||||
CipherSuite: state.CipherSuite,
|
|
||||||
NegotiatedProtocol: state.NegotiatedProtocol,
|
|
||||||
NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual,
|
|
||||||
ServerName: state.ServerName,
|
|
||||||
PeerCertificates: state.PeerCertificates,
|
|
||||||
VerifiedChains: state.VerifiedChains,
|
|
||||||
SignedCertificateTimestamps: state.SignedCertificateTimestamps,
|
|
||||||
OCSPResponse: state.OCSPResponse,
|
|
||||||
TLSUnique: state.TLSUnique,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewECHClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
|
||||||
var serverName string
|
|
||||||
if options.ServerName != "" {
|
|
||||||
serverName = options.ServerName
|
|
||||||
} else if serverAddress != "" {
|
|
||||||
if _, err := netip.ParseAddr(serverName); err != nil {
|
|
||||||
serverName = serverAddress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if serverName == "" && !options.Insecure {
|
|
||||||
return nil, E.New("missing server_name or insecure=true")
|
|
||||||
}
|
|
||||||
|
|
||||||
var tlsConfig cftls.Config
|
|
||||||
if options.DisableSNI {
|
|
||||||
tlsConfig.ServerName = "127.0.0.1"
|
|
||||||
} else {
|
|
||||||
tlsConfig.ServerName = serverName
|
|
||||||
}
|
|
||||||
if options.Insecure {
|
|
||||||
tlsConfig.InsecureSkipVerify = options.Insecure
|
|
||||||
} else if options.DisableSNI {
|
|
||||||
tlsConfig.InsecureSkipVerify = true
|
|
||||||
tlsConfig.VerifyConnection = func(state cftls.ConnectionState) error {
|
|
||||||
verifyOptions := x509.VerifyOptions{
|
|
||||||
DNSName: serverName,
|
|
||||||
Intermediates: x509.NewCertPool(),
|
|
||||||
}
|
|
||||||
for _, cert := range state.PeerCertificates[1:] {
|
|
||||||
verifyOptions.Intermediates.AddCert(cert)
|
|
||||||
}
|
|
||||||
_, err := state.PeerCertificates[0].Verify(verifyOptions)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(options.ALPN) > 0 {
|
|
||||||
tlsConfig.NextProtos = options.ALPN
|
|
||||||
}
|
|
||||||
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 cftls.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
|
|
||||||
if options.Certificate != "" {
|
|
||||||
certificate = []byte(options.Certificate)
|
|
||||||
} else if options.CertificatePath != "" {
|
|
||||||
content, err := os.ReadFile(options.CertificatePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "read certificate")
|
|
||||||
}
|
|
||||||
certificate = content
|
|
||||||
}
|
|
||||||
if len(certificate) > 0 {
|
|
||||||
certPool := x509.NewCertPool()
|
|
||||||
if !certPool.AppendCertsFromPEM(certificate) {
|
|
||||||
return nil, E.New("failed to parse certificate:\n\n", certificate)
|
|
||||||
}
|
|
||||||
tlsConfig.RootCAs = certPool
|
|
||||||
}
|
|
||||||
|
|
||||||
// ECH Config
|
|
||||||
|
|
||||||
tlsConfig.ECHEnabled = true
|
|
||||||
tlsConfig.PQSignatureSchemesEnabled = options.ECH.PQSignatureSchemesEnabled
|
|
||||||
tlsConfig.DynamicRecordSizingDisabled = options.ECH.DynamicRecordSizingDisabled
|
|
||||||
if options.ECH.Config != "" {
|
|
||||||
clientConfigContent, err := base64.StdEncoding.DecodeString(options.ECH.Config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
clientConfig, err := cftls.UnmarshalECHConfigs(clientConfigContent)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tlsConfig.ClientECHConfigs = clientConfig
|
|
||||||
} else {
|
|
||||||
tlsConfig.GetClientECHConfigs = fetchECHClientConfig(router)
|
|
||||||
}
|
|
||||||
return &ECHClientConfig{&tlsConfig}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchECHClientConfig(router adapter.Router) func(ctx context.Context, serverName string) ([]cftls.ECHConfig, error) {
|
|
||||||
return func(ctx context.Context, serverName string) ([]cftls.ECHConfig, error) {
|
|
||||||
message := &mDNS.Msg{
|
|
||||||
MsgHdr: mDNS.MsgHdr{
|
|
||||||
RecursionDesired: true,
|
|
||||||
},
|
|
||||||
Question: []mDNS.Question{
|
|
||||||
{
|
|
||||||
Name: serverName + ".",
|
|
||||||
Qtype: mDNS.TypeHTTPS,
|
|
||||||
Qclass: mDNS.ClassINET,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
response, err := router.Exchange(ctx, message)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if response.Rcode != mDNS.RcodeSuccess {
|
|
||||||
return nil, dns.RCodeError(response.Rcode)
|
|
||||||
}
|
|
||||||
for _, rr := range response.Answer {
|
|
||||||
switch resource := rr.(type) {
|
|
||||||
case *mDNS.HTTPS:
|
|
||||||
for _, value := range resource.Value {
|
|
||||||
if value.Key().String() == "ech" {
|
|
||||||
echConfig, err := base64.StdEncoding.DecodeString(value.String())
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "decode ECH config")
|
|
||||||
}
|
|
||||||
return cftls.UnmarshalECHConfigs(echConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, E.New("unknown resource record type: ", resource.Header().Rrtype)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, E.New("no ECH config found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
//go:build !with_ech
|
|
||||||
|
|
||||||
package tls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
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`)
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package tls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/pem"
|
|
||||||
"math/big"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GenerateKeyPair(serverName string) (*tls.Certificate, error) {
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
template := &x509.Certificate{
|
|
||||||
SerialNumber: serialNumber,
|
|
||||||
NotBefore: time.Now().Add(time.Hour * -1),
|
|
||||||
NotAfter: time.Now().Add(time.Hour),
|
|
||||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
Subject: pkix.Name{
|
|
||||||
CommonName: serverName,
|
|
||||||
},
|
|
||||||
DNSNames: []string{serverName},
|
|
||||||
}
|
|
||||||
publicDer, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
privateDer, err := x509.MarshalPKCS8PrivateKey(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
publicPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer})
|
|
||||||
privPem := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateDer})
|
|
||||||
keyPair, err := tls.X509KeyPair(publicPem, privPem)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &keyPair, err
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package tls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/badtls"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
|
|
||||||
if !options.Enabled {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return NewSTDServer(ctx, logger, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
|
|
||||||
tlsConn := config.Server(conn)
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
|
||||||
defer cancel()
|
|
||||||
err := tlsConn.HandshakeContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if stdConn, isSTD := tlsConn.(*tls.Conn); isSTD {
|
|
||||||
var badConn badtls.TLSConn
|
|
||||||
badConn, err = badtls.Create(stdConn)
|
|
||||||
if err == nil {
|
|
||||||
return badConn, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tlsConn, nil
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
package tls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errInsecureUnused = E.New("tls: insecure unused")
|
|
||||||
|
|
||||||
type STDServerConfig struct {
|
|
||||||
config *tls.Config
|
|
||||||
logger log.Logger
|
|
||||||
acmeService adapter.Service
|
|
||||||
certificate []byte
|
|
||||||
key []byte
|
|
||||||
certificatePath string
|
|
||||||
keyPath string
|
|
||||||
watcher *fsnotify.Watcher
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *STDServerConfig) ServerName() string {
|
|
||||||
return c.config.ServerName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *STDServerConfig) SetServerName(serverName string) {
|
|
||||||
c.config.ServerName = serverName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *STDServerConfig) NextProtos() []string {
|
|
||||||
return c.config.NextProtos
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
|
||||||
c.config.NextProtos = nextProto
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *STDServerConfig) Config() (*STDConfig, error) {
|
|
||||||
return c.config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *STDServerConfig) Client(conn net.Conn) Conn {
|
|
||||||
return tls.Client(conn, c.config)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *STDServerConfig) Server(conn net.Conn) Conn {
|
|
||||||
return tls.Server(conn, c.config)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *STDServerConfig) Clone() Config {
|
|
||||||
return &STDServerConfig{
|
|
||||||
config: c.config.Clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *STDServerConfig) Start() error {
|
|
||||||
if c.acmeService != nil {
|
|
||||||
return c.acmeService.Start()
|
|
||||||
} else {
|
|
||||||
if c.certificatePath == "" && c.keyPath == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err := c.startWatcher()
|
|
||||||
if err != nil {
|
|
||||||
c.logger.Warn("create fsnotify watcher: ", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *STDServerConfig) 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 *STDServerConfig) loopUpdate() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event, ok := <-c.watcher.Events:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if event.Op&fsnotify.Write != fsnotify.Write {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err := c.reloadKeyPair()
|
|
||||||
if err != nil {
|
|
||||||
c.logger.Error(E.Cause(err, "reload TLS key pair"))
|
|
||||||
}
|
|
||||||
case err, ok := <-c.watcher.Errors:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.logger.Error(E.Cause(err, "fsnotify error"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *STDServerConfig) reloadKeyPair() error {
|
|
||||||
if c.certificatePath != "" {
|
|
||||||
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 := tls.X509KeyPair(c.certificate, c.key)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "reload key pair")
|
|
||||||
}
|
|
||||||
c.config.Certificates = []tls.Certificate{keyPair}
|
|
||||||
c.logger.Info("reloaded TLS certificate")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *STDServerConfig) Close() error {
|
|
||||||
if c.acmeService != nil {
|
|
||||||
return c.acmeService.Close()
|
|
||||||
}
|
|
||||||
if c.watcher != nil {
|
|
||||||
return c.watcher.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSTDServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
|
|
||||||
if !options.Enabled {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
var tlsConfig *tls.Config
|
|
||||||
var acmeService adapter.Service
|
|
||||||
var err error
|
|
||||||
if options.ACME != nil && len(options.ACME.Domain) > 0 {
|
|
||||||
tlsConfig, acmeService, err = startACME(ctx, common.PtrValueOrDefault(options.ACME))
|
|
||||||
//nolint:staticcheck
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if options.Insecure {
|
|
||||||
return nil, errInsecureUnused
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tlsConfig = &tls.Config{}
|
|
||||||
}
|
|
||||||
if options.ServerName != "" {
|
|
||||||
tlsConfig.ServerName = options.ServerName
|
|
||||||
}
|
|
||||||
if len(options.ALPN) > 0 {
|
|
||||||
tlsConfig.NextProtos = append(tlsConfig.NextProtos, options.ALPN...)
|
|
||||||
}
|
|
||||||
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 acmeService == nil {
|
|
||||||
if options.Certificate != "" {
|
|
||||||
certificate = []byte(options.Certificate)
|
|
||||||
} else if options.CertificatePath != "" {
|
|
||||||
content, err := os.ReadFile(options.CertificatePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "read certificate")
|
|
||||||
}
|
|
||||||
certificate = content
|
|
||||||
}
|
|
||||||
if options.Key != "" {
|
|
||||||
key = []byte(options.Key)
|
|
||||||
} 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 && key == nil && options.Insecure {
|
|
||||||
tlsConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
||||||
return GenerateKeyPair(info.ServerName)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if certificate == nil {
|
|
||||||
return nil, E.New("missing certificate")
|
|
||||||
} else if key == nil {
|
|
||||||
return nil, E.New("missing key")
|
|
||||||
}
|
|
||||||
|
|
||||||
keyPair, err := tls.X509KeyPair(certificate, key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "parse x509 key pair")
|
|
||||||
}
|
|
||||||
tlsConfig.Certificates = []tls.Certificate{keyPair}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &STDServerConfig{
|
|
||||||
config: tlsConfig,
|
|
||||||
logger: logger,
|
|
||||||
acmeService: acmeService,
|
|
||||||
certificate: certificate,
|
|
||||||
key: key,
|
|
||||||
certificatePath: options.CertificatePath,
|
|
||||||
keyPath: options.KeyPath,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
//go:build with_utls
|
|
||||||
|
|
||||||
package tls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
|
|
||||||
utls "github.com/refraction-networking/utls"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UTLSClientConfig struct {
|
|
||||||
config *utls.Config
|
|
||||||
id utls.ClientHelloID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *UTLSClientConfig) ServerName() string {
|
|
||||||
return e.config.ServerName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *UTLSClientConfig) SetServerName(serverName string) {
|
|
||||||
e.config.ServerName = serverName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *UTLSClientConfig) NextProtos() []string {
|
|
||||||
return e.config.NextProtos
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *UTLSClientConfig) SetNextProtos(nextProto []string) {
|
|
||||||
e.config.NextProtos = nextProto
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *UTLSClientConfig) Config() (*STDConfig, error) {
|
|
||||||
return nil, E.New("unsupported usage for uTLS")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *UTLSClientConfig) Client(conn net.Conn) Conn {
|
|
||||||
return &utlsConnWrapper{utls.UClient(conn, e.config.Clone(), e.id)}
|
|
||||||
}
|
|
||||||
|
|
||||||
type utlsConnWrapper struct {
|
|
||||||
*utls.UConn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *utlsConnWrapper) ConnectionState() tls.ConnectionState {
|
|
||||||
state := c.Conn.ConnectionState()
|
|
||||||
return tls.ConnectionState{
|
|
||||||
Version: state.Version,
|
|
||||||
HandshakeComplete: state.HandshakeComplete,
|
|
||||||
DidResume: state.DidResume,
|
|
||||||
CipherSuite: state.CipherSuite,
|
|
||||||
NegotiatedProtocol: state.NegotiatedProtocol,
|
|
||||||
NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual,
|
|
||||||
ServerName: state.ServerName,
|
|
||||||
PeerCertificates: state.PeerCertificates,
|
|
||||||
VerifiedChains: state.VerifiedChains,
|
|
||||||
SignedCertificateTimestamps: state.SignedCertificateTimestamps,
|
|
||||||
OCSPResponse: state.OCSPResponse,
|
|
||||||
TLSUnique: state.TLSUnique,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *UTLSClientConfig) Clone() Config {
|
|
||||||
return &UTLSClientConfig{
|
|
||||||
config: e.config.Clone(),
|
|
||||||
id: e.id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUTLSClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
|
||||||
var serverName string
|
|
||||||
if options.ServerName != "" {
|
|
||||||
serverName = options.ServerName
|
|
||||||
} else if serverAddress != "" {
|
|
||||||
if _, err := netip.ParseAddr(serverName); err != nil {
|
|
||||||
serverName = serverAddress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if serverName == "" && !options.Insecure {
|
|
||||||
return nil, E.New("missing server_name or insecure=true")
|
|
||||||
}
|
|
||||||
|
|
||||||
var tlsConfig utls.Config
|
|
||||||
if options.DisableSNI {
|
|
||||||
tlsConfig.ServerName = "127.0.0.1"
|
|
||||||
} else {
|
|
||||||
tlsConfig.ServerName = serverName
|
|
||||||
}
|
|
||||||
if options.Insecure {
|
|
||||||
tlsConfig.InsecureSkipVerify = options.Insecure
|
|
||||||
} else if options.DisableSNI {
|
|
||||||
return nil, E.New("disable_sni is unsupported in uTLS")
|
|
||||||
}
|
|
||||||
if len(options.ALPN) > 0 {
|
|
||||||
tlsConfig.NextProtos = options.ALPN
|
|
||||||
}
|
|
||||||
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
|
|
||||||
if options.Certificate != "" {
|
|
||||||
certificate = []byte(options.Certificate)
|
|
||||||
} else if options.CertificatePath != "" {
|
|
||||||
content, err := os.ReadFile(options.CertificatePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "read certificate")
|
|
||||||
}
|
|
||||||
certificate = content
|
|
||||||
}
|
|
||||||
if len(certificate) > 0 {
|
|
||||||
certPool := x509.NewCertPool()
|
|
||||||
if !certPool.AppendCertsFromPEM(certificate) {
|
|
||||||
return nil, E.New("failed to parse certificate:\n\n", certificate)
|
|
||||||
}
|
|
||||||
tlsConfig.RootCAs = certPool
|
|
||||||
}
|
|
||||||
var id utls.ClientHelloID
|
|
||||||
switch options.UTLS.Fingerprint {
|
|
||||||
case "chrome", "":
|
|
||||||
id = utls.HelloChrome_Auto
|
|
||||||
case "firefox":
|
|
||||||
id = utls.HelloFirefox_Auto
|
|
||||||
case "edge":
|
|
||||||
id = utls.HelloEdge_Auto
|
|
||||||
case "safari":
|
|
||||||
id = utls.HelloSafari_Auto
|
|
||||||
case "360":
|
|
||||||
id = utls.Hello360_Auto
|
|
||||||
case "qq":
|
|
||||||
id = utls.HelloQQ_Auto
|
|
||||||
case "ios":
|
|
||||||
id = utls.HelloIOS_Auto
|
|
||||||
case "android":
|
|
||||||
id = utls.HelloAndroid_11_OkHttp
|
|
||||||
case "random":
|
|
||||||
id = utls.HelloRandomized
|
|
||||||
default:
|
|
||||||
return nil, E.New("unknown uTLS fingerprint: ", options.UTLS.Fingerprint)
|
|
||||||
}
|
|
||||||
return &UTLSClientConfig{&tlsConfig, id}, nil
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
//go:build !with_utls
|
|
||||||
|
|
||||||
package tls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewUTLSClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
|
||||||
return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`)
|
|
||||||
}
|
|
||||||
145
common/trafficcontrol/manager.go
Normal file
145
common/trafficcontrol/manager.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package trafficcontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"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 Manager[U comparable] struct {
|
||||||
|
access sync.Mutex
|
||||||
|
users map[U]*Traffic
|
||||||
|
}
|
||||||
|
|
||||||
|
type Traffic struct {
|
||||||
|
Upload uint64
|
||||||
|
Download uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager[U comparable]() *Manager[U] {
|
||||||
|
return &Manager[U]{
|
||||||
|
users: make(map[U]*Traffic),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager[U]) Reset() {
|
||||||
|
m.users = make(map[U]*Traffic)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager[U]) TrackConnection(user U, conn net.Conn) net.Conn {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
var traffic *Traffic
|
||||||
|
if t, loaded := m.users[user]; loaded {
|
||||||
|
traffic = t
|
||||||
|
} else {
|
||||||
|
traffic = new(Traffic)
|
||||||
|
m.users[user] = traffic
|
||||||
|
}
|
||||||
|
return &TrackConn{conn, traffic}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager[U]) TrackPacketConnection(user U, conn N.PacketConn) N.PacketConn {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
var traffic *Traffic
|
||||||
|
if t, loaded := m.users[user]; loaded {
|
||||||
|
traffic = t
|
||||||
|
} else {
|
||||||
|
traffic = new(Traffic)
|
||||||
|
m.users[user] = traffic
|
||||||
|
}
|
||||||
|
return &TrackPacketConn{conn, traffic}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager[U]) ReadTraffics() map[U]Traffic {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
|
||||||
|
trafficMap := make(map[U]Traffic)
|
||||||
|
for user, traffic := range m.users {
|
||||||
|
upload := atomic.SwapUint64(&traffic.Upload, 0)
|
||||||
|
download := atomic.SwapUint64(&traffic.Download, 0)
|
||||||
|
if upload == 0 && download == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
trafficMap[user] = Traffic{
|
||||||
|
Upload: upload,
|
||||||
|
Download: download,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trafficMap
|
||||||
|
}
|
||||||
|
|
||||||
|
type TrackConn struct {
|
||||||
|
net.Conn
|
||||||
|
*Traffic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TrackConn) Read(p []byte) (n int, err error) {
|
||||||
|
n, err = c.Conn.Read(p)
|
||||||
|
if n > 0 {
|
||||||
|
atomic.AddUint64(&c.Upload, uint64(n))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TrackConn) Write(p []byte) (n int, err error) {
|
||||||
|
n, err = c.Conn.Write(p)
|
||||||
|
if n > 0 {
|
||||||
|
atomic.AddUint64(&c.Download, uint64(n))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TrackConn) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
|
n, err = bufio.Copy(w, c.Conn)
|
||||||
|
if n > 0 {
|
||||||
|
atomic.AddUint64(&c.Upload, uint64(n))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TrackConn) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
|
n, err = bufio.Copy(c.Conn, r)
|
||||||
|
if n > 0 {
|
||||||
|
atomic.AddUint64(&c.Download, uint64(n))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TrackConn) Upstream() any {
|
||||||
|
return c.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
type TrackPacketConn struct {
|
||||||
|
N.PacketConn
|
||||||
|
*Traffic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TrackPacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) {
|
||||||
|
destination, err := c.PacketConn.ReadPacket(buffer)
|
||||||
|
if err == nil {
|
||||||
|
atomic.AddUint64(&c.Upload, uint64(buffer.Len()))
|
||||||
|
}
|
||||||
|
return destination, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TrackPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||||
|
n := buffer.Len()
|
||||||
|
err := c.PacketConn.WritePacket(buffer, destination)
|
||||||
|
if err == nil {
|
||||||
|
atomic.AddUint64(&c.Download, uint64(n))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TrackPacketConn) Upstream() any {
|
||||||
|
return c.PacketConn
|
||||||
|
}
|
||||||
11
constant/dns.go
Normal file
11
constant/dns.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
type DomainStrategy = uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
DomainStrategyAsIS DomainStrategy = iota
|
||||||
|
DomainStrategyPreferIPv4
|
||||||
|
DomainStrategyPreferIPv6
|
||||||
|
DomainStrategyUseIPv4
|
||||||
|
DomainStrategyUseIPv6
|
||||||
|
)
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package constant
|
|
||||||
|
|
||||||
import E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
|
|
||||||
var ErrTLSRequired = E.New("TLS required")
|
|
||||||
|
|
||||||
var ErrQUICNotIncluded = E.New(`QUIC is not included in this build, rebuild with -tags with_quic`)
|
|
||||||
@@ -20,7 +20,7 @@ const IsIos = goos.IsIos == 1
|
|||||||
|
|
||||||
const IsJs = goos.IsJs == 1
|
const IsJs = goos.IsJs == 1
|
||||||
|
|
||||||
const IsLinux = goos.IsLinux == 1 || goos.IsAndroid == 1
|
const IsLinux = goos.IsLinux == 1
|
||||||
|
|
||||||
const IsNacl = goos.IsNacl == 1
|
const IsNacl = goos.IsNacl == 1
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build unix || linux
|
//go:build unix
|
||||||
|
|
||||||
package constant
|
package constant
|
||||||
|
|
||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
resourcePaths = append(resourcePaths, "/etc")
|
resourcePaths = append(resourcePaths, "/etc/config")
|
||||||
resourcePaths = append(resourcePaths, "/usr/share")
|
resourcePaths = append(resourcePaths, "/usr/share")
|
||||||
resourcePaths = append(resourcePaths, "/usr/local/etc")
|
resourcePaths = append(resourcePaths, "/usr/local/etc/config")
|
||||||
resourcePaths = append(resourcePaths, "/usr/local/share")
|
resourcePaths = append(resourcePaths, "/usr/local/share")
|
||||||
if homeDir := os.Getenv("HOME"); homeDir != "" {
|
if homeDir := os.Getenv("HOME"); homeDir != "" {
|
||||||
resourcePaths = append(resourcePaths, homeDir+"/.local/share")
|
resourcePaths = append(resourcePaths, homeDir+"/.local/share")
|
||||||
|
|||||||
@@ -1,29 +1,21 @@
|
|||||||
package constant
|
package constant
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TypeTun = "tun"
|
TypeTun = "tun"
|
||||||
TypeRedirect = "redirect"
|
TypeRedirect = "redirect"
|
||||||
TypeTProxy = "tproxy"
|
TypeTProxy = "tproxy"
|
||||||
TypeDirect = "direct"
|
TypeDirect = "direct"
|
||||||
TypeBlock = "block"
|
TypeBlock = "block"
|
||||||
TypeDNS = "dns"
|
TypeDNS = "dns"
|
||||||
TypeSocks = "socks"
|
TypeSocks = "socks"
|
||||||
TypeHTTP = "http"
|
TypeHTTP = "http"
|
||||||
TypeMixed = "mixed"
|
TypeMixed = "mixed"
|
||||||
TypeShadowsocks = "shadowsocks"
|
TypeShadowsocks = "shadowsocks"
|
||||||
TypeVMess = "vmess"
|
TypeVMess = "vmess"
|
||||||
TypeTrojan = "trojan"
|
TypeTrojan = "trojan"
|
||||||
TypeNaive = "naive"
|
TypeNaive = "naive"
|
||||||
TypeWireGuard = "wireguard"
|
|
||||||
TypeHysteria = "hysteria"
|
|
||||||
TypeTor = "tor"
|
|
||||||
TypeSSH = "ssh"
|
|
||||||
TypeShadowTLS = "shadowtls"
|
|
||||||
TypeShadowsocksR = "shadowsocksr"
|
|
||||||
TypeVLESS = "vless"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TypeSelector = "selector"
|
TypeSelector = "selector"
|
||||||
TypeURLTest = "urltest"
|
|
||||||
)
|
)
|
||||||
|
|||||||
5
constant/quic.go
Normal file
5
constant/quic.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//go:build with_quic
|
||||||
|
|
||||||
|
package constant
|
||||||
|
|
||||||
|
const QUIC_AVAILABLE = true
|
||||||
5
constant/quic_stub.go
Normal file
5
constant/quic_stub.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//go:build !with_quic
|
||||||
|
|
||||||
|
package constant
|
||||||
|
|
||||||
|
const QUIC_AVAILABLE = false
|
||||||
@@ -3,11 +3,10 @@ package constant
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TCPTimeout = 5 * time.Second
|
TCPTimeout = 5 * time.Second
|
||||||
ReadPayloadTimeout = 300 * time.Millisecond
|
ReadPayloadTimeout = 300 * time.Millisecond
|
||||||
DNSTimeout = 10 * time.Second
|
DNSTimeout = 10 * time.Second
|
||||||
QUICTimeout = 30 * time.Second
|
QUICTimeout = 30 * time.Second
|
||||||
STUNTimeout = 15 * time.Second
|
STUNTimeout = 15 * time.Second
|
||||||
UDPTimeout = 5 * time.Minute
|
UDPTimeout = 5 * time.Minute
|
||||||
DefaultURLTestInterval = 1 * time.Minute
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
package constant
|
|
||||||
|
|
||||||
const (
|
|
||||||
V2RayTransportTypeHTTP = "http"
|
|
||||||
V2RayTransportTypeWebsocket = "ws"
|
|
||||||
V2RayTransportTypeQUIC = "quic"
|
|
||||||
V2RayTransportTypeGRPC = "grpc"
|
|
||||||
)
|
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
package constant
|
package constant
|
||||||
|
|
||||||
var Version = "1.1"
|
var (
|
||||||
|
Version = "20220812"
|
||||||
|
Commit = ""
|
||||||
|
)
|
||||||
|
|||||||
9
docs/benchmark.md
Normal file
9
docs/benchmark.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Benchmark
|
||||||
|
|
||||||
|
## Shadowsocks
|
||||||
|
|
||||||
|
| / | none | aes-128-gcm | 2022-blake3-aes-128-gcm |
|
||||||
|
|------------------------------------|:-----------:|:-----------:|:-----------------------:|
|
||||||
|
| v2ray-core (5.0.7) | 13.0 Gbps | 5.02 Gbps | / |
|
||||||
|
| shadowsocks-rust (v1.15.0-alpha.5) | 10.7 Gbps | / | 9.36 Gbps |
|
||||||
|
| sing-box | 29.0 Gbps | / | 11.8 Gbps |
|
||||||
@@ -1,463 +1,19 @@
|
|||||||
#### 1.1
|
#### 2022/08/12
|
||||||
|
|
||||||
* Fix close clash cache
|
|
||||||
|
|
||||||
Important changes since 1.0:
|
|
||||||
|
|
||||||
* Add support for use with android VPNService
|
|
||||||
* Add tun support for WireGuard outbound
|
|
||||||
* Add system tun stack
|
|
||||||
* Add comment filter for config
|
|
||||||
* Add option for allow optional proxy protocol header
|
|
||||||
* Add Clash mode and persistence support
|
|
||||||
* Add TLS ECH and uTLS support for outbound TLS options
|
|
||||||
* Add internal simple-obfs and v2ray-plugin
|
|
||||||
* Add ShadowsocksR outbound
|
|
||||||
* Add VLESS outbound and XUDP
|
|
||||||
* Skip wait for hysteria tcp handshake response
|
|
||||||
* Add v2ray mux support for all inbound
|
|
||||||
* Add XUDP support for VMess
|
|
||||||
* Improve websocket writer
|
|
||||||
* Refine tproxy write back
|
|
||||||
* Fix DNS leak caused by
|
|
||||||
Windows' ordinary multihomed DNS resolution behavior
|
|
||||||
* Add sniff_timeout listen option
|
|
||||||
* Add custom route support for tun
|
|
||||||
* Add option for custom wireguard reserved bytes
|
|
||||||
* Split bind_address into ipv4 and ipv6
|
|
||||||
* Add ShadowTLS v1 and v2 support
|
|
||||||
|
|
||||||
#### 1.1-rc1
|
|
||||||
|
|
||||||
* Fix TLS config for h2 server
|
|
||||||
* Fix crash when input bad method in shadowsocks multi-user inbound
|
|
||||||
* Fix listen UDP
|
|
||||||
* Fix check invalid packet on macOS
|
|
||||||
|
|
||||||
#### 1.1-beta18
|
|
||||||
|
|
||||||
* Enhance defense against active probe for shadowtls server **1**
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
The `fallback_after` option has been removed.
|
|
||||||
|
|
||||||
#### 1.1-beta17
|
|
||||||
|
|
||||||
* Fix shadowtls server **1**
|
|
||||||
|
|
||||||
*1*:
|
|
||||||
|
|
||||||
Added [fallback_after](/configuration/inbound/shadowtls#fallback_after) option.
|
|
||||||
|
|
||||||
#### 1.0.7
|
|
||||||
|
|
||||||
* Add support for new x/h2 deadline
|
|
||||||
* Fix copy pipe
|
|
||||||
* Fix decrypt xplus packet
|
|
||||||
* Fix macOS Ventura process name match
|
|
||||||
* Fix smux keepalive
|
|
||||||
* Fix vmess request buffer
|
|
||||||
* Fix h2c transport
|
|
||||||
* Fix tor geoip
|
|
||||||
* Fix udp connect for mux client
|
|
||||||
* Fix default dns transport strategy
|
|
||||||
|
|
||||||
#### 1.1-beta16
|
|
||||||
|
|
||||||
* Improve shadowtls server
|
|
||||||
* Fix default dns transport strategy
|
|
||||||
* Update uTLS to v1.2.0
|
|
||||||
|
|
||||||
#### 1.1-beta15
|
|
||||||
|
|
||||||
* Add support for new x/h2 deadline
|
|
||||||
* Fix udp connect for mux client
|
|
||||||
* Fix dns buffer
|
|
||||||
* Fix quic dns retry
|
|
||||||
* Fix create TLS config
|
|
||||||
* Fix websocket alpn
|
|
||||||
* Fix tor geoip
|
|
||||||
|
|
||||||
#### 1.1-beta14
|
|
||||||
|
|
||||||
* Add multi-user support for hysteria inbound **1**
|
|
||||||
* Add custom tls client support for std grpc
|
|
||||||
* Fix smux keep alive
|
|
||||||
* Fix vmess request buffer
|
|
||||||
* Fix default local DNS server behavior
|
|
||||||
* Fix h2c transport
|
|
||||||
|
|
||||||
*1*:
|
|
||||||
|
|
||||||
The `auth` and `auth_str` fields have been replaced by the `users` field.
|
|
||||||
|
|
||||||
#### 1.1-beta13
|
|
||||||
|
|
||||||
* Add custom worker count option for WireGuard outbound
|
|
||||||
* Split bind_address into ipv4 and ipv6
|
|
||||||
* Move WFP manipulation to strict route
|
|
||||||
* Fix WireGuard outbound panic when close
|
|
||||||
* Fix macOS Ventura process name match
|
|
||||||
* Fix QUIC connection migration by @HyNetwork
|
|
||||||
* Fix handling QUIC client SNI by @HyNetwork
|
|
||||||
|
|
||||||
#### 1.1-beta12
|
|
||||||
|
|
||||||
* Fix uTLS config
|
|
||||||
* Update quic-go to v0.30.0
|
|
||||||
* Update cloudflare-tls to go1.18.7
|
|
||||||
|
|
||||||
#### 1.1-beta11
|
|
||||||
|
|
||||||
* Add option for custom wireguard reserved bytes
|
|
||||||
* Fix shadowtls v2
|
|
||||||
* Fix h3 dns transport
|
|
||||||
* Fix copy pipe
|
|
||||||
* Fix decrypt xplus packet
|
|
||||||
* Fix v2ray api
|
|
||||||
* Suppress no network error
|
|
||||||
* Improve local dns transport
|
|
||||||
|
|
||||||
#### 1.1-beta10
|
|
||||||
|
|
||||||
* Add [sniff_timeout](/configuration/shared/listen#sniff_timeout) listen option
|
|
||||||
* Add [custom route](/configuration/inbound/tun#inet4_route_address) support for tun **1**
|
|
||||||
* Fix interface monitor
|
|
||||||
* Fix websocket headroom
|
|
||||||
* Fix uTLS handshake
|
|
||||||
* Fix ssh outbound
|
|
||||||
* Fix sniff fragmented quic client hello
|
|
||||||
* Fix DF for hysteria
|
|
||||||
* Fix naive overflow
|
|
||||||
* Check destination before udp connect
|
|
||||||
* Update uTLS to v1.1.5
|
|
||||||
* Update tfo-go to v2.0.2
|
|
||||||
* Update fsnotify to v1.6.0
|
|
||||||
* Update grpc to v1.50.1
|
|
||||||
|
|
||||||
*1*:
|
|
||||||
|
|
||||||
The `strict_route` on windows is removed.
|
|
||||||
|
|
||||||
#### 1.0.6
|
|
||||||
|
|
||||||
* Fix ssh outbound
|
|
||||||
* Fix sniff fragmented quic client hello
|
|
||||||
* Fix naive overflow
|
|
||||||
* Check destination before udp connect
|
|
||||||
|
|
||||||
#### 1.1-beta9
|
|
||||||
|
|
||||||
* Fix windows route **1**
|
|
||||||
* Add [v2ray statistics api](/configuration/experimental#v2ray-api-fields)
|
|
||||||
* Add ShadowTLS v2 support **2**
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
* Fix DNS leak caused by
|
|
||||||
Windows' [ordinary multihomed DNS resolution behavior](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29)
|
|
||||||
* Flush Windows DNS cache when start/close
|
|
||||||
|
|
||||||
**2**:
|
|
||||||
|
|
||||||
See [ShadowTLS inbound](/configuration/inbound/shadowtls#version)
|
|
||||||
and [ShadowTLS outbound](/configuration/outbound/shadowtls#version)
|
|
||||||
|
|
||||||
#### 1.1-beta8
|
|
||||||
|
|
||||||
* Fix leaks on close
|
|
||||||
* Improve websocket writer
|
|
||||||
* Refine tproxy write back
|
|
||||||
* Refine 4in6 processing
|
|
||||||
* Fix shadowsocks plugins
|
|
||||||
* Fix missing source address from transport connection
|
|
||||||
* Fix fqdn socks5 outbound connection
|
|
||||||
* Fix read source address from grpc-go
|
|
||||||
|
|
||||||
#### 1.0.5
|
|
||||||
|
|
||||||
* Fix missing source address from transport connection
|
|
||||||
* Fix fqdn socks5 outbound connection
|
|
||||||
* Fix read source address from grpc-go
|
|
||||||
|
|
||||||
#### 1.1-beta7
|
|
||||||
|
|
||||||
* Add v2ray mux and XUDP support for VMess inbound
|
|
||||||
* Add XUDP support for VMess outbound
|
|
||||||
* Disable DF on direct outbound by default
|
|
||||||
* Fix bugs in 1.1-beta6
|
|
||||||
|
|
||||||
#### 1.1-beta6
|
|
||||||
|
|
||||||
* Add [URLTest outbound](/configuration/outbound/urltest)
|
|
||||||
* Fix bugs in 1.1-beta5
|
|
||||||
|
|
||||||
#### 1.1-beta5
|
|
||||||
|
|
||||||
* Print tags in version command
|
|
||||||
* Redirect clash hello to external ui
|
|
||||||
* Move shadowsocksr implementation to clash
|
|
||||||
* Make gVisor optional **1**
|
|
||||||
* Refactor to miekg/dns
|
|
||||||
* Refactor bind control
|
|
||||||
* Fix build on go1.18
|
|
||||||
* Fix clash store-selected
|
|
||||||
* Fix close grpc conn
|
|
||||||
* Fix port rule match logic
|
|
||||||
* Fix clash api proxy type
|
|
||||||
|
|
||||||
*1*:
|
|
||||||
|
|
||||||
The build tag `no_gvisor` is replaced by `with_gvisor`.
|
|
||||||
|
|
||||||
The default tun stack is changed to system.
|
|
||||||
|
|
||||||
#### 1.0.4
|
|
||||||
|
|
||||||
* Fix close grpc conn
|
|
||||||
* Fix port rule match logic
|
|
||||||
* Fix clash api proxy type
|
|
||||||
|
|
||||||
#### 1.1-beta4
|
|
||||||
|
|
||||||
* Add internal simple-obfs and v2ray-plugin [Shadowsocks plugins](/configuration/outbound/shadowsocks#plugin)
|
|
||||||
* Add [ShadowsocksR outbound](/configuration/outbound/shadowsocksr)
|
|
||||||
* Add [VLESS outbound and XUDP](/configuration/outbound/vless)
|
|
||||||
* Skip wait for hysteria tcp handshake response
|
|
||||||
* Fix socks4 client
|
|
||||||
* Fix hysteria inbound
|
|
||||||
* Fix concurrent write
|
|
||||||
|
|
||||||
#### 1.0.3
|
|
||||||
|
|
||||||
* Fix socks4 client
|
|
||||||
* Fix hysteria inbound
|
|
||||||
* Fix concurrent write
|
|
||||||
|
|
||||||
#### 1.1-beta3
|
|
||||||
|
|
||||||
* Fix using custom TLS client in http2 client
|
|
||||||
* Fix bugs in 1.1-beta2
|
|
||||||
|
|
||||||
#### 1.1-beta2
|
|
||||||
|
|
||||||
* Add Clash mode and persistence support **1**
|
|
||||||
* Add TLS ECH and uTLS support for outbound TLS options **2**
|
|
||||||
* Fix socks4 request
|
|
||||||
* Fix processing empty dns result
|
|
||||||
|
|
||||||
*1*:
|
|
||||||
|
|
||||||
Switching modes using the Clash API, and `store-selected` are now supported,
|
|
||||||
see [Experimental](/configuration/experimental).
|
|
||||||
|
|
||||||
*2*:
|
|
||||||
|
|
||||||
ECH (Encrypted Client Hello) is a TLS extension that allows a client to encrypt the first part of its ClientHello
|
|
||||||
message, see [TLS#ECH](/configuration/shared/tls#ech).
|
|
||||||
|
|
||||||
uTLS is a fork of "crypto/tls", which provides ClientHello fingerprinting resistance,
|
|
||||||
see [TLS#uTLS](/configuration/shared/tls#utls).
|
|
||||||
|
|
||||||
#### 1.0.2
|
|
||||||
|
|
||||||
* Fix socks4 request
|
|
||||||
* Fix processing empty dns result
|
|
||||||
|
|
||||||
#### 1.1-beta1
|
|
||||||
|
|
||||||
* Add support for use with android VPNService **1**
|
|
||||||
* Add tun support for WireGuard outbound **2**
|
|
||||||
* Add system tun stack **3**
|
|
||||||
* Add comment filter for config **4**
|
|
||||||
* Add option for allow optional proxy protocol header
|
|
||||||
* Add half close for smux
|
|
||||||
* Set UDP DF by default **5**
|
|
||||||
* Set default tun mtu to 9000
|
|
||||||
* Update gVisor to 20220905.0
|
|
||||||
|
|
||||||
*1*:
|
|
||||||
|
|
||||||
In previous versions, Android VPN would not work with tun enabled.
|
|
||||||
|
|
||||||
The usage of tun over VPN and VPN over tun is now supported, see [Tun Inbound](/configuration/inbound/tun#auto_route).
|
|
||||||
|
|
||||||
*2*:
|
|
||||||
|
|
||||||
In previous releases, WireGuard outbound support was backed by the lower performance gVisor virtual interface.
|
|
||||||
|
|
||||||
It achieves the same performance as wireguard-go by providing automatic system interface support.
|
|
||||||
|
|
||||||
*3*:
|
|
||||||
|
|
||||||
It does not depend on gVisor and has better performance in some cases.
|
|
||||||
|
|
||||||
It is less compatible and may not be available in some environments.
|
|
||||||
|
|
||||||
*4*:
|
|
||||||
|
|
||||||
Annotated json configuration files are now supported.
|
|
||||||
|
|
||||||
*5*:
|
|
||||||
|
|
||||||
UDP fragmentation is now blocked by default.
|
|
||||||
|
|
||||||
Including shadowsocks-libev, shadowsocks-rust and quic-go all disable segmentation by default.
|
|
||||||
|
|
||||||
See [Dial Fields](/configuration/shared/dial#udp_fragment)
|
|
||||||
and [Listen Fields](/configuration/shared/listen#udp_fragment).
|
|
||||||
|
|
||||||
#### 1.0.1
|
|
||||||
|
|
||||||
* Fix match 4in6 address in ip_cidr
|
|
||||||
* Fix clash api log level format error
|
|
||||||
* Fix clash api unknown proxy type
|
|
||||||
|
|
||||||
#### 1.0
|
|
||||||
|
|
||||||
* Fix wireguard reconnect
|
|
||||||
* Fix naive inbound
|
|
||||||
* Fix json format error message
|
|
||||||
* Fix processing vmess termination signal
|
|
||||||
* Fix hysteria stream error
|
|
||||||
* Fix listener close when proxyproto failed
|
|
||||||
|
|
||||||
#### 1.0-rc1
|
|
||||||
|
|
||||||
* Fix write log timestamp
|
|
||||||
* Fix write zero
|
|
||||||
* Fix dial parallel in direct outbound
|
|
||||||
* Fix write trojan udp
|
|
||||||
* Fix DNS routing
|
|
||||||
* Add attribute support for geosite
|
|
||||||
* Update documentation for [Dial Fields](/configuration/shared/dial)
|
|
||||||
|
|
||||||
#### 1.0-beta3
|
|
||||||
|
|
||||||
* Add [chained inbound](/configuration/shared/listen#detour) support
|
|
||||||
* Add process_path rule item
|
|
||||||
* Add macOS redirect support
|
|
||||||
* Add ShadowTLS [Inbound](/configuration/inbound/shadowtls), [Outbound](/configuration/outbound/shadowtls)
|
|
||||||
and [Examples](/examples/shadowtls)
|
|
||||||
* Fix search android package in non-owner users
|
|
||||||
* Fix socksaddr type condition
|
|
||||||
* Fix smux session status
|
|
||||||
* Refactor inbound and outbound documentation
|
|
||||||
* Minor fixes
|
|
||||||
|
|
||||||
#### 1.0-beta2
|
|
||||||
|
|
||||||
* Add strict_route option for [Tun inbound](/configuration/inbound/tun#strict_route)
|
|
||||||
* Add packetaddr support for [VMess outbound](/configuration/outbound/vmess#packet_addr)
|
|
||||||
* Add better performing alternative gRPC implementation
|
|
||||||
* Add [docker image](https://github.com/SagerNet/sing-box/pkgs/container/sing-box)
|
|
||||||
* Fix sniff override destination
|
|
||||||
|
|
||||||
#### 1.0-beta1
|
|
||||||
|
|
||||||
* Initial release
|
|
||||||
|
|
||||||
##### 2022/08/26
|
|
||||||
|
|
||||||
* Fix ipv6 route on linux
|
|
||||||
* Fix read DNS message
|
|
||||||
|
|
||||||
##### 2022/08/25
|
|
||||||
|
|
||||||
* Let vmess use zero instead of auto if TLS enabled
|
|
||||||
* Add trojan fallback for ALPN
|
|
||||||
* Improve ip_cidr rule
|
|
||||||
* Fix format bind_address
|
|
||||||
* Fix http proxy with compressed response
|
|
||||||
* Fix route connections
|
|
||||||
|
|
||||||
##### 2022/08/24
|
|
||||||
|
|
||||||
* Fix naive padding
|
|
||||||
* Fix unix search path
|
|
||||||
* Fix close non-duplex connections
|
|
||||||
* Add ACME EAB support
|
|
||||||
* Fix early close on windows and catch any
|
|
||||||
* Initial zh-CN document translation
|
|
||||||
|
|
||||||
##### 2022/08/23
|
|
||||||
|
|
||||||
* Add [V2Ray Transport](/configuration/shared/v2ray-transport) support for VMess and Trojan
|
|
||||||
* Allow plain http request in Naive inbound (It can now be used with nginx)
|
|
||||||
* Add proxy protocol support
|
|
||||||
* Free memory after start
|
|
||||||
* Parse X-Forward-For in HTTP requests
|
|
||||||
* Handle SIGHUP signal
|
|
||||||
|
|
||||||
##### 2022/08/22
|
|
||||||
|
|
||||||
* Add strategy setting for each [DNS server](/configuration/dns/server)
|
|
||||||
* Add bind address to outbound options
|
|
||||||
|
|
||||||
##### 2022/08/21
|
|
||||||
|
|
||||||
* Add [Tor outbound](/configuration/outbound/tor)
|
|
||||||
* Add [SSH outbound](/configuration/outbound/ssh)
|
|
||||||
|
|
||||||
##### 2022/08/20
|
|
||||||
|
|
||||||
* Attempt to unwrap ip-in-fqdn socksaddr
|
|
||||||
* Fix read packages in android 12
|
|
||||||
* Fix route on some android devices
|
|
||||||
* Improve linux process searcher
|
|
||||||
* Fix write socks5 username password auth request
|
|
||||||
* Skip bind connection with private destination to interface
|
|
||||||
* Add [Trojan connection fallback](/configuration/inbound/trojan#fallback)
|
|
||||||
|
|
||||||
##### 2022/08/19
|
|
||||||
|
|
||||||
* Add Hysteria [Inbound](/configuration/inbound/hysteria) and [Outbund](/configuration/outbound/hysteria)
|
|
||||||
* Add [ACME TLS certificate issuer](/configuration/shared/tls)
|
|
||||||
* Allow read config from stdin (-c stdin)
|
|
||||||
* Update gVisor to 20220815.0
|
|
||||||
|
|
||||||
##### 2022/08/18
|
|
||||||
|
|
||||||
* Fix find process with lwip stack
|
|
||||||
* Fix crash on shadowsocks server
|
|
||||||
* Fix crash on darwin tun
|
|
||||||
* Fix write log to file
|
|
||||||
|
|
||||||
##### 2022/08/17
|
|
||||||
|
|
||||||
* Improve async dns transports
|
|
||||||
|
|
||||||
##### 2022/08/16
|
|
||||||
|
|
||||||
* Add ip_version (route/dns) rule item
|
|
||||||
* Add [WireGuard](/configuration/outbound/wireguard) outbound
|
|
||||||
|
|
||||||
##### 2022/08/15
|
|
||||||
|
|
||||||
* Add uid, android user and package rules support in [Tun](/configuration/inbound/tun) routing.
|
|
||||||
|
|
||||||
##### 2022/08/13
|
|
||||||
|
|
||||||
* Fix dns concurrent write
|
|
||||||
|
|
||||||
##### 2022/08/12
|
|
||||||
|
|
||||||
* Performance improvements
|
* Performance improvements
|
||||||
* Add UoT option for [SOCKS](/configuration/outbound/socks) outbound
|
* Add UoT option for [Socks](/configuration/outbound/socks) outbound
|
||||||
|
|
||||||
##### 2022/08/11
|
#### 2022/08/11
|
||||||
|
|
||||||
* Add UoT option for [Shadowsocks](/configuration/outbound/shadowsocks) outbound, UoT support for all inbounds
|
* Add UoT option for [Shadowsocks](/configuration/outbound/shadowsocks) outbound, UoT support for all inbounds
|
||||||
|
|
||||||
##### 2022/08/10
|
#### 2022/08/10
|
||||||
|
|
||||||
* Add full-featured [Naive](/configuration/inbound/naive) inbound
|
* Add full-featured [Naive](/configuration/inbound/naive) inbound
|
||||||
* Fix default dns server option [#9] by iKirby
|
* Fix default dns server option [#9] by iKirby
|
||||||
|
|
||||||
##### 2022/08/09
|
#### 2022/08/09
|
||||||
|
|
||||||
No changelog before.
|
No changelog before.
|
||||||
|
|
||||||
[#9]: https://github.com/SagerNet/sing-box/pull/9
|
[#9]: https://github.com/SagerNet/sing-box/pull/9
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
# DNS
|
|
||||||
|
|
||||||
### Structure
|
### Structure
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -35,8 +33,6 @@ Default domain strategy for resolving the domain names.
|
|||||||
|
|
||||||
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
||||||
|
|
||||||
Take no effect if `server.strategy` is set.
|
|
||||||
|
|
||||||
#### disable_cache
|
#### disable_cache
|
||||||
|
|
||||||
Disable dns cache.
|
Disable dns cache.
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
# DNS
|
|
||||||
|
|
||||||
### 结构
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"dns": {
|
|
||||||
"servers": [],
|
|
||||||
"rules": [],
|
|
||||||
"final": "",
|
|
||||||
"strategy": "",
|
|
||||||
"disable_cache": false,
|
|
||||||
"disable_expire": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### 字段
|
|
||||||
|
|
||||||
| 键 | 格式 |
|
|
||||||
|----------|------------------------|
|
|
||||||
| `server` | 一组 [DNS 服务器](./server) |
|
|
||||||
| `rules` | 一组 [DNS 规则](./rule) |
|
|
||||||
|
|
||||||
#### final
|
|
||||||
|
|
||||||
默认 DNS 服务器的标签。
|
|
||||||
|
|
||||||
默认使用第一个服务器。
|
|
||||||
|
|
||||||
#### strategy
|
|
||||||
|
|
||||||
默认解析域名策略。
|
|
||||||
|
|
||||||
可选值: `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。
|
|
||||||
|
|
||||||
如果设置了 `server.strategy`,则不生效。
|
|
||||||
|
|
||||||
#### disable_cache
|
|
||||||
|
|
||||||
禁用 DNS 缓存。
|
|
||||||
|
|
||||||
#### disable_expire
|
|
||||||
|
|
||||||
禁用 DNS 缓存过期。
|
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
"inbound": [
|
"inbound": [
|
||||||
"mixed-in"
|
"mixed-in"
|
||||||
],
|
],
|
||||||
"ip_version": 6,
|
|
||||||
"network": "tcp",
|
"network": "tcp",
|
||||||
"auth_user": [
|
"auth_user": [
|
||||||
"usera",
|
"usera",
|
||||||
@@ -38,8 +37,7 @@
|
|||||||
"private"
|
"private"
|
||||||
],
|
],
|
||||||
"source_ip_cidr": [
|
"source_ip_cidr": [
|
||||||
"10.0.0.0/24",
|
"10.0.0.0/24"
|
||||||
"192.168.0.1"
|
|
||||||
],
|
],
|
||||||
"source_port": [
|
"source_port": [
|
||||||
12345
|
12345
|
||||||
@@ -61,9 +59,6 @@
|
|||||||
"process_name": [
|
"process_name": [
|
||||||
"curl"
|
"curl"
|
||||||
],
|
],
|
||||||
"process_path": [
|
|
||||||
"/usr/bin/curl"
|
|
||||||
],
|
|
||||||
"package_name": [
|
"package_name": [
|
||||||
"com.termux"
|
"com.termux"
|
||||||
],
|
],
|
||||||
@@ -73,7 +68,6 @@
|
|||||||
"user_id": [
|
"user_id": [
|
||||||
1000
|
1000
|
||||||
],
|
],
|
||||||
"clash_mode": "direct",
|
|
||||||
"invert": false,
|
"invert": false,
|
||||||
"outbound": [
|
"outbound": [
|
||||||
"direct"
|
"direct"
|
||||||
@@ -104,26 +98,18 @@
|
|||||||
|
|
||||||
The default rule uses the following matching logic:
|
The default rule uses the following matching logic:
|
||||||
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite`) &&
|
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite`) &&
|
||||||
(`port` || `port_range`) &&
|
|
||||||
(`source_geoip` || `source_ip_cidr`) &&
|
(`source_geoip` || `source_ip_cidr`) &&
|
||||||
(`source_port` || `source_port_range`) &&
|
`other fields`
|
||||||
`other fields`
|
|
||||||
|
|
||||||
#### inbound
|
#### inbound
|
||||||
|
|
||||||
Tags of [Inbound](/configuration/inbound).
|
Tags of [inbound](../inbound).
|
||||||
|
|
||||||
#### ip_version
|
|
||||||
|
|
||||||
4 (A DNS query) or 6 (AAAA DNS query).
|
|
||||||
|
|
||||||
Not limited if empty.
|
|
||||||
|
|
||||||
#### network
|
#### network
|
||||||
|
|
||||||
`tcp` or `udp`.
|
`tcp` or `udp`.
|
||||||
|
|
||||||
#### auth_user
|
#### user
|
||||||
|
|
||||||
Username, see each inbound for details.
|
Username, see each inbound for details.
|
||||||
|
|
||||||
@@ -183,14 +169,6 @@ Match port range.
|
|||||||
|
|
||||||
Match process name.
|
Match process name.
|
||||||
|
|
||||||
#### process_path
|
|
||||||
|
|
||||||
!!! error ""
|
|
||||||
|
|
||||||
Only supported on Linux, Windows, and macOS.
|
|
||||||
|
|
||||||
Match process path.
|
|
||||||
|
|
||||||
#### package_name
|
#### package_name
|
||||||
|
|
||||||
Match android package name.
|
Match android package name.
|
||||||
@@ -211,10 +189,6 @@ Match user name.
|
|||||||
|
|
||||||
Match user id.
|
Match user id.
|
||||||
|
|
||||||
#### clash_mode
|
|
||||||
|
|
||||||
Match Clash mode.
|
|
||||||
|
|
||||||
#### invert
|
#### invert
|
||||||
|
|
||||||
Invert match result.
|
Invert match result.
|
||||||
|
|||||||
@@ -1,261 +0,0 @@
|
|||||||
### 结构
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"dns": {
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"inbound": [
|
|
||||||
"mixed-in"
|
|
||||||
],
|
|
||||||
"ip_version": 6,
|
|
||||||
"network": "tcp",
|
|
||||||
"auth_user": [
|
|
||||||
"usera",
|
|
||||||
"userb"
|
|
||||||
],
|
|
||||||
"protocol": [
|
|
||||||
"tls",
|
|
||||||
"http",
|
|
||||||
"quic"
|
|
||||||
],
|
|
||||||
"domain": [
|
|
||||||
"test.com"
|
|
||||||
],
|
|
||||||
"domain_suffix": [
|
|
||||||
".cn"
|
|
||||||
],
|
|
||||||
"domain_keyword": [
|
|
||||||
"test"
|
|
||||||
],
|
|
||||||
"domain_regex": [
|
|
||||||
"^stun\\..+"
|
|
||||||
],
|
|
||||||
"geosite": [
|
|
||||||
"cn"
|
|
||||||
],
|
|
||||||
"source_geoip": [
|
|
||||||
"private"
|
|
||||||
],
|
|
||||||
"source_ip_cidr": [
|
|
||||||
"10.0.0.0/24"
|
|
||||||
],
|
|
||||||
"source_port": [
|
|
||||||
12345
|
|
||||||
],
|
|
||||||
"source_port_range": [
|
|
||||||
"1000:2000",
|
|
||||||
":3000",
|
|
||||||
"4000:"
|
|
||||||
],
|
|
||||||
"port": [
|
|
||||||
80,
|
|
||||||
443
|
|
||||||
],
|
|
||||||
"port_range": [
|
|
||||||
"1000:2000",
|
|
||||||
":3000",
|
|
||||||
"4000:"
|
|
||||||
],
|
|
||||||
"process_name": [
|
|
||||||
"curl"
|
|
||||||
],
|
|
||||||
"process_path": [
|
|
||||||
"/usr/bin/curl"
|
|
||||||
],
|
|
||||||
"package_name": [
|
|
||||||
"com.termux"
|
|
||||||
],
|
|
||||||
"user": [
|
|
||||||
"sekai"
|
|
||||||
],
|
|
||||||
"user_id": [
|
|
||||||
1000
|
|
||||||
],
|
|
||||||
"clash_mode": "direct",
|
|
||||||
"invert": false,
|
|
||||||
"outbound": [
|
|
||||||
"direct"
|
|
||||||
],
|
|
||||||
"server": "local",
|
|
||||||
"disable_cache": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "logical",
|
|
||||||
"mode": "and",
|
|
||||||
"rules": [],
|
|
||||||
"server": "local",
|
|
||||||
"disable_cache": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! note ""
|
|
||||||
|
|
||||||
当内容只有一项时,可以忽略 JSON 数组 [] 标签
|
|
||||||
|
|
||||||
### 默认字段
|
|
||||||
|
|
||||||
!!! note ""
|
|
||||||
|
|
||||||
默认规则使用以下匹配逻辑:
|
|
||||||
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite`) &&
|
|
||||||
(`port` || `port_range`) &&
|
|
||||||
(`source_geoip` || `source_ip_cidr`) &&
|
|
||||||
(`source_port` || `source_port_range`) &&
|
|
||||||
`other fields`
|
|
||||||
|
|
||||||
#### inbound
|
|
||||||
|
|
||||||
[入站](/zh/configuration/inbound) 标签.
|
|
||||||
|
|
||||||
#### ip_version
|
|
||||||
|
|
||||||
4 (A DNS 查询) 或 6 (AAAA DNS 查询)。
|
|
||||||
|
|
||||||
默认不限制。
|
|
||||||
|
|
||||||
#### network
|
|
||||||
|
|
||||||
`tcp` 或 `udp`。
|
|
||||||
|
|
||||||
#### auth_user
|
|
||||||
|
|
||||||
认证用户名,参阅入站设置。
|
|
||||||
|
|
||||||
#### protocol
|
|
||||||
|
|
||||||
探测到的协议, 参阅 [协议探测](/zh/configuration/route/sniff/)。
|
|
||||||
|
|
||||||
#### domain
|
|
||||||
|
|
||||||
匹配完整域名。
|
|
||||||
|
|
||||||
#### domain_suffix
|
|
||||||
|
|
||||||
匹配域名后缀。
|
|
||||||
|
|
||||||
#### domain_keyword
|
|
||||||
|
|
||||||
匹配域名关键字。
|
|
||||||
|
|
||||||
#### domain_regex
|
|
||||||
|
|
||||||
匹配域名正则表达式。
|
|
||||||
|
|
||||||
#### geosite
|
|
||||||
|
|
||||||
匹配 GeoSite。
|
|
||||||
|
|
||||||
#### source_geoip
|
|
||||||
|
|
||||||
匹配源 GeoIP。
|
|
||||||
|
|
||||||
#### source_ip_cidr
|
|
||||||
|
|
||||||
匹配源 IP CIDR。
|
|
||||||
|
|
||||||
#### source_port
|
|
||||||
|
|
||||||
匹配源端口。
|
|
||||||
|
|
||||||
#### source_port_range
|
|
||||||
|
|
||||||
匹配源端口范围。
|
|
||||||
|
|
||||||
#### port
|
|
||||||
|
|
||||||
匹配端口。
|
|
||||||
|
|
||||||
#### port_range
|
|
||||||
|
|
||||||
匹配端口范围。
|
|
||||||
|
|
||||||
#### process_name
|
|
||||||
|
|
||||||
!!! error ""
|
|
||||||
|
|
||||||
仅支持 Linux、Windows 和 macOS.
|
|
||||||
|
|
||||||
匹配进程名称。
|
|
||||||
|
|
||||||
#### process_path
|
|
||||||
|
|
||||||
!!! error ""
|
|
||||||
|
|
||||||
仅支持 Linux、Windows 和 macOS.
|
|
||||||
|
|
||||||
匹配进程路径。
|
|
||||||
|
|
||||||
#### package_name
|
|
||||||
|
|
||||||
匹配 Android 应用包名。
|
|
||||||
|
|
||||||
#### user
|
|
||||||
|
|
||||||
!!! error ""
|
|
||||||
|
|
||||||
仅支持 Linux。
|
|
||||||
|
|
||||||
匹配用户名。
|
|
||||||
|
|
||||||
#### user_id
|
|
||||||
|
|
||||||
!!! error ""
|
|
||||||
|
|
||||||
仅支持 Linux。
|
|
||||||
|
|
||||||
匹配用户 ID。
|
|
||||||
|
|
||||||
#### clash_mode
|
|
||||||
|
|
||||||
匹配 Clash 模式。
|
|
||||||
|
|
||||||
#### invert
|
|
||||||
|
|
||||||
反选匹配结果。
|
|
||||||
|
|
||||||
#### outbound
|
|
||||||
|
|
||||||
匹配出站。
|
|
||||||
|
|
||||||
#### server
|
|
||||||
|
|
||||||
==必填==
|
|
||||||
|
|
||||||
目标 DNS 服务器的标签。
|
|
||||||
|
|
||||||
#### disable_cache
|
|
||||||
|
|
||||||
在此查询中禁用缓存。
|
|
||||||
|
|
||||||
### 逻辑字段
|
|
||||||
|
|
||||||
#### type
|
|
||||||
|
|
||||||
`logical`
|
|
||||||
|
|
||||||
#### mode
|
|
||||||
|
|
||||||
`and` 或 `or`
|
|
||||||
|
|
||||||
#### rules
|
|
||||||
|
|
||||||
包括的默认规则。
|
|
||||||
|
|
||||||
#### invert
|
|
||||||
|
|
||||||
反选匹配结果。
|
|
||||||
|
|
||||||
#### server
|
|
||||||
|
|
||||||
==必填==
|
|
||||||
|
|
||||||
目标 DNS 服务器的标签。
|
|
||||||
|
|
||||||
#### disable_cache
|
|
||||||
|
|
||||||
在此查询中禁用缓存。
|
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
"address": "tls://dns.google",
|
"address": "tls://dns.google",
|
||||||
"address_resolver": "local",
|
"address_resolver": "local",
|
||||||
"address_strategy": "prefer_ipv4",
|
"address_strategy": "prefer_ipv4",
|
||||||
"strategy": "ipv4_only",
|
|
||||||
"detour": "direct"
|
"detour": "direct"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -43,11 +42,11 @@ The address of the dns server.
|
|||||||
|
|
||||||
!!! warning ""
|
!!! warning ""
|
||||||
|
|
||||||
To ensure that system DNS is in effect, rather than Go's built-in default resolver, enable CGO at compile time.
|
To ensure that system DNS is in effect, rather than go's built-in default resolver, enable CGO at compile time.
|
||||||
|
|
||||||
!!! warning ""
|
!!! warning ""
|
||||||
|
|
||||||
QUIC and HTTP3 transport is not included by default, see [Installation](/#installation).
|
QUIC and HTTP3 transport is not included by default, see [Installation](/#Installation).
|
||||||
|
|
||||||
!!! info ""
|
!!! info ""
|
||||||
|
|
||||||
@@ -60,7 +59,6 @@ The address of the dns server.
|
|||||||
| `server_failure` | `Server failure` |
|
| `server_failure` | `Server failure` |
|
||||||
| `name_error` | `Non-existent domain` |
|
| `name_error` | `Non-existent domain` |
|
||||||
| `not_implemented` | `Not implemented` |
|
| `not_implemented` | `Not implemented` |
|
||||||
| `refused` | `Query refused` |
|
|
||||||
|
|
||||||
#### address_resolver
|
#### address_resolver
|
||||||
|
|
||||||
@@ -76,14 +74,6 @@ One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
|||||||
|
|
||||||
`dns.strategy` will be used if empty.
|
`dns.strategy` will be used if empty.
|
||||||
|
|
||||||
#### strategy
|
|
||||||
|
|
||||||
Default domain strategy for resolving the domain names.
|
|
||||||
|
|
||||||
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
|
||||||
|
|
||||||
Take no effect if override by other settings.
|
|
||||||
|
|
||||||
#### detour
|
#### detour
|
||||||
|
|
||||||
Tag of an outbound for connecting to the dns server.
|
Tag of an outbound for connecting to the dns server.
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
### 结构
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"dns": {
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
"tag": "google",
|
|
||||||
"address": "tls://dns.google",
|
|
||||||
"address_resolver": "local",
|
|
||||||
"address_strategy": "prefer_ipv4",
|
|
||||||
"strategy": "ipv4_only",
|
|
||||||
"detour": "direct"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### 字段
|
|
||||||
|
|
||||||
#### tag
|
|
||||||
|
|
||||||
DNS 服务器的标签。
|
|
||||||
|
|
||||||
#### address
|
|
||||||
|
|
||||||
==必填==
|
|
||||||
|
|
||||||
DNS 服务器的地址。
|
|
||||||
|
|
||||||
| 协议 | 格式 |
|
|
||||||
|----------|-----------------------------|
|
|
||||||
| `System` | `local` |
|
|
||||||
| `TCP` | `tcp://1.0.0.1` |
|
|
||||||
| `UDP` | `8.8.8.8` `udp://8.8.4.4` |
|
|
||||||
| `TLS` | `tls://dns.google` |
|
|
||||||
| `HTTPS` | `https://1.1.1.1/dns-query` |
|
|
||||||
| `QUIC` | `quic://dns.adguard.com` |
|
|
||||||
| `HTTP3` | `h3://8.8.8.8/dns-query` |
|
|
||||||
| `RCode` | `rcode://refused` |
|
|
||||||
|
|
||||||
!!! warning ""
|
|
||||||
|
|
||||||
为了确保系统 DNS 生效,而不是 Go 的内置默认解析器,请在编译时启用 CGO。
|
|
||||||
|
|
||||||
!!! warning ""
|
|
||||||
|
|
||||||
默认安装不包含 QUIC 和 HTTP3 传输层,请参阅 [安装](/zh/#_2)。
|
|
||||||
|
|
||||||
!!! info ""
|
|
||||||
|
|
||||||
RCode 传输层传输层常用于屏蔽请求. 与 DNS 规则和 `disable_cache` 规则选项一起使用。
|
|
||||||
|
|
||||||
| RCode | 描述 |
|
|
||||||
|-------------------|----------|
|
|
||||||
| `success` | `无错误` |
|
|
||||||
| `format_error` | `请求格式错误` |
|
|
||||||
| `server_failure` | `服务器出错` |
|
|
||||||
| `name_error` | `域名不存在` |
|
|
||||||
| `not_implemented` | `功能未实现` |
|
|
||||||
| `refused` | `请求被拒绝` |
|
|
||||||
|
|
||||||
#### address_resolver
|
|
||||||
|
|
||||||
==如果服务器地址包括域名则必须==
|
|
||||||
|
|
||||||
用于解析本 DNS 服务器的域名的另一个 DNS 服务器的标签。
|
|
||||||
|
|
||||||
#### address_strategy
|
|
||||||
|
|
||||||
用于解析本 DNS 服务器的域名的策略。
|
|
||||||
|
|
||||||
可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。
|
|
||||||
|
|
||||||
默认使用 `dns.strategy`。
|
|
||||||
|
|
||||||
#### strategy
|
|
||||||
|
|
||||||
默认解析策略。
|
|
||||||
|
|
||||||
可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。
|
|
||||||
|
|
||||||
如果被其他设置覆盖则不生效。
|
|
||||||
|
|
||||||
#### detour
|
|
||||||
|
|
||||||
用于连接到 DNS 服务器的出站的标签。
|
|
||||||
|
|
||||||
如果为空,将使用默认出站。
|
|
||||||
39
docs/configuration/experimental.md
Normal file
39
docs/configuration/experimental.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
### Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"experimental": {
|
||||||
|
"clash_api": {
|
||||||
|
"external_controller": "127.0.0.1:9090",
|
||||||
|
"external_ui": "folder",
|
||||||
|
"secret": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clash API Fields
|
||||||
|
|
||||||
|
!!! error ""
|
||||||
|
|
||||||
|
Clash API is not included by default, see [Installation](/#Installation).
|
||||||
|
|
||||||
|
!!! note ""
|
||||||
|
|
||||||
|
Traffic statistics and connection management will disable TCP splice in linux and reduce performance, use at your own risk.
|
||||||
|
|
||||||
|
#### external_controller
|
||||||
|
|
||||||
|
RESTful web API listening address. Disabled if empty.
|
||||||
|
|
||||||
|
#### external_ui
|
||||||
|
|
||||||
|
A relative path to the configuration directory or an absolute path to a
|
||||||
|
directory in which you put some static web resource. Clash core will then
|
||||||
|
serve it at `http://{{external-controller}}/ui`.
|
||||||
|
|
||||||
|
#### secret
|
||||||
|
|
||||||
|
Secret for the RESTful API (optional)
|
||||||
|
Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}`
|
||||||
|
ALWAYS set a secret if RESTful API is listening on 0.0.0.0
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
# Experimental
|
|
||||||
|
|
||||||
### Structure
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"experimental": {
|
|
||||||
"clash_api": {
|
|
||||||
"external_controller": "127.0.0.1:9090",
|
|
||||||
"external_ui": "folder",
|
|
||||||
"secret": "",
|
|
||||||
"direct_io": false,
|
|
||||||
"default_mode": "rule",
|
|
||||||
"store_selected": false,
|
|
||||||
"cache_file": "cache.db"
|
|
||||||
},
|
|
||||||
"v2ray_api": {
|
|
||||||
"listen": "127.0.0.1:8080",
|
|
||||||
"stats": {
|
|
||||||
"enabled": true,
|
|
||||||
"direct_io": false,
|
|
||||||
"inbounds": [
|
|
||||||
"socks-in"
|
|
||||||
],
|
|
||||||
"outbounds": [
|
|
||||||
"proxy",
|
|
||||||
"direct"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! note ""
|
|
||||||
|
|
||||||
Traffic statistics and connection management can degrade performance.
|
|
||||||
|
|
||||||
### Clash API Fields
|
|
||||||
|
|
||||||
!!! error ""
|
|
||||||
|
|
||||||
Clash API is not included by default, see [Installation](/#installation).
|
|
||||||
|
|
||||||
#### external_controller
|
|
||||||
|
|
||||||
RESTful web API listening address. Clash API will be disabled if empty.
|
|
||||||
|
|
||||||
#### external_ui
|
|
||||||
|
|
||||||
A relative path to the configuration directory or an absolute path to a
|
|
||||||
directory in which you put some static web resource. sing-box will then
|
|
||||||
serve it at `http://{{external-controller}}/ui`.
|
|
||||||
|
|
||||||
#### secret
|
|
||||||
|
|
||||||
Secret for the RESTful API (optional)
|
|
||||||
Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}`
|
|
||||||
ALWAYS set a secret if RESTful API is listening on 0.0.0.0
|
|
||||||
|
|
||||||
#### direct_io
|
|
||||||
|
|
||||||
Allows lossless relays like splice without real-time traffic reporting.
|
|
||||||
|
|
||||||
#### default_mode
|
|
||||||
|
|
||||||
Default mode in clash, `rule` will be used if empty.
|
|
||||||
|
|
||||||
This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item.
|
|
||||||
|
|
||||||
#### store_selected
|
|
||||||
|
|
||||||
!!! note ""
|
|
||||||
|
|
||||||
The tag must be set for target outbounds.
|
|
||||||
|
|
||||||
Store selected outbound for the `Selector` outbound in cache file.
|
|
||||||
|
|
||||||
#### cache_file
|
|
||||||
|
|
||||||
Cache file path, `cache.db` will be used if empty.
|
|
||||||
|
|
||||||
### V2Ray API Fields
|
|
||||||
|
|
||||||
!!! error ""
|
|
||||||
|
|
||||||
V2Ray API is not included by default, see [Installation](/#installation).
|
|
||||||
|
|
||||||
#### listen
|
|
||||||
|
|
||||||
gRPC API listening address. V2Ray API will be disabled if empty.
|
|
||||||
|
|
||||||
#### stats
|
|
||||||
|
|
||||||
Traffic statistics service settings.
|
|
||||||
|
|
||||||
#### stats.enabled
|
|
||||||
|
|
||||||
Enable statistics service.
|
|
||||||
|
|
||||||
#### stats.direct_io
|
|
||||||
|
|
||||||
Allows lossless relays like splice without real-time traffic reporting.
|
|
||||||
|
|
||||||
#### stats.inbounds
|
|
||||||
|
|
||||||
Inbound list to count traffic.
|
|
||||||
|
|
||||||
#### stats.outbounds
|
|
||||||
|
|
||||||
Outbound list to count traffic.
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
# 实验性
|
|
||||||
|
|
||||||
### 结构
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"experimental": {
|
|
||||||
"clash_api": {
|
|
||||||
"external_controller": "127.0.0.1:9090",
|
|
||||||
"external_ui": "folder",
|
|
||||||
"secret": "",
|
|
||||||
"direct_io": false,
|
|
||||||
"default_mode": "rule",
|
|
||||||
"store_selected": false,
|
|
||||||
"cache_file": "cache.db"
|
|
||||||
},
|
|
||||||
"v2ray_api": {
|
|
||||||
"listen": "127.0.0.1:8080",
|
|
||||||
"stats": {
|
|
||||||
"enabled": true,
|
|
||||||
"direct_io": false,
|
|
||||||
"inbounds": [
|
|
||||||
"socks-in"
|
|
||||||
],
|
|
||||||
"outbounds": [
|
|
||||||
"proxy",
|
|
||||||
"direct"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! note ""
|
|
||||||
|
|
||||||
流量统计和连接管理会降低性能。
|
|
||||||
|
|
||||||
### Clash API 字段
|
|
||||||
|
|
||||||
!!! error ""
|
|
||||||
|
|
||||||
默认安装不包含 Clash API,参阅 [安装](/zh/#_2)。
|
|
||||||
|
|
||||||
#### external_controller
|
|
||||||
|
|
||||||
RESTful web API 监听地址。如果为空,则禁用 Clash API。
|
|
||||||
|
|
||||||
#### external_ui
|
|
||||||
|
|
||||||
到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。
|
|
||||||
|
|
||||||
#### secret
|
|
||||||
|
|
||||||
RESTful API 的密钥(可选)
|
|
||||||
通过指定 HTTP 标头 `Authorization: Bearer ${secret}` 进行身份验证
|
|
||||||
如果 RESTful API 正在监听 0.0.0.0,请始终设置一个密钥。
|
|
||||||
|
|
||||||
#### direct_io
|
|
||||||
|
|
||||||
允许像 splice 这样的没有实时流量报告的无损中继。
|
|
||||||
|
|
||||||
#### default_mode
|
|
||||||
|
|
||||||
Clash 中的默认模式,默认使用 `rule`。
|
|
||||||
|
|
||||||
此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。
|
|
||||||
|
|
||||||
#### store_selected
|
|
||||||
|
|
||||||
!!! note ""
|
|
||||||
|
|
||||||
必须为目标出站设置标签。
|
|
||||||
|
|
||||||
将 `Selector` 中出站的选定的目标出站存储在缓存文件中。
|
|
||||||
|
|
||||||
#### cache_file
|
|
||||||
|
|
||||||
缓存文件路径,默认使用`cache.db`。
|
|
||||||
|
|
||||||
### V2Ray API 字段
|
|
||||||
|
|
||||||
!!! error ""
|
|
||||||
|
|
||||||
默认安装不包含 V2Ray API,参阅 [安装](/zh/#_2)。
|
|
||||||
|
|
||||||
#### listen
|
|
||||||
|
|
||||||
gRPC API 监听地址。如果为空,则禁用 V2Ray API。
|
|
||||||
|
|
||||||
#### stats
|
|
||||||
|
|
||||||
流量统计服务设置。
|
|
||||||
|
|
||||||
#### stats.enabled
|
|
||||||
|
|
||||||
启用统计服务。
|
|
||||||
|
|
||||||
#### stats.direct_io
|
|
||||||
|
|
||||||
允许像 splice 这样的没有实时流量报告的无损中继。
|
|
||||||
|
|
||||||
#### stats.inbounds
|
|
||||||
|
|
||||||
统计流量的入站列表。
|
|
||||||
|
|
||||||
#### stats.outbounds
|
|
||||||
|
|
||||||
统计流量的出站列表。
|
|
||||||
@@ -4,22 +4,70 @@
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "direct",
|
"inbounds": [
|
||||||
"tag": "direct-in",
|
{
|
||||||
|
"type": "direct",
|
||||||
... // Listen Fields
|
"tag": "direct-in",
|
||||||
|
|
||||||
"network": "udp",
|
"listen": "::",
|
||||||
"override_address": "1.0.0.1",
|
"listen_port": 5353,
|
||||||
"override_port": 53
|
"tcp_fast_open": false,
|
||||||
|
"sniff": false,
|
||||||
|
"sniff_override_destination": false,
|
||||||
|
"domain_strategy": "prefer_ipv6",
|
||||||
|
"udp_timeout": 300,
|
||||||
|
|
||||||
|
"network": "udp",
|
||||||
|
"override_address": "1.0.0.1",
|
||||||
|
"override_port": 53
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Listen Fields
|
### Listen Fields
|
||||||
|
|
||||||
See [Listen Fields](/configuration/shared/listen) for details.
|
#### listen
|
||||||
|
|
||||||
### Fields
|
==Required==
|
||||||
|
|
||||||
|
Listen address.
|
||||||
|
|
||||||
|
#### listen_port
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
Listen port.
|
||||||
|
|
||||||
|
#### tcp_fast_open
|
||||||
|
|
||||||
|
Enable tcp fast open for listener.
|
||||||
|
|
||||||
|
#### sniff
|
||||||
|
|
||||||
|
Enable sniffing.
|
||||||
|
|
||||||
|
See [Sniff](/configuration/route/sniff/) for details.
|
||||||
|
|
||||||
|
#### sniff_override_destination
|
||||||
|
|
||||||
|
Override the connection destination address with the sniffed domain.
|
||||||
|
|
||||||
|
If the domain name is invalid (like tor), this will not work.
|
||||||
|
|
||||||
|
#### domain_strategy
|
||||||
|
|
||||||
|
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
||||||
|
|
||||||
|
If set, the requested domain name will be resolved to IP before routing.
|
||||||
|
|
||||||
|
If `sniff_override_destination` is in effect, its value will be taken as a fallback.
|
||||||
|
|
||||||
|
#### udp_timeout
|
||||||
|
|
||||||
|
UDP NAT expiration time in seconds, default is 300 (5 minutes).
|
||||||
|
|
||||||
|
### Direct Fields
|
||||||
|
|
||||||
#### network
|
#### network
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user