forked from mirror/serenity
Init commit
This commit is contained in:
commit
1e27e80af9
5
.github/update_dependencies.sh
vendored
Executable file
5
.github/update_dependencies.sh
vendored
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
PROJECTS=$(dirname "$0")/../..
|
||||||
|
go get -x github.com/sagernet/$1@$(git -C $PROJECTS/$1 rev-parse HEAD)
|
||||||
|
go mod tidy
|
51
.github/workflows/debug.yml
vendored
Normal file
51
.github/workflows/debug.yml
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
name: Debug build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
- '.github/**'
|
||||||
|
- '!.github/workflows/debug.yml'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Debug build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Get latest go version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
echo go_version=$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g') >> $GITHUB_OUTPUT
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ steps.version.outputs.go_version }}
|
||||||
|
- name: Cache go module
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: go-${{ hashFiles('**/go.sum') }}
|
||||||
|
- name: Add cache to Go proxy
|
||||||
|
run: |
|
||||||
|
version=`git rev-parse HEAD`
|
||||||
|
mkdir build
|
||||||
|
pushd build
|
||||||
|
go mod init build
|
||||||
|
go get -v github.com/sagernet/serenity@$version
|
||||||
|
popd
|
||||||
|
continue-on-error: true
|
||||||
|
- name: Run Test
|
||||||
|
run: |
|
||||||
|
go test -v ./...
|
47
.github/workflows/docker.yml
vendored
Normal file
47
.github/workflows/docker.yml
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
name: Build Docker Images
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: "The tag version you want to build"
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||||
|
- name: Setup Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Setup QEMU for Docker Buildx
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Docker metadata
|
||||||
|
id: metadata
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ghcr.io/sagernet/serenity
|
||||||
|
- name: Get tag to build
|
||||||
|
id: tag
|
||||||
|
run: |
|
||||||
|
echo "latest=ghcr.io/sagernet/serenity:latest" >> $GITHUB_OUTPUT
|
||||||
|
if [[ -z "${{ github.event.inputs.tag }}" ]]; then
|
||||||
|
echo "versioned=ghcr.io/sagernet/serenity:${{ github.ref_name }}" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "versioned=ghcr.io/sagernet/serenity:${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
- name: Build and release Docker images
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
platforms: linux/386,linux/amd64,linux/arm64,linux/s390x
|
||||||
|
target: dist
|
||||||
|
build-args: |
|
||||||
|
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
||||||
|
tags: |
|
||||||
|
${{ steps.tag.outputs.latest }}
|
||||||
|
${{ steps.tag.outputs.versioned }}
|
||||||
|
push: true
|
39
.github/workflows/lint.yml
vendored
Normal file
39
.github/workflows/lint.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
name: Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
- '.github/**'
|
||||||
|
- '!.github/workflows/lint.yml'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Get latest go version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
echo go_version=$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g') >> $GITHUB_OUTPUT
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ steps.version.outputs.go_version }}
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v3
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
args: --timeout=30m
|
||||||
|
install-mode: binary
|
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/.idea/
|
||||||
|
/vendor/
|
||||||
|
/*.json
|
||||||
|
/*.db
|
||||||
|
/site/
|
||||||
|
/bin/
|
||||||
|
/dist/
|
||||||
|
/build/
|
||||||
|
.DS_Store
|
||||||
|
/serenity
|
||||||
|
/serenity.exe
|
18
.golangci.yml
Normal file
18
.golangci.yml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
linters:
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
- gofumpt
|
||||||
|
- govet
|
||||||
|
- gci
|
||||||
|
- staticcheck
|
||||||
|
- paralleltest
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
gci:
|
||||||
|
custom-order: true
|
||||||
|
sections:
|
||||||
|
- standard
|
||||||
|
- prefix(github.com/sagernet/)
|
||||||
|
- default
|
||||||
|
staticcheck:
|
||||||
|
go: '1.20'
|
77
.goreleaser.yaml
Normal file
77
.goreleaser.yaml
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
project_name: serenity
|
||||||
|
builds:
|
||||||
|
- id: main
|
||||||
|
main: ./cmd/serenity
|
||||||
|
flags:
|
||||||
|
- -v
|
||||||
|
- -trimpath
|
||||||
|
asmflags:
|
||||||
|
- all=-trimpath={{.Env.GOPATH}}
|
||||||
|
gcflags:
|
||||||
|
- all=-trimpath={{.Env.GOPATH}}
|
||||||
|
ldflags:
|
||||||
|
- -X github.com/sagernet/serenity/constant.Version={{ .Version }} -s -w -buildid=
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
targets:
|
||||||
|
- linux_386
|
||||||
|
- linux_amd64_v1
|
||||||
|
- linux_arm64
|
||||||
|
- linux_arm_7
|
||||||
|
- linux_s390x
|
||||||
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
snapshot:
|
||||||
|
name_template: "{{ .Version }}.{{ .ShortCommit }}"
|
||||||
|
archives:
|
||||||
|
- id: archive
|
||||||
|
builds:
|
||||||
|
- main
|
||||||
|
- android
|
||||||
|
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: serenity
|
||||||
|
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://serenity.sagernet.org/
|
||||||
|
maintainer: nekohasekai <contact-git@sekai.icu>
|
||||||
|
description: The configuration generator for sing-box.
|
||||||
|
license: GPLv3 or later
|
||||||
|
formats:
|
||||||
|
- deb
|
||||||
|
- rpm
|
||||||
|
- archlinux
|
||||||
|
priority: extra
|
||||||
|
contents:
|
||||||
|
- src: release/config/config.json
|
||||||
|
dst: /etc/serenity/config.json
|
||||||
|
type: config
|
||||||
|
- src: release/config/serenity.service
|
||||||
|
dst: /etc/systemd/system/serenity.service
|
||||||
|
- src: release/config/serenity@.service
|
||||||
|
dst: /etc/systemd/system/serenity@.service
|
||||||
|
- src: LICENSE
|
||||||
|
dst: /usr/share/licenses/serenity/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: serenity
|
||||||
|
name_template: '{{ if .IsSnapshot }}{{ nightly }}{{ else }}{{ .Version }}{{ end }}'
|
||||||
|
draft: true
|
||||||
|
mode: replace
|
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS builder
|
||||||
|
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||||
|
COPY . /go/src/github.com/sagernet/serenity
|
||||||
|
WORKDIR /go/src/github.com/sagernet/serenity
|
||||||
|
ARG TARGETOS TARGETARCH
|
||||||
|
ARG GOPROXY=""
|
||||||
|
ENV GOPROXY ${GOPROXY}
|
||||||
|
ENV CGO_ENABLED=0
|
||||||
|
ENV GOOS=$TARGETOS
|
||||||
|
ENV GOARCH=$TARGETARCH
|
||||||
|
RUN set -ex \
|
||||||
|
&& apk add git build-base \
|
||||||
|
&& export COMMIT=$(git rev-parse --short HEAD) \
|
||||||
|
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
||||||
|
&& go build -v -trimpath \
|
||||||
|
-o /go/bin/serenity \
|
||||||
|
-ldflags "-X \"github.com/sagernet/serenity/cmd/serenity.Version=$VERSION\" -s -w -buildid=" \
|
||||||
|
./cmd/serenity
|
||||||
|
FROM --platform=$TARGETPLATFORM 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/serenity /usr/local/bin/serenity
|
||||||
|
ENTRYPOINT ["serenity"]
|
17
LICENSE
Normal file
17
LICENSE
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
Copyright (C) 2022 by nekohasekai <contact-sagernet@sekai.icu>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
In addition, no derivative work may use the name or imply association
|
||||||
|
with this application without prior consent.
|
65
Makefile
Normal file
65
Makefile
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
NAME = serenity
|
||||||
|
COMMIT = $(shell git rev-parse --short HEAD)
|
||||||
|
TAG = $(shell git describe --tags --always)
|
||||||
|
VERSION = $(TAG:v%=%)
|
||||||
|
|
||||||
|
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/serenity/constant.Version=$(VERSION)' -s -w -buildid="
|
||||||
|
MAIN_PARAMS = $(PARAMS)
|
||||||
|
MAIN = ./cmd/serenity
|
||||||
|
PREFIX ?= $(shell go env GOPATH)
|
||||||
|
|
||||||
|
.PHONY: release docs
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build $(MAIN_PARAMS) $(MAIN)
|
||||||
|
|
||||||
|
install:
|
||||||
|
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
@gofumpt -l -w .
|
||||||
|
@gofmt -s -w .
|
||||||
|
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" .
|
||||||
|
|
||||||
|
fmt_install:
|
||||||
|
go install -v mvdan.cc/gofumpt@latest
|
||||||
|
go install -v github.com/daixiang0/gci@latest
|
||||||
|
|
||||||
|
lint:
|
||||||
|
GOOS=linux golangci-lint run ./...
|
||||||
|
GOOS=android golangci-lint run ./...
|
||||||
|
GOOS=windows golangci-lint run ./...
|
||||||
|
GOOS=darwin golangci-lint run ./...
|
||||||
|
GOOS=freebsd golangci-lint run ./...
|
||||||
|
|
||||||
|
lint_install:
|
||||||
|
go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||||
|
|
||||||
|
release:
|
||||||
|
goreleaser release --clean --skip-publish || exit 1
|
||||||
|
mkdir dist/release
|
||||||
|
mv dist/*.tar.gz dist/*.deb dist/*.rpm dist/*.pkg.tar.zst dist/release
|
||||||
|
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release
|
||||||
|
rm -r dist/release
|
||||||
|
|
||||||
|
release_install:
|
||||||
|
go install -v github.com/goreleaser/goreleaser@latest
|
||||||
|
go install -v github.com/tcnksm/ghr@latest
|
||||||
|
|
||||||
|
docs:
|
||||||
|
mkdocs serve
|
||||||
|
|
||||||
|
publish_docs:
|
||||||
|
mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
|
||||||
|
|
||||||
|
docs_install:
|
||||||
|
pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf bin dist serenity
|
||||||
|
rm -f $(shell go env GOPATH)/serenity
|
||||||
|
|
||||||
|
update:
|
||||||
|
git fetch
|
||||||
|
git reset FETCH_HEAD --hard
|
||||||
|
git clean -fdx
|
26
README.md
Normal file
26
README.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# serenity
|
||||||
|
|
||||||
|
The configuration generator for sing-box.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
https://serenity.sagernet.org
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
```
|
||||||
|
Copyright (C) 2022 by nekohasekai <contact-sagernet@sekai.icu>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
```
|
40
cmd/serenity/cmd_check.go
Normal file
40
cmd/serenity/cmd_check.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sagernet/serenity/server"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandCheck = &cobra.Command{
|
||||||
|
Use: "check",
|
||||||
|
Short: "Check configuration",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := check()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
mainCommand.AddCommand(commandCheck)
|
||||||
|
}
|
||||||
|
|
||||||
|
func check() error {
|
||||||
|
options, err := readConfigAndMerge()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
instance, err := server.New(ctx, options)
|
||||||
|
if err == nil {
|
||||||
|
instance.Close()
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
return err
|
||||||
|
}
|
75
cmd/serenity/cmd_format.go
Normal file
75
cmd/serenity/cmd_format.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
"github.com/sagernet/sing/common/json/badjson"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandFormatFlagWrite bool
|
||||||
|
|
||||||
|
var commandFormat = &cobra.Command{
|
||||||
|
Use: "format",
|
||||||
|
Short: "Format configuration",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := format()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandFormat.Flags().BoolVarP(&commandFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout")
|
||||||
|
mainCommand.AddCommand(commandFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func format() error {
|
||||||
|
optionsList, err := readConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, optionsEntry := range optionsList {
|
||||||
|
optionsEntry.options, err = badjson.Omitempty(optionsEntry.options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
encoder := json.NewEncoder(buffer)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
err = encoder.Encode(optionsEntry.options)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "encode config")
|
||||||
|
}
|
||||||
|
outputPath, _ := filepath.Abs(optionsEntry.path)
|
||||||
|
if !commandFormatFlagWrite {
|
||||||
|
if len(optionsList) > 1 {
|
||||||
|
os.Stdout.WriteString(outputPath + "\n")
|
||||||
|
}
|
||||||
|
os.Stdout.WriteString(buffer.String() + "\n")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if bytes.Equal(optionsEntry.content, buffer.Bytes()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
output, err := os.Create(optionsEntry.path)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "open output")
|
||||||
|
}
|
||||||
|
_, err = output.Write(buffer.Bytes())
|
||||||
|
output.Close()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "write output")
|
||||||
|
}
|
||||||
|
os.Stderr.WriteString(outputPath + "\n")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
207
cmd/serenity/cmd_run.go
Normal file
207
cmd/serenity/cmd_run.go
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
runtimeDebug "runtime/debug"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/serenity/option"
|
||||||
|
"github.com/sagernet/serenity/server"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
boxOption "github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
"github.com/sagernet/sing/common/json/badjson"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandRun = &cobra.Command{
|
||||||
|
Use: "run",
|
||||||
|
Short: "Run service",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := run()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
mainCommand.AddCommand(commandRun)
|
||||||
|
}
|
||||||
|
|
||||||
|
type OptionsEntry struct {
|
||||||
|
content []byte
|
||||||
|
path string
|
||||||
|
options option.Options
|
||||||
|
}
|
||||||
|
|
||||||
|
func readConfigAt(path string) (*OptionsEntry, error) {
|
||||||
|
var (
|
||||||
|
configContent []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if path == "stdin" {
|
||||||
|
configContent, err = io.ReadAll(os.Stdin)
|
||||||
|
} else {
|
||||||
|
configContent, err = os.ReadFile(path)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "read config at ", path)
|
||||||
|
}
|
||||||
|
options, err := json.UnmarshalExtended[option.Options](configContent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "decode config at ", path)
|
||||||
|
}
|
||||||
|
return &OptionsEntry{
|
||||||
|
content: configContent,
|
||||||
|
path: path,
|
||||||
|
options: options,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readConfig() ([]*OptionsEntry, error) {
|
||||||
|
var optionsList []*OptionsEntry
|
||||||
|
for _, path := range configPaths {
|
||||||
|
optionsEntry, err := readConfigAt(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
optionsList = append(optionsList, optionsEntry)
|
||||||
|
}
|
||||||
|
for _, directory := range configDirectories {
|
||||||
|
entries, err := os.ReadDir(directory)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "read config directory at ", directory)
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !strings.HasSuffix(entry.Name(), ".json") || entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
optionsEntry, err := readConfigAt(filepath.Join(directory, entry.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
optionsList = append(optionsList, optionsEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(optionsList, func(i, j int) bool {
|
||||||
|
return optionsList[i].path < optionsList[j].path
|
||||||
|
})
|
||||||
|
return optionsList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readConfigAndMerge() (option.Options, error) {
|
||||||
|
optionsList, err := readConfig()
|
||||||
|
if err != nil {
|
||||||
|
return option.Options{}, err
|
||||||
|
}
|
||||||
|
if len(optionsList) == 1 {
|
||||||
|
return optionsList[0].options, nil
|
||||||
|
}
|
||||||
|
var mergedMessage json.RawMessage
|
||||||
|
for _, options := range optionsList {
|
||||||
|
mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage)
|
||||||
|
if err != nil {
|
||||||
|
return option.Options{}, E.Cause(err, "merge config at ", options.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var mergedOptions option.Options
|
||||||
|
err = mergedOptions.UnmarshalJSON(mergedMessage)
|
||||||
|
if err != nil {
|
||||||
|
return option.Options{}, E.Cause(err, "unmarshal merged config")
|
||||||
|
}
|
||||||
|
return mergedOptions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func create() (*server.Server, context.CancelFunc, error) {
|
||||||
|
options, err := readConfigAndMerge()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if disableColor {
|
||||||
|
if options.Log == nil {
|
||||||
|
options.Log = &boxOption.LogOptions{}
|
||||||
|
}
|
||||||
|
options.Log.DisableColor = true
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
instance, err := server.New(ctx, options)
|
||||||
|
if err != nil {
|
||||||
|
cancel()
|
||||||
|
return nil, nil, 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)
|
||||||
|
}()
|
||||||
|
startCtx, finishStart := context.WithCancel(context.Background())
|
||||||
|
go func() {
|
||||||
|
_, loaded := <-osSignals
|
||||||
|
if loaded {
|
||||||
|
cancel()
|
||||||
|
closeMonitor(startCtx)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
err = instance.Start()
|
||||||
|
finishStart()
|
||||||
|
if err != nil {
|
||||||
|
cancel()
|
||||||
|
return nil, nil, E.Cause(err, "start service")
|
||||||
|
}
|
||||||
|
return instance, cancel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
closeCtx, closed := context.WithCancel(context.Background())
|
||||||
|
go closeMonitor(closeCtx)
|
||||||
|
instance.Close()
|
||||||
|
closed()
|
||||||
|
if osSignal != syscall.SIGHUP {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeMonitor(ctx context.Context) {
|
||||||
|
time.Sleep(C.DefaultStopFatalTimeout)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
log.Fatal("sing-box did not close!")
|
||||||
|
}
|
52
cmd/serenity/cmd_version.go
Normal file
52
cmd/serenity/cmd_version.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
|
C "github.com/sagernet/serenity/constant"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandVersion = &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Short: "Print current version of serenity",
|
||||||
|
Run: printVersion,
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
var nameOnly bool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandVersion.Flags().BoolVarP(&nameOnly, "name", "n", false, "print version name only")
|
||||||
|
mainCommand.AddCommand(commandVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printVersion(cmd *cobra.Command, args []string) {
|
||||||
|
if nameOnly {
|
||||||
|
os.Stdout.WriteString(C.Version + "\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
version := "serenity version " + C.Version + " (sing-box " + C.CoreVersion() + ")\n\n"
|
||||||
|
version += "Environment: " + runtime.Version() + " " + runtime.GOOS + "/" + runtime.GOARCH + "\n"
|
||||||
|
|
||||||
|
var revision string
|
||||||
|
|
||||||
|
debugInfo, loaded := debug.ReadBuildInfo()
|
||||||
|
if loaded {
|
||||||
|
for _, setting := range debugInfo.Settings {
|
||||||
|
switch setting.Key {
|
||||||
|
case "vcs.revision":
|
||||||
|
revision = setting.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if revision != "" {
|
||||||
|
version += "Revision: " + revision + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Stdout.WriteString(version)
|
||||||
|
}
|
56
cmd/serenity/main.go
Normal file
56
cmd/serenity/main.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/sagernet/sing-box/include"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
configPaths []string
|
||||||
|
configDirectories []string
|
||||||
|
workingDir string
|
||||||
|
disableColor bool
|
||||||
|
)
|
||||||
|
|
||||||
|
var mainCommand = &cobra.Command{
|
||||||
|
Use: "serenity",
|
||||||
|
Short: "the configuration generator for sing-box",
|
||||||
|
PersistentPreRun: preRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
mainCommand.PersistentFlags().StringArrayVarP(&configPaths, "config", "c", nil, "set configuration file path")
|
||||||
|
mainCommand.PersistentFlags().StringArrayVarP(&configDirectories, "config-directory", "C", nil, "set configuration directory path")
|
||||||
|
mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
|
||||||
|
mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := mainCommand.Execute(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func preRun(cmd *cobra.Command, args []string) {
|
||||||
|
if disableColor {
|
||||||
|
log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger())
|
||||||
|
}
|
||||||
|
if workingDir != "" {
|
||||||
|
_, err := os.Stat(workingDir)
|
||||||
|
if err != nil {
|
||||||
|
os.MkdirAll(workingDir, 0o777)
|
||||||
|
}
|
||||||
|
if err := os.Chdir(workingDir); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(configPaths) == 0 && len(configDirectories) == 0 {
|
||||||
|
configPaths = append(configPaths, "config.json")
|
||||||
|
}
|
||||||
|
}
|
114
common/cachefile/cache.go
Normal file
114
common/cachefile/cache.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
package cachefile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/bbolt"
|
||||||
|
bboltErrors "github.com/sagernet/bbolt/errors"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
bucketSubscription = []byte("subscription")
|
||||||
|
|
||||||
|
bucketNameList = []string{
|
||||||
|
string(bucketSubscription),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type CacheFile struct {
|
||||||
|
path string
|
||||||
|
DB *bbolt.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(path string) *CacheFile {
|
||||||
|
return &CacheFile{
|
||||||
|
path: path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CacheFile) Start() error {
|
||||||
|
const fileMode = 0o666
|
||||||
|
options := bbolt.Options{Timeout: time.Second}
|
||||||
|
var (
|
||||||
|
db *bbolt.DB
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
db, err = bbolt.Open(c.path, fileMode, &options)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if errors.Is(err, bboltErrors.ErrTimeout) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) {
|
||||||
|
rmErr := os.Remove(c.path)
|
||||||
|
if rmErr != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.Batch(func(tx *bbolt.Tx) error {
|
||||||
|
return tx.ForEach(func(name []byte, b *bbolt.Bucket) error {
|
||||||
|
bucketName := string(name)
|
||||||
|
if !(common.Contains(bucketNameList, bucketName)) {
|
||||||
|
_ = tx.DeleteBucket(name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
db.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.DB = db
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CacheFile) Close() error {
|
||||||
|
if c.DB == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.DB.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CacheFile) LoadSubscription(name string) *Subscription {
|
||||||
|
var subscription Subscription
|
||||||
|
err := c.DB.View(func(tx *bbolt.Tx) error {
|
||||||
|
bucket := tx.Bucket(bucketSubscription)
|
||||||
|
if bucket == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data := bucket.Get([]byte(name))
|
||||||
|
if data == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return subscription.UnmarshalBinary(data)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &subscription
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CacheFile) StoreSubscription(name string, subscription *Subscription) error {
|
||||||
|
data, err := subscription.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.DB.Batch(func(tx *bbolt.Tx) error {
|
||||||
|
bucket, err := tx.CreateBucketIfNotExists(bucketSubscription)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bucket.Put([]byte(name), data)
|
||||||
|
})
|
||||||
|
}
|
76
common/cachefile/subscription.go
Normal file
76
common/cachefile/subscription.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package cachefile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
"github.com/sagernet/sing/common/rw"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Subscription struct {
|
||||||
|
Content []option.Outbound
|
||||||
|
LastUpdated time.Time
|
||||||
|
LastEtag string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Subscription) MarshalBinary() ([]byte, error) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
buffer.WriteByte(1)
|
||||||
|
content, err := json.Marshal(c.Content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = rw.WriteUVariant(&buffer, uint64(len(content)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = buffer.Write(content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = binary.Write(&buffer, binary.BigEndian, c.LastUpdated.Unix())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = rw.WriteVString(&buffer, c.LastEtag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Subscription) UnmarshalBinary(data []byte) error {
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
version, err := reader.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = version
|
||||||
|
contentLength, err := rw.ReadUVariant(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
content := make([]byte, contentLength)
|
||||||
|
_, err = reader.Read(content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(content, &c.Content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var lastUpdatedUnix int64
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &lastUpdatedUnix)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.LastUpdated = time.Unix(lastUpdatedUnix, 0)
|
||||||
|
c.LastEtag, err = rw.ReadVString(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
90
common/metadata/metadata.go
Normal file
90
common/metadata/metadata.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package metadata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/serenity/common/semver"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Platform string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PlatformUnknown Platform = ""
|
||||||
|
PlatformAndroid Platform = "android"
|
||||||
|
PlatformiOS Platform = "ios"
|
||||||
|
PlatformMacOS Platform = "macos"
|
||||||
|
PlatformAppleTVOS Platform = "tvos"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParsePlatform(name string) (Platform, error) {
|
||||||
|
switch strings.ToLower(name) {
|
||||||
|
case "android":
|
||||||
|
return PlatformAndroid, nil
|
||||||
|
case "ios":
|
||||||
|
return PlatformiOS, nil
|
||||||
|
case "macos":
|
||||||
|
return PlatformMacOS, nil
|
||||||
|
case "tvos":
|
||||||
|
return PlatformAppleTVOS, nil
|
||||||
|
default:
|
||||||
|
return PlatformUnknown, E.New("unknown platform: ", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Platform) IsApple() bool {
|
||||||
|
switch m {
|
||||||
|
case PlatformiOS, PlatformMacOS, PlatformAppleTVOS:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Platform) IsNetworkExtensionMemoryLimited() bool {
|
||||||
|
switch m {
|
||||||
|
case PlatformiOS, PlatformAppleTVOS:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Platform) TunOnly() bool {
|
||||||
|
return m.IsApple()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Platform) String() string {
|
||||||
|
return string(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Metadata struct {
|
||||||
|
UserAgent string
|
||||||
|
Platform Platform
|
||||||
|
Version *semver.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
func Detect(userAgent string) Metadata {
|
||||||
|
var metadata Metadata
|
||||||
|
metadata.UserAgent = userAgent
|
||||||
|
if strings.HasPrefix(userAgent, "SFA") {
|
||||||
|
metadata.Platform = PlatformAndroid
|
||||||
|
} else if strings.HasPrefix(userAgent, "SFI") {
|
||||||
|
metadata.Platform = PlatformiOS
|
||||||
|
} else if strings.HasPrefix(userAgent, "SFM") {
|
||||||
|
metadata.Platform = PlatformMacOS
|
||||||
|
} else if strings.HasPrefix(userAgent, "SFT") {
|
||||||
|
metadata.Platform = PlatformAppleTVOS
|
||||||
|
}
|
||||||
|
var versionName string
|
||||||
|
if strings.Contains(userAgent, "sing-box ") {
|
||||||
|
versionName = strings.Split(userAgent, "sing-box ")[1]
|
||||||
|
versionName = strings.Split(versionName, " ")[0]
|
||||||
|
versionName = strings.Split(versionName, ")")[0]
|
||||||
|
}
|
||||||
|
if semver.IsValid(versionName) {
|
||||||
|
version := semver.ParseVersion(versionName)
|
||||||
|
metadata.Version = &version
|
||||||
|
}
|
||||||
|
return metadata
|
||||||
|
}
|
148
common/semver/version.go
Normal file
148
common/semver/version.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package semver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
|
||||||
|
"golang.org/x/mod/semver"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Version struct {
|
||||||
|
Major int
|
||||||
|
Minor int
|
||||||
|
Patch int
|
||||||
|
Commit string
|
||||||
|
PreReleaseIdentifier string
|
||||||
|
PreReleaseVersion int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) LessThan(anotherVersion Version) bool {
|
||||||
|
return !v.GreaterThanOrEqual(anotherVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) LessThanOrEqual(anotherVersion Version) bool {
|
||||||
|
return v == anotherVersion || anotherVersion.GreaterThan(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) GreaterThanOrEqual(anotherVersion Version) bool {
|
||||||
|
return v == anotherVersion || v.GreaterThan(anotherVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) GreaterThan(anotherVersion Version) bool {
|
||||||
|
if v.Major > anotherVersion.Major {
|
||||||
|
return true
|
||||||
|
} else if v.Major < anotherVersion.Major {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if v.Minor > anotherVersion.Minor {
|
||||||
|
return true
|
||||||
|
} else if v.Minor < anotherVersion.Minor {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if v.Patch > anotherVersion.Patch {
|
||||||
|
return true
|
||||||
|
} else if v.Patch < anotherVersion.Patch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if v.PreReleaseIdentifier == "" && anotherVersion.PreReleaseIdentifier != "" {
|
||||||
|
return true
|
||||||
|
} else if v.PreReleaseIdentifier != "" && anotherVersion.PreReleaseIdentifier == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if v.PreReleaseIdentifier != "" && anotherVersion.PreReleaseIdentifier != "" {
|
||||||
|
if v.PreReleaseIdentifier == anotherVersion.PreReleaseIdentifier {
|
||||||
|
if v.PreReleaseVersion > anotherVersion.PreReleaseVersion {
|
||||||
|
return true
|
||||||
|
} else if v.PreReleaseVersion < anotherVersion.PreReleaseVersion {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
preReleaseIdentifier := parsePreReleaseIdentifier(v.PreReleaseIdentifier)
|
||||||
|
anotherPreReleaseIdentifier := parsePreReleaseIdentifier(anotherVersion.PreReleaseIdentifier)
|
||||||
|
if preReleaseIdentifier < anotherPreReleaseIdentifier {
|
||||||
|
return true
|
||||||
|
} else if preReleaseIdentifier > anotherPreReleaseIdentifier {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePreReleaseIdentifier(identifier string) int {
|
||||||
|
if strings.HasPrefix(identifier, "rc") {
|
||||||
|
return 1
|
||||||
|
} else if strings.HasPrefix(identifier, "beta") {
|
||||||
|
return 2
|
||||||
|
} else if strings.HasPrefix(identifier, "alpha") {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) String() string {
|
||||||
|
version := F.ToString(v.Major, ".", v.Minor, ".", v.Patch)
|
||||||
|
if v.PreReleaseIdentifier != "" {
|
||||||
|
version = F.ToString(version, "-", v.PreReleaseIdentifier, ".", v.PreReleaseVersion)
|
||||||
|
}
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) BadString() string {
|
||||||
|
version := F.ToString(v.Major, ".", v.Minor)
|
||||||
|
if v.Patch > 0 {
|
||||||
|
version = F.ToString(version, ".", v.Patch)
|
||||||
|
}
|
||||||
|
if v.PreReleaseIdentifier != "" {
|
||||||
|
version = F.ToString(version, "-", v.PreReleaseIdentifier)
|
||||||
|
if v.PreReleaseVersion > 0 {
|
||||||
|
version = F.ToString(version, v.PreReleaseVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsValid(versionName string) bool {
|
||||||
|
return semver.IsValid("v" + versionName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseVersion(versionName string) (version Version) {
|
||||||
|
if strings.HasPrefix(versionName, "v") {
|
||||||
|
versionName = versionName[1:]
|
||||||
|
}
|
||||||
|
if strings.Contains(versionName, "-") {
|
||||||
|
parts := strings.Split(versionName, "-")
|
||||||
|
versionName = parts[0]
|
||||||
|
identifier := parts[1]
|
||||||
|
if strings.Contains(identifier, ".") {
|
||||||
|
identifierParts := strings.Split(identifier, ".")
|
||||||
|
version.PreReleaseIdentifier = identifierParts[0]
|
||||||
|
if len(identifierParts) >= 2 {
|
||||||
|
version.PreReleaseVersion, _ = strconv.Atoi(identifierParts[1])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if strings.HasPrefix(identifier, "alpha") {
|
||||||
|
version.PreReleaseIdentifier = "alpha"
|
||||||
|
version.PreReleaseVersion, _ = strconv.Atoi(identifier[5:])
|
||||||
|
} else if strings.HasPrefix(identifier, "beta") {
|
||||||
|
version.PreReleaseIdentifier = "beta"
|
||||||
|
version.PreReleaseVersion, _ = strconv.Atoi(identifier[4:])
|
||||||
|
} else {
|
||||||
|
version.Commit = identifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
versionElements := strings.Split(versionName, ".")
|
||||||
|
versionLen := len(versionElements)
|
||||||
|
if versionLen >= 1 {
|
||||||
|
version.Major, _ = strconv.Atoi(versionElements[0])
|
||||||
|
}
|
||||||
|
if versionLen >= 2 {
|
||||||
|
version.Minor, _ = strconv.Atoi(versionElements[1])
|
||||||
|
}
|
||||||
|
if versionLen >= 3 {
|
||||||
|
version.Patch, _ = strconv.Atoi(versionElements[2])
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
37
constant/version.go
Normal file
37
constant/version.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime/debug"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Version = ""
|
||||||
|
coreVersion string
|
||||||
|
initializeCoreVersionOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func CoreVersion() string {
|
||||||
|
initializeCoreVersionOnce.Do(initializeCoreVersion)
|
||||||
|
return coreVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
func initializeCoreVersion() {
|
||||||
|
if !initializeCoreVersion0() {
|
||||||
|
coreVersion = "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initializeCoreVersion0() bool {
|
||||||
|
buildInfo, loaded := debug.ReadBuildInfo()
|
||||||
|
if !loaded {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, it := range buildInfo.Deps {
|
||||||
|
if it.Path == "github.com/sagernet/sing-box" {
|
||||||
|
coreVersion = it.Version
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
1
docs/CNAME
Normal file
1
docs/CNAME
Normal file
@ -0,0 +1 @@
|
|||||||
|
serenity.sagernet.org
|
37
docs/assets/icon.svg
Normal file
37
docs/assets/icon.svg
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<svg width="1027" height="1109" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" overflow="hidden">
|
||||||
|
<defs>
|
||||||
|
<filter id="fx0" x="-10%" y="-10%" width="120%" height="120%" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse">
|
||||||
|
<feComponentTransfer color-interpolation-filters="sRGB">
|
||||||
|
<feFuncR type="discrete" tableValues="0 0" />
|
||||||
|
<feFuncG type="discrete" tableValues="0 0" />
|
||||||
|
<feFuncB type="discrete" tableValues="0 0" />
|
||||||
|
<feFuncA type="linear" slope="0.4" intercept="0" />
|
||||||
|
</feComponentTransfer>
|
||||||
|
<feGaussianBlur stdDeviation="4.58333 4.58333" />
|
||||||
|
</filter>
|
||||||
|
<clipPath id="clip1">
|
||||||
|
<rect x="692" y="855" width="1027" height="1109" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip2">
|
||||||
|
<rect x="-2" y="-2" width="541" height="786" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip3">
|
||||||
|
<rect x="0" y="0" width="535" height="782" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g clip-path="url(#clip1)" transform="translate(-692 -855)">
|
||||||
|
<path d="M692 1191 692 1575.69C692 1640.41 731.499 1651.19 731.499 1651.19L1148.03 1931.62C1212.66 1974.77 1194.71 1881.29 1194.71 1881.29L1194.71 1528.96 692 1191Z" fill="#37474F" fill-rule="evenodd" />
|
||||||
|
<g clip-path="url(#clip2)" filter="url(#fx0)" transform="translate(1184 1182)">
|
||||||
|
<g clip-path="url(#clip3)">
|
||||||
|
<path d="M520.482 15.4819 520.482 400.176C520.482 464.89 480.983 475.676 480.983 475.676 480.983 475.676 129.086 712.963 64.4523 756.106-0.181814 799.25 17.7721 705.773 17.7721 705.773L17.7721 353.437 520.482 15.4819Z" fill="#455A64" fill-rule="evenodd" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M1698 1191 1698 1575.69C1698 1640.41 1658.5 1651.19 1658.5 1651.19 1658.5 1651.19 1306.6 1888.48 1241.97 1931.62 1177.34 1974.77 1195.29 1881.29 1195.29 1881.29L1195.29 1528.96 1698 1191Z" fill="#455A64" fill-rule="evenodd" />
|
||||||
|
<path d="M1241.71 868.473C1212.96 850.509 1169.85 850.509 1144.7 868.473L713.557 1163.07C684.814 1181.04 684.814 1213.37 713.557 1231.33L1144.7 1529.53C1173.44 1547.49 1216.56 1547.49 1241.71 1529.53L1676.44 1227.74C1705.19 1209.78 1705.19 1177.44 1676.44 1159.48L1241.71 868.473Z" fill="#546E7A" fill-rule="evenodd" />
|
||||||
|
<path d="M1195 1949C1173.4 1949 1159 1935.19 1159 1917.92L1159 1531.08C1159 1513.82 1173.4 1500 1195 1500 1216.6 1500 1231 1513.82 1231 1531.08L1231 1914.46C1231 1935.19 1216.6 1949 1195 1949Z" fill="#546E7A" fill-rule="evenodd" />
|
||||||
|
<path d="M1553.92 1435.92C1553.92 1471.89 1557.5 1486.27 1518.03 1511.45L1428.32 1568.99C1388.85 1594.17 1374.5 1572.59 1374.5 1540.22L1374.5 1446.71C1374.5 1439.52 1374.5 1435.92 1363.73 1428.73 1270.43 1363.99 911.591 1115.84 847 1069.09L1012.07 954C1058.72 982.772 1399.61 1209.35 1539.56 1306.45 1546.74 1310.05 1550.33 1317.24 1550.33 1320.84L1550.33 1435.92Z" fill="#99AAB5" fill-rule="evenodd" />
|
||||||
|
<path d="M1543.41 1310.21C1399.82 1213.17 1058.79 986.752 1015.72 958L951.103 997.534 847 1069.41C911.615 1116.14 1270.59 1360.53 1363.92 1425.22 1371.1 1428.81 1371.1 1432.41 1371.1 1436L1547 1313.8C1547 1313.8 1547 1310.21 1543.41 1310.21Z" fill="#CCD6DD" fill-rule="evenodd" />
|
||||||
|
<path d="M1554.9 1435.48 1554.9 1324.19C1554.9 1317.01 1551.3 1313.42 1544.11 1309.83 1400.28 1212.89 1058.67 986.721 1015.51 958L940 1008.26C1062.26 1090.83 1389.49 1306.24 1475.79 1367.27 1486.58 1374.45 1486.58 1381.63 1486.58 1385.22L1486.58 1536 1522.54 1510.87C1558.5 1485.74 1554.9 1467.79 1554.9 1435.48Z" fill="#CCD6DD" fill-rule="evenodd" />
|
||||||
|
<path d="M1543.23 1309.95C1399.6 1212.98 1058.49 986.731 1015.4 958L940 1008.28C1062.08 1090.88 1388.83 1306.36 1475.01 1367.41 1475.01 1367.41 1478.6 1371 1478.6 1371L1554 1317.13C1546.82 1313.54 1546.82 1309.95 1543.23 1309.95Z" fill="#E1E8ED" fill-rule="evenodd" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.7 KiB |
7
docs/changelog.md
Normal file
7
docs/changelog.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
icon: material/alert-decagram
|
||||||
|
---
|
||||||
|
|
||||||
|
##### 2023/12/12
|
||||||
|
|
||||||
|
No changelog before.
|
75
docs/configuration/index.md
Normal file
75
docs/configuration/index.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# Introduction
|
||||||
|
|
||||||
|
serenity uses JSON for configuration files.
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"log": {},
|
||||||
|
"listen": "",
|
||||||
|
"tls": {},
|
||||||
|
"cache_file": "",
|
||||||
|
"outbounds": [],
|
||||||
|
"subscriptions": [],
|
||||||
|
"templates": [],
|
||||||
|
"profiles": [],
|
||||||
|
"users": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
#### log
|
||||||
|
|
||||||
|
Log configuration, see [Log](https://sing-box.sagernet.org/configuration/log/).
|
||||||
|
|
||||||
|
#### listen
|
||||||
|
|
||||||
|
Listen address.
|
||||||
|
|
||||||
|
#### tls
|
||||||
|
|
||||||
|
TLS configuration, see [TLS](https://sing-box.sagernet.org/configuration/shared/tls/#inbound).
|
||||||
|
|
||||||
|
#### cache_file
|
||||||
|
|
||||||
|
Cache file path.
|
||||||
|
|
||||||
|
`cache.db` will be used if empty.
|
||||||
|
|
||||||
|
#### outbounds
|
||||||
|
|
||||||
|
List of [Outbound][outbound], can be referenced in [Profile](./profile).
|
||||||
|
|
||||||
|
For chained outbounds, use an array of outbounds as an item, and the first outbound will be the entry.
|
||||||
|
|
||||||
|
#### subscriptions
|
||||||
|
|
||||||
|
List of [Subscription](./subscription), can be referenced in [Profile](./profile).
|
||||||
|
|
||||||
|
#### templates
|
||||||
|
|
||||||
|
List of [Template](./template), can be referenced in [Profile](./profile).
|
||||||
|
|
||||||
|
#### profiles
|
||||||
|
|
||||||
|
List of [Profile](./profile), can be referenced in [User](./user).
|
||||||
|
|
||||||
|
#### users
|
||||||
|
|
||||||
|
List of [User](./user).
|
||||||
|
|
||||||
|
### Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
serenity check
|
||||||
|
```
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
```bash
|
||||||
|
serenity format -w -c config.json -D config_directory
|
||||||
|
```
|
||||||
|
|
||||||
|
[outbound]: https://sing-box.sagernet.org/configuration/outbound/
|
42
docs/configuration/profile.md
Normal file
42
docs/configuration/profile.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
### Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"template": "",
|
||||||
|
"template_for_platform": {},
|
||||||
|
"template_for_user_agent": {},
|
||||||
|
"outbound": [],
|
||||||
|
"subscription": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
#### name
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
Profile name.
|
||||||
|
|
||||||
|
#### template
|
||||||
|
|
||||||
|
Default template name.
|
||||||
|
|
||||||
|
A empty template is used by default.
|
||||||
|
|
||||||
|
#### template_for_platform
|
||||||
|
|
||||||
|
Custom template for different graphical client.
|
||||||
|
|
||||||
|
The key is one of `android`, `ios`, `macos`, `tvos`.
|
||||||
|
|
||||||
|
The Value is the template name.
|
||||||
|
|
||||||
|
#### template_for_user_agent
|
||||||
|
|
||||||
|
Custom template for different user agent.
|
||||||
|
|
||||||
|
The key is a regular expression matching the user agent.
|
||||||
|
|
||||||
|
The value is the template name.
|
114
docs/configuration/subscription.md
Normal file
114
docs/configuration/subscription.md
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
### Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"url": "",
|
||||||
|
"user_agent": "",
|
||||||
|
"process": [
|
||||||
|
{
|
||||||
|
"filter": [],
|
||||||
|
"exclude": [],
|
||||||
|
"filter_outbound_type": [],
|
||||||
|
"exclude_outbound_type": [],
|
||||||
|
"rename": [],
|
||||||
|
"remove_emoji": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"deduplication": false,
|
||||||
|
"update_interval": "5m",
|
||||||
|
"generate_selector": false,
|
||||||
|
"generate_urltest": false,
|
||||||
|
"urltest_suffix": false,
|
||||||
|
"custom_selector": {},
|
||||||
|
"custom_urltest": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
#### name
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
Name of the subscription, will be used in group tags.
|
||||||
|
|
||||||
|
#### url
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
Subscription URL.
|
||||||
|
|
||||||
|
#### user_agent
|
||||||
|
|
||||||
|
User-Agent in HTTP request.
|
||||||
|
|
||||||
|
`ClashForAndroid/serenity` is used by default.
|
||||||
|
|
||||||
|
#### process
|
||||||
|
|
||||||
|
!!! note ""
|
||||||
|
|
||||||
|
You can ignore the JSON Array [] tag when the content is only one item
|
||||||
|
|
||||||
|
Process rules.
|
||||||
|
|
||||||
|
#### process.filter
|
||||||
|
|
||||||
|
Regexp filter rules, non-matching outbounds will be removed.
|
||||||
|
|
||||||
|
#### process.exclude
|
||||||
|
|
||||||
|
Regexp exclude rules, matching outbounds will be removed.
|
||||||
|
|
||||||
|
#### process.filter_outbound_type
|
||||||
|
|
||||||
|
Outbound type filter rules, non-matching outbounds will be removed.
|
||||||
|
|
||||||
|
#### process.exclude_outbound_type
|
||||||
|
|
||||||
|
Outbound type exclude rules, matching outbounds will be removed.
|
||||||
|
|
||||||
|
#### process.rename
|
||||||
|
|
||||||
|
Regexp rename rules, matching outbounds will be renamed.
|
||||||
|
|
||||||
|
#### process.remove_emoji
|
||||||
|
|
||||||
|
Remove emojis in outbound tags.
|
||||||
|
|
||||||
|
#### deduplication
|
||||||
|
|
||||||
|
Remove outbounds with duplicate server destinations (Domain will be resolved to compare).
|
||||||
|
|
||||||
|
#### update_interval
|
||||||
|
|
||||||
|
Subscription update interval.
|
||||||
|
|
||||||
|
`1h` is used by default.
|
||||||
|
|
||||||
|
#### generate_selector
|
||||||
|
|
||||||
|
Generate a global `Selector` outbound for the subscription.
|
||||||
|
|
||||||
|
If both `generate_selector` and `generate_urltest` are disabled, subscription outbounds will be added to global groups.
|
||||||
|
|
||||||
|
#### generate_urltest
|
||||||
|
|
||||||
|
Generate a global `URLTest` outbound for the subscription.
|
||||||
|
|
||||||
|
If both `generate_selector` and `generate_urltest` are disabled, subscription outbounds will be added to global groups.
|
||||||
|
|
||||||
|
#### urltest_suffix
|
||||||
|
|
||||||
|
Tag suffix of generated `URLTest` outbound.
|
||||||
|
|
||||||
|
` - URLTest` is used by default.
|
||||||
|
|
||||||
|
#### custom_selector
|
||||||
|
|
||||||
|
Custom [Selector](https://sing-box.w.org/configuration/outbound/selector/) template.
|
||||||
|
|
||||||
|
#### custom_urltest
|
||||||
|
|
||||||
|
Custom [URLTest](https://sing-box.sagernet.org/configuration/outbound/urltest/) template.
|
283
docs/configuration/template.md
Normal file
283
docs/configuration/template.md
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
### Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
|
||||||
|
// Global
|
||||||
|
|
||||||
|
"domain_strategy": "",
|
||||||
|
"disable_traffic_bypass": false,
|
||||||
|
"disable_rule_set": false,
|
||||||
|
"remote_resolve": false,
|
||||||
|
|
||||||
|
// DNS
|
||||||
|
|
||||||
|
"dns_default": "",
|
||||||
|
"dns_local": "",
|
||||||
|
"enable_fakeip": false,
|
||||||
|
"pre_dns_rules": [],
|
||||||
|
"custom_dns_rules": [],
|
||||||
|
|
||||||
|
// Inbound
|
||||||
|
|
||||||
|
"disable_tun": false,
|
||||||
|
"disable_system_proxy": false,
|
||||||
|
"custom_tun": {},
|
||||||
|
"custom_mixed": {},
|
||||||
|
|
||||||
|
// Outbound
|
||||||
|
|
||||||
|
"extra_groups": [
|
||||||
|
{
|
||||||
|
"tag": "",
|
||||||
|
"type": "",
|
||||||
|
"filter": "",
|
||||||
|
"exclude": "",
|
||||||
|
"custom_selector": {},
|
||||||
|
"custom_urltest": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generate_global_urltest": false,
|
||||||
|
"direct_tag": "",
|
||||||
|
"default_tag": "",
|
||||||
|
"urltest_tag": "",
|
||||||
|
"custom_direct": {},
|
||||||
|
"custom_selector": {},
|
||||||
|
"custom_urltest": {},
|
||||||
|
|
||||||
|
// Route
|
||||||
|
|
||||||
|
"disable_default_rules": false,
|
||||||
|
"pre_rules": [],
|
||||||
|
"custom_rules": [],
|
||||||
|
"enable_jsdelivr": false,
|
||||||
|
"custom_geoip": {},
|
||||||
|
"custom_geosite": {},
|
||||||
|
"custom_rule_set": [],
|
||||||
|
|
||||||
|
// Experimental
|
||||||
|
|
||||||
|
"disable_cache_file": false,
|
||||||
|
"disable_clash_mode": false,
|
||||||
|
"clash_mode_rule": "",
|
||||||
|
"clash_mode_global": "",
|
||||||
|
"clash_mode_direct": "",
|
||||||
|
"custom_clash_api": {},
|
||||||
|
|
||||||
|
// Debug
|
||||||
|
|
||||||
|
"pprof_listen": "",
|
||||||
|
"memory_limit": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
#### name
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
Profile name.
|
||||||
|
|
||||||
|
#### domain_strategy
|
||||||
|
|
||||||
|
Global sing-box domain strategy.
|
||||||
|
|
||||||
|
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
||||||
|
|
||||||
|
If `*_only` enabled, TUN and DNS will be configured to disable the other network.
|
||||||
|
|
||||||
|
Note that if want `prefer_*` to take effect on transparent proxy requests, set `enable_fakeip`.
|
||||||
|
|
||||||
|
#### disable_rule_set
|
||||||
|
|
||||||
|
Use `geoip` and `geosite` for traffic bypassing instead of rule sets.
|
||||||
|
|
||||||
|
#### disable_traffic_bypass
|
||||||
|
|
||||||
|
Disable traffic bypass for Chinese DNS queries and connections.
|
||||||
|
|
||||||
|
#### remote_resolve
|
||||||
|
|
||||||
|
Don't generate `doamin_strategy` options for inbounds.
|
||||||
|
|
||||||
|
#### dns_default
|
||||||
|
|
||||||
|
Default DNS server.
|
||||||
|
|
||||||
|
`tls://8.8.8.8` is used by default.
|
||||||
|
|
||||||
|
#### dns_local
|
||||||
|
|
||||||
|
DNS server used for China DNS requests.
|
||||||
|
|
||||||
|
`114.114.114.114` is used by default.
|
||||||
|
|
||||||
|
#### enable_fakeip
|
||||||
|
|
||||||
|
Enable FakeIP.
|
||||||
|
|
||||||
|
#### pre_dns_rules
|
||||||
|
|
||||||
|
List of [DNS Rule](https://sing-box.sagernet.org/configuration/dns/rule/).
|
||||||
|
|
||||||
|
Will be applied before traffic bypassing rules.
|
||||||
|
|
||||||
|
#### custom_dns_rules
|
||||||
|
|
||||||
|
List of [DNS Rule](https://sing-box.sagernet.org/configuration/dns/rule/).
|
||||||
|
|
||||||
|
No default traffic bypassing DNS rules will be generated if not empty.
|
||||||
|
|
||||||
|
#### disable_tun
|
||||||
|
|
||||||
|
Don't generate TUN inbound.
|
||||||
|
|
||||||
|
If the target platform can only use TUN for proxy (currently all Apple platforms), this item will not take effect.
|
||||||
|
|
||||||
|
#### disable_system_proxy
|
||||||
|
|
||||||
|
Don't generate `tun.platform.http_proxy` for known platforms and `set_system_proxy` for unknown platforms.
|
||||||
|
|
||||||
|
#### custom_tun
|
||||||
|
|
||||||
|
Custom [TUN](https://sing-box.sagernet.org/configuration/inbound/tun/) inbound template.
|
||||||
|
|
||||||
|
#### custom_mixed
|
||||||
|
|
||||||
|
Custom [Mixed](https://sing-box.sagernet.org/configuration/inbound/mixed/) inbound template.
|
||||||
|
|
||||||
|
#### extra_groups
|
||||||
|
|
||||||
|
Generate extra outbound groups.
|
||||||
|
|
||||||
|
#### extra_groups.tag
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
Tag of the group outbound.
|
||||||
|
|
||||||
|
#### extra_groups.type
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
Type of the group outbound.
|
||||||
|
|
||||||
|
#### extra_groups.filter
|
||||||
|
|
||||||
|
Regexp filter rules, non-matching outbounds will be removed.
|
||||||
|
|
||||||
|
#### extra_groups.exclude
|
||||||
|
|
||||||
|
Regexp exclude rules, matching outbounds will be removed.
|
||||||
|
|
||||||
|
#### extra_groups.custom_selector
|
||||||
|
|
||||||
|
Custom [Selector](https://sing-box.sagernet.org/configuration/outbound/selector/) template.
|
||||||
|
|
||||||
|
#### extra_groups.custom_urltest
|
||||||
|
|
||||||
|
Custom [URLTest](https://sing-box.sagernet.org/configuration/outbound/urltest/) template.
|
||||||
|
|
||||||
|
#### generate_global_urltest
|
||||||
|
|
||||||
|
Generate a global `URLTest` outbound with all global outbounds.
|
||||||
|
|
||||||
|
#### direct_tag
|
||||||
|
|
||||||
|
Custom direct outbound tag.
|
||||||
|
|
||||||
|
#### default_tag
|
||||||
|
|
||||||
|
Custom default outbound tag.
|
||||||
|
|
||||||
|
#### urltest_tag
|
||||||
|
|
||||||
|
Custom URLTest outbound tag.
|
||||||
|
|
||||||
|
#### custom_direct
|
||||||
|
|
||||||
|
Custom [Direct](https://sing-box.sagernet.org/configuration/outbound/direct/) outbound template.
|
||||||
|
|
||||||
|
#### custom_selector
|
||||||
|
|
||||||
|
Custom [Selector](https://sing-box.sagernet.org/configuration/outbound/selector/) outbound template.
|
||||||
|
|
||||||
|
#### custom_urltest
|
||||||
|
|
||||||
|
Custom [URLTest](https://sing-box.sagernet.org/configuration/outbound/urltest/) outbound template.
|
||||||
|
|
||||||
|
#### disable_default_rules
|
||||||
|
|
||||||
|
Don't generate some useful rules.
|
||||||
|
|
||||||
|
#### pre_rules
|
||||||
|
|
||||||
|
List of [Rule](https://sing-box.sagernet.org/configuration/route/rule/).
|
||||||
|
|
||||||
|
Will be applied before traffic bypassing rules.
|
||||||
|
|
||||||
|
#### custom_rules
|
||||||
|
|
||||||
|
List of [Rule](https://sing-box.sagernet.org/configuration/route/rule/).
|
||||||
|
|
||||||
|
No default traffic bypassing rules will be generated if not empty.
|
||||||
|
|
||||||
|
#### enable_jsdelivr
|
||||||
|
|
||||||
|
Use jsDelivr CDN and direct outbound for default rule sets or Geo resources.
|
||||||
|
|
||||||
|
#### custom_geoip
|
||||||
|
|
||||||
|
Custom [GeoIP](https://sing-box.sagernet.org/configuration/route/geoip/) template.
|
||||||
|
|
||||||
|
#### custom_geosite
|
||||||
|
|
||||||
|
Custom [GeoSite](https://sing-box.sagernet.org/configuration/route/geosite/) template.
|
||||||
|
|
||||||
|
#### custom_rule_set
|
||||||
|
|
||||||
|
List of [RuleSet](https://sing-box.sagernet.org/configuration/rule-set/).
|
||||||
|
|
||||||
|
Default rule sets will not be generated if not empty.
|
||||||
|
|
||||||
|
#### disable_cache_file
|
||||||
|
|
||||||
|
Don't generate `cache_file` related options.
|
||||||
|
|
||||||
|
#### disable_clash_mode
|
||||||
|
|
||||||
|
Don't generate `clash_mode` related options.
|
||||||
|
|
||||||
|
#### clash_mode_rule
|
||||||
|
|
||||||
|
Name of the 'Rule' Clash mode.
|
||||||
|
|
||||||
|
`Rule` is used by default.
|
||||||
|
|
||||||
|
#### clash_mode_global
|
||||||
|
|
||||||
|
Name of the 'Global' Clash mode.
|
||||||
|
|
||||||
|
`Global` is used by default.
|
||||||
|
|
||||||
|
#### clash_mode_direct
|
||||||
|
|
||||||
|
Name of the 'Direct' Clash mode.
|
||||||
|
|
||||||
|
`Direct` is used by default.
|
||||||
|
|
||||||
|
#### custom_clash_api
|
||||||
|
|
||||||
|
Custom [Clash API](https://sing-box.sagernet.org/configuration/experimental/clash-api/) template.
|
||||||
|
|
||||||
|
#### pprof_listen
|
||||||
|
|
||||||
|
Listen address of the pprof server.
|
||||||
|
|
||||||
|
#### memory_limit
|
||||||
|
|
||||||
|
Set soft memory limit for sing-box.
|
||||||
|
|
||||||
|
`100m` is recommended if memory limit is required.
|
36
docs/configuration/user.md
Normal file
36
docs/configuration/user.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
### Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"password": "",
|
||||||
|
"profile": [],
|
||||||
|
"default_profile": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note ""
|
||||||
|
|
||||||
|
You can ignore the JSON Array [] tag when the content is only one item
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
#### name
|
||||||
|
|
||||||
|
HTTP basic authentication username.
|
||||||
|
|
||||||
|
#### password
|
||||||
|
|
||||||
|
HTTP basic authentication password.
|
||||||
|
|
||||||
|
#### profile
|
||||||
|
|
||||||
|
Accessible profiles for this user.
|
||||||
|
|
||||||
|
List of [Profile](./profile) name.
|
||||||
|
|
||||||
|
#### default_profile
|
||||||
|
|
||||||
|
Default profile name.
|
||||||
|
|
||||||
|
First profile is used by default.
|
31
docs/index.md
Normal file
31
docs/index.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
description: Welcome to the wiki page for the serenity project.
|
||||||
|
---
|
||||||
|
|
||||||
|
# :material-home: Home
|
||||||
|
|
||||||
|
Welcome to the wiki page for the serenity project.
|
||||||
|
|
||||||
|
The configuration generator for sing-box.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
```
|
||||||
|
Copyright (C) 2022 by nekohasekai <contact-sagernet@sekai.icu>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
In addition, no derivative work may use the name or imply association
|
||||||
|
with this application without prior consent.
|
||||||
|
```
|
31
docs/index.zh.md
Normal file
31
docs/index.zh.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
description: 欢迎来到该 sing-box 项目的文档页。
|
||||||
|
---
|
||||||
|
|
||||||
|
# :material-home: 开始
|
||||||
|
|
||||||
|
欢迎来到该 sing-box 项目的文档页。
|
||||||
|
|
||||||
|
sing-box 配置生成器。
|
||||||
|
|
||||||
|
## 授权
|
||||||
|
|
||||||
|
```
|
||||||
|
Copyright (C) 2022 by nekohasekai <contact-sagernet@sekai.icu>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
In addition, no derivative work may use the name or imply association
|
||||||
|
with this application without prior consent.
|
||||||
|
```
|
23
docs/installation/build-from-source.md
Normal file
23
docs/installation/build-from-source.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
icon: material/file-code
|
||||||
|
---
|
||||||
|
|
||||||
|
# Build from source
|
||||||
|
|
||||||
|
## :material-graph: Requirements
|
||||||
|
|
||||||
|
* Go 1.21.x
|
||||||
|
|
||||||
|
You can download and install Go from: https://go.dev/doc/install, latest version is recommended.
|
||||||
|
|
||||||
|
## :material-fast-forward: Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
Or build and install binary to `$GOBIN`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make install
|
||||||
|
```
|
23
docs/installation/build-from-source.zh.md
Normal file
23
docs/installation/build-from-source.zh.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
icon: material/file-code
|
||||||
|
---
|
||||||
|
|
||||||
|
# 从源代码构建
|
||||||
|
|
||||||
|
## :material-graph: 要求
|
||||||
|
|
||||||
|
* Go 1.21.x
|
||||||
|
|
||||||
|
您可以从 https://go.dev/doc/install 下载并安装 Go,推荐使用最新版本。
|
||||||
|
|
||||||
|
## :material-fast-forward: 构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
或者构建二进制文件并将其安装到 `$GOBIN`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make install
|
||||||
|
```
|
31
docs/installation/docker.md
Normal file
31
docs/installation/docker.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
icon: material/docker
|
||||||
|
---
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
|
||||||
|
## :material-console: Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
-v /etc/serenity:/etc/serenity/ \
|
||||||
|
--name=serenity \
|
||||||
|
--restart=always \
|
||||||
|
ghcr.io/sagernet/serenity \
|
||||||
|
-D /var/lib/serenity \
|
||||||
|
-C /etc/serenity/ run
|
||||||
|
```
|
||||||
|
|
||||||
|
## :material-box-shadow: Compose
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3.8"
|
||||||
|
services:
|
||||||
|
serenity:
|
||||||
|
image: ghcr.io/sagernet/serenity
|
||||||
|
container_name: serenity
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- /etc/serenity:/etc/serenity/
|
||||||
|
command: -D /var/lib/serenity -C /etc/serenity/ run
|
||||||
|
```
|
31
docs/installation/docker.zh.md
Normal file
31
docs/installation/docker.zh.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
icon: material/docker
|
||||||
|
---
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
|
||||||
|
## :material-console: 命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
-v /etc/serenity:/etc/serenity/ \
|
||||||
|
--name=serenity \
|
||||||
|
--restart=always \
|
||||||
|
ghcr.io/sagernet/serenity \
|
||||||
|
-D /var/lib/serenity \
|
||||||
|
-C /etc/serenity/ run
|
||||||
|
```
|
||||||
|
|
||||||
|
## :material-box-shadow: Compose
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3.8"
|
||||||
|
services:
|
||||||
|
serenity:
|
||||||
|
image: ghcr.io/sagernet/serenity
|
||||||
|
container_name: serenity
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- /etc/serenity:/etc/serenity/
|
||||||
|
command: -D /var/lib/serenity -C /etc/serenity/ run
|
||||||
|
```
|
43
docs/installation/package-manager.md
Normal file
43
docs/installation/package-manager.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
icon: material/package
|
||||||
|
---
|
||||||
|
|
||||||
|
# Package Manager
|
||||||
|
|
||||||
|
## :material-download-box: Manual Installation
|
||||||
|
|
||||||
|
=== ":material-debian: Debian / DEB"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -fsSL https://sing-box.app/serenity/deb-install.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
=== ":material-redhat: Redhat / RPM"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -fsSL https://sing-box.app/serenity/rpm-install.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
=== ":simple-archlinux: Archlinux / PKG"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -fsSL https://sing-box.app/serenity/arch-install.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
## :material-book-multiple: Service Management
|
||||||
|
|
||||||
|
For Linux systems with [systemd][systemd], usually the installation already includes a serenity service,
|
||||||
|
you can manage the service using the following command:
|
||||||
|
|
||||||
|
| Operation | Command |
|
||||||
|
|-----------|-----------------------------------------------|
|
||||||
|
| Enable | `sudo systemctl enable serenity` |
|
||||||
|
| Disable | `sudo systemctl disable serenity` |
|
||||||
|
| Start | `sudo systemctl start serenity` |
|
||||||
|
| Stop | `sudo systemctl stop serenity` |
|
||||||
|
| Kill | `sudo systemctl kill serenity` |
|
||||||
|
| Restart | `sudo systemctl restart serenity` |
|
||||||
|
| Logs | `sudo journalctl -u serenity --output cat -e` |
|
||||||
|
| New Logs | `sudo journalctl -u serenity --output cat -f` |
|
||||||
|
|
||||||
|
[systemd]: https://systemd.io/
|
43
docs/installation/package-manager.zh.md
Normal file
43
docs/installation/package-manager.zh.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
icon: material/package
|
||||||
|
---
|
||||||
|
|
||||||
|
# 包管理器
|
||||||
|
|
||||||
|
## :material-download-box: 手动安装
|
||||||
|
|
||||||
|
=== ":material-debian: Debian / DEB"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -fsSL https://sing-box.app/serenity/deb-install.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
=== ":material-redhat: Redhat / RPM"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -fsSL https://sing-box.app/serenity/rpm-install.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
=== ":simple-archlinux: Archlinux / PKG"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -fsSL https://sing-box.app/serenity/arch-install.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
## :material-book-multiple: 服务管理
|
||||||
|
|
||||||
|
对于带有 [systemd][systemd] 的 Linux 系统,通常安装已经包含 serenity 服务,
|
||||||
|
您可以使用以下命令管理服务:
|
||||||
|
|
||||||
|
| 行动 | 命令 |
|
||||||
|
|------|-----------------------------------------------|
|
||||||
|
| 启用 | `sudo systemctl enable serenity` |
|
||||||
|
| 禁用 | `sudo systemctl disable serenity` |
|
||||||
|
| 启动 | `sudo systemctl start serenity` |
|
||||||
|
| 停止 | `sudo systemctl stop serenity` |
|
||||||
|
| 强行停止 | `sudo systemctl kill serenity` |
|
||||||
|
| 重新启动 | `sudo systemctl restart serenity` |
|
||||||
|
| 查看日志 | `sudo journalctl -u serenity --output cat -e` |
|
||||||
|
| 实时日志 | `sudo journalctl -u serenity --output cat -f` |
|
||||||
|
|
||||||
|
[systemd]: https://systemd.io/
|
22
docs/installation/scripts/arch-install.sh
Normal file
22
docs/installation/scripts/arch-install.sh
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e -o pipefail
|
||||||
|
|
||||||
|
ARCH_RAW=$(uname -m)
|
||||||
|
case "${ARCH_RAW}" in
|
||||||
|
'x86_64') ARCH='amd64';;
|
||||||
|
'x86' | 'i686' | 'i386') ARCH='386';;
|
||||||
|
'aarch64' | 'arm64') ARCH='arm64';;
|
||||||
|
'armv7l') ARCH='armv7';;
|
||||||
|
's390x') ARCH='s390x';;
|
||||||
|
*) echo "Unsupported architecture: ${ARCH_RAW}"; exit 1;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
VERSION=$(curl -s https://api.github.com/repos/SagerNet/serenity/releases/latest \
|
||||||
|
| grep tag_name \
|
||||||
|
| cut -d ":" -f2 \
|
||||||
|
| sed 's/\"//g;s/\,//g;s/\ //g;s/v//')
|
||||||
|
|
||||||
|
curl -Lo serenity.pkg.tar.zst "https://github.com/SagerNet/serenity/releases/download/v${VERSION}/serenity_${VERSION}_linux_${ARCH}.pkg.tar.zst"
|
||||||
|
sudo pacman -U serenity.pkg.tar.zst
|
||||||
|
rm serenity.pkg.tar.zst
|
23
docs/installation/scripts/deb-install.sh
Normal file
23
docs/installation/scripts/deb-install.sh
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e -o pipefail
|
||||||
|
|
||||||
|
ARCH_RAW=$(uname -m)
|
||||||
|
case "${ARCH_RAW}" in
|
||||||
|
'x86_64') ARCH='amd64';;
|
||||||
|
'x86' | 'i686' | 'i386') ARCH='386';;
|
||||||
|
'aarch64' | 'arm64') ARCH='arm64';;
|
||||||
|
'armv7l') ARCH='armv7';;
|
||||||
|
's390x') ARCH='s390x';;
|
||||||
|
*) echo "Unsupported architecture: ${ARCH_RAW}"; exit 1;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
VERSION=$(curl -s https://api.github.com/repos/SagerNet/serenity/releases/latest \
|
||||||
|
| grep tag_name \
|
||||||
|
| cut -d ":" -f2 \
|
||||||
|
| sed 's/\"//g;s/\,//g;s/\ //g;s/v//')
|
||||||
|
|
||||||
|
curl -Lo serenity.deb "https://github.com/SagerNet/serenity/releases/download/v${VERSION}/serenity_${VERSION}_linux_${ARCH}.deb"
|
||||||
|
sudo dpkg -i serenity.deb
|
||||||
|
rm serenity.deb
|
||||||
|
|
22
docs/installation/scripts/rpm-install.sh
Normal file
22
docs/installation/scripts/rpm-install.sh
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e -o pipefail
|
||||||
|
|
||||||
|
ARCH_RAW=$(uname -m)
|
||||||
|
case "${ARCH_RAW}" in
|
||||||
|
'x86_64') ARCH='amd64';;
|
||||||
|
'x86' | 'i686' | 'i386') ARCH='386';;
|
||||||
|
'aarch64' | 'arm64') ARCH='arm64';;
|
||||||
|
'armv7l') ARCH='armv7';;
|
||||||
|
's390x') ARCH='s390x';;
|
||||||
|
*) echo "Unsupported architecture: ${ARCH_RAW}"; exit 1;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
VERSION=$(curl -s https://api.github.com/repos/SagerNet/serenity/releases/latest \
|
||||||
|
| grep tag_name \
|
||||||
|
| cut -d ":" -f2 \
|
||||||
|
| sed 's/\"//g;s/\,//g;s/\ //g;s/v//')
|
||||||
|
|
||||||
|
curl -Lo serenity.rpm "https://github.com/SagerNet/serenity/releases/download/v${VERSION}/serenity_${VERSION}_linux_${ARCH}.rpm"
|
||||||
|
sudo rpm -i serenity.rpm
|
||||||
|
rm serenity.rpm
|
13
docs/support.md
Normal file
13
docs/support.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
icon: material/forum
|
||||||
|
---
|
||||||
|
|
||||||
|
# Support
|
||||||
|
|
||||||
|
| Channel | Link |
|
||||||
|
|:------------------------------|:--------------------------------------------|
|
||||||
|
| Community | https://community.sagernet.org |
|
||||||
|
| GitHub Issues | https://github.com/SagerNet/serenity/issues |
|
||||||
|
| Telegram notification channel | https://t.me/yapnc |
|
||||||
|
| Telegram user group | https://t.me/yapug |
|
||||||
|
| Email | contact@sagernet.org |
|
14
docs/support.zh.md
Normal file
14
docs/support.zh.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
icon: material/forum
|
||||||
|
---
|
||||||
|
|
||||||
|
# 支持
|
||||||
|
|
||||||
|
| 通道 | 链接 |
|
||||||
|
|:--------------|:--------------------------------------------|
|
||||||
|
| 社区 | https://community.sagernet.org |
|
||||||
|
| GitHub Issues | https://github.com/SagerNet/serenity/issues |
|
||||||
|
| Telegram 通知频道 | https://t.me/yapnc |
|
||||||
|
| Telegram 用户组 | https://t.me/yapug |
|
||||||
|
| 邮件 | contact@sagernet.org |
|
||||||
|
|
102
go.mod
Normal file
102
go.mod
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
module github.com/sagernet/serenity
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Dreamacro/clash v1.18.0
|
||||||
|
github.com/go-chi/chi/v5 v5.0.11
|
||||||
|
github.com/go-chi/cors v1.2.1
|
||||||
|
github.com/go-chi/render v1.0.3
|
||||||
|
github.com/miekg/dns v1.1.57
|
||||||
|
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
|
||||||
|
github.com/sagernet/sing v0.3.0-rc.5
|
||||||
|
github.com/sagernet/sing-box v1.8.0-rc.4
|
||||||
|
github.com/sagernet/sing-dns v0.1.12
|
||||||
|
github.com/spf13/cobra v1.8.0
|
||||||
|
golang.org/x/mod v0.14.0
|
||||||
|
golang.org/x/net v0.19.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
berty.tech/go-libtor v1.0.385 // indirect
|
||||||
|
github.com/Dreamacro/protobytes v0.0.0-20230617041236-6500a9f4f158 // indirect
|
||||||
|
github.com/ajg/form v1.5.1 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||||
|
github.com/caddyserver/certmagic v0.20.0 // indirect
|
||||||
|
github.com/cloudflare/circl v1.3.6 // indirect
|
||||||
|
github.com/cretz/bine v0.2.0 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
|
github.com/gaukas/godicttls v0.0.4 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||||
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
|
github.com/gofrs/uuid/v5 v5.0.0 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
github.com/google/btree v1.1.2 // indirect
|
||||||
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
|
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect
|
||||||
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.17.4 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||||
|
github.com/libdns/alidns v1.0.3 // indirect
|
||||||
|
github.com/libdns/cloudflare v0.1.0 // indirect
|
||||||
|
github.com/libdns/libdns v0.2.1 // indirect
|
||||||
|
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
|
||||||
|
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||||
|
github.com/mdlayher/socket v0.4.1 // indirect
|
||||||
|
github.com/mholt/acmez v1.2.0 // indirect
|
||||||
|
github.com/onsi/ginkgo/v2 v2.9.7 // indirect
|
||||||
|
github.com/ooni/go-libtor v1.1.8 // indirect
|
||||||
|
github.com/oschwald/geoip2-golang v1.9.0 // indirect
|
||||||
|
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.14 // indirect
|
||||||
|
github.com/quic-go/qpack v0.4.0 // indirect
|
||||||
|
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||||
|
github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 // indirect
|
||||||
|
github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e // indirect
|
||||||
|
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect
|
||||||
|
github.com/sagernet/quic-go v0.40.0 // indirect
|
||||||
|
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 // indirect
|
||||||
|
github.com/sagernet/sing-mux v0.1.7-rc.1 // indirect
|
||||||
|
github.com/sagernet/sing-quic v0.1.7-rc.1 // indirect
|
||||||
|
github.com/sagernet/sing-shadowsocks2 v0.1.6-rc.1 // indirect
|
||||||
|
github.com/sagernet/sing-shadowtls v0.1.4 // indirect
|
||||||
|
github.com/sagernet/sing-tun v0.2.0-rc.1 // indirect
|
||||||
|
github.com/sagernet/sing-vmess v0.1.8 // indirect
|
||||||
|
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
|
||||||
|
github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6 // indirect
|
||||||
|
github.com/sagernet/utls v1.5.4 // indirect
|
||||||
|
github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8 // indirect
|
||||||
|
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect
|
||||||
|
github.com/samber/lo v1.38.1 // indirect
|
||||||
|
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
|
||||||
|
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230420174744-55c8b9515a01 // indirect
|
||||||
|
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
|
||||||
|
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||||
|
go.etcd.io/bbolt v1.3.7 // indirect
|
||||||
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
go.uber.org/zap v1.26.0 // indirect
|
||||||
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||||
|
golang.org/x/crypto v0.17.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // indirect
|
||||||
|
golang.org/x/sync v0.5.0 // indirect
|
||||||
|
golang.org/x/sys v0.15.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
golang.org/x/time v0.5.0 // indirect
|
||||||
|
golang.org/x/tools v0.16.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||||
|
google.golang.org/grpc v1.59.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
lukechampine.com/blake3 v1.2.1 // indirect
|
||||||
|
)
|
249
go.sum
Normal file
249
go.sum
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
berty.tech/go-libtor v1.0.385 h1:RWK94C3hZj6Z2GdvePpHJLnWYobFr3bY/OdUJ5aoEXw=
|
||||||
|
berty.tech/go-libtor v1.0.385/go.mod h1:9swOOQVb+kmvuAlsgWUK/4c52pm69AdbJsxLzk+fJEw=
|
||||||
|
github.com/Dreamacro/clash v1.18.0 h1:tic7ykTOCaT0mxwAkXo6QP3LN3Nps8oZz9atgr6TU8A=
|
||||||
|
github.com/Dreamacro/clash v1.18.0/go.mod h1:r//xe/2pA3Zl+3fjIiI/o6RjIVd+z87drCD58dpRnFg=
|
||||||
|
github.com/Dreamacro/protobytes v0.0.0-20230617041236-6500a9f4f158 h1:JFnwKplz9hj8ubqYjm8HkgZS1Rvz9yW+u/XCNNTxr0k=
|
||||||
|
github.com/Dreamacro/protobytes v0.0.0-20230617041236-6500a9f4f158/go.mod h1:QvmEZ/h6KXszPOr2wUFl7Zn3hfFNYdfbXwPVDTyZs6k=
|
||||||
|
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||||
|
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||||
|
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||||
|
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
|
github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc=
|
||||||
|
github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg=
|
||||||
|
github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg=
|
||||||
|
github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw=
|
||||||
|
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
|
||||||
|
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||||
|
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
|
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
||||||
|
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||||
|
github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
|
||||||
|
github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
|
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||||
|
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||||
|
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||||
|
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||||
|
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||||
|
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||||
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||||
|
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||||
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
|
||||||
|
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||||
|
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
|
||||||
|
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||||
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||||
|
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
|
||||||
|
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
|
||||||
|
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
|
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||||
|
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
|
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||||
|
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/libdns/alidns v1.0.3 h1:LFHuGnbseq5+HCeGa1aW8awyX/4M2psB9962fdD2+yQ=
|
||||||
|
github.com/libdns/alidns v1.0.3/go.mod h1:e18uAG6GanfRhcJj6/tps2rCMzQJaYVcGKT+ELjdjGE=
|
||||||
|
github.com/libdns/cloudflare v0.1.0 h1:93WkJaGaiXCe353LHEP36kAWCUw0YjFqwhkBkU2/iic=
|
||||||
|
github.com/libdns/cloudflare v0.1.0/go.mod h1:a44IP6J1YH6nvcNl1PverfJviADgXUnsozR3a7vBKN8=
|
||||||
|
github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
||||||
|
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
|
||||||
|
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
||||||
|
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
|
||||||
|
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||||
|
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||||
|
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||||
|
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||||
|
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||||
|
github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
|
||||||
|
github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE=
|
||||||
|
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
|
||||||
|
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0=
|
||||||
|
github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
|
||||||
|
github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4=
|
||||||
|
github.com/ooni/go-libtor v1.1.8 h1:Wo3V3DVTxl5vZdxtQakqYP+DAHx7pPtAFSl1bnAa08w=
|
||||||
|
github.com/ooni/go-libtor v1.1.8/go.mod h1:q1YyLwRD9GeMyeerVvwc0vJ2YgwDLTp2bdVcrh/JXyI=
|
||||||
|
github.com/oschwald/geoip2-golang v1.9.0 h1:uvD3O6fXAXs+usU+UGExshpdP13GAqp4GBrzN7IgKZc=
|
||||||
|
github.com/oschwald/geoip2-golang v1.9.0/go.mod h1:BHK6TvDyATVQhKNbQBdrj9eAvuwOMi2zSFXizL3K81Y=
|
||||||
|
github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs=
|
||||||
|
github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||||
|
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||||
|
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
|
||||||
|
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0=
|
||||||
|
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
|
||||||
|
github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 h1:YbmpqPQEMdlk9oFSKYWRqVuu9qzNiOayIonKmv1gCXY=
|
||||||
|
github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1/go.mod h1:J2yAxTFPDjrDPhuAi9aWFz2L3ox9it4qAluBBbN0H5k=
|
||||||
|
github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e h1:DOkjByVeAR56dkszjnMZke4wr7yM/1xHaJF3G9olkEE=
|
||||||
|
github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e/go.mod h1:fLxq/gtp0qzkaEwywlRRiGmjOK5ES/xUzyIKIFP2Asw=
|
||||||
|
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
|
||||||
|
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||||
|
github.com/sagernet/quic-go v0.40.0 h1:DvQNPb72lzvNQDe9tcUyHTw8eRv6PLtM2mNYmdlzUMo=
|
||||||
|
github.com/sagernet/quic-go v0.40.0/go.mod h1:VqtdhlbkeeG5Okhb3eDMb/9o0EoglReHunNT9ukrJAI=
|
||||||
|
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
|
||||||
|
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
|
||||||
|
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
||||||
|
github.com/sagernet/sing v0.3.0-rc.5 h1:dliXrWhkeI1ppcwTfwDPktLf75DXQLbd9g5ngtAEmc0=
|
||||||
|
github.com/sagernet/sing v0.3.0-rc.5/go.mod h1:9pfuAH6mZfgnz/YjP6xu5sxx882rfyjpcrTdUpd6w3g=
|
||||||
|
github.com/sagernet/sing-box v1.8.0-rc.4 h1:DJdSlBFnc/7RQDRWjZBRrOWXro1+TMbyohd57smdgZo=
|
||||||
|
github.com/sagernet/sing-box v1.8.0-rc.4/go.mod h1:RXQ/HtakATVTLpEpVnrpn39gGCAm9SnNYJRb7hqDnS0=
|
||||||
|
github.com/sagernet/sing-dns v0.1.12 h1:1HqZ+ln+Rezx/aJMStaS0d7oPeX2EobSV1NT537kyj4=
|
||||||
|
github.com/sagernet/sing-dns v0.1.12/go.mod h1:rx/DTOisneQpCgNQ4jbFU/JNEtnz0lYcHXenlVzpjEU=
|
||||||
|
github.com/sagernet/sing-mux v0.1.7-rc.1 h1:4XlQSeIqKA6uaW1KGQA9wCG5zt9ajhvc/zJnnZV/n1c=
|
||||||
|
github.com/sagernet/sing-mux v0.1.7-rc.1/go.mod h1:KK5zCbNujj5kn36G+wLFROOXyJhaaXLyaZWY2w7kBNQ=
|
||||||
|
github.com/sagernet/sing-quic v0.1.7-rc.1 h1:HoZC7YFmHCpfrUNXbousGauLw6yg25f6qcIkqrYIDbg=
|
||||||
|
github.com/sagernet/sing-quic v0.1.7-rc.1/go.mod h1:+XDZtFPD8YJ4V1MMObOdf2k5+Ma1jy75OlRnlBUHihI=
|
||||||
|
github.com/sagernet/sing-shadowsocks2 v0.1.6-rc.1 h1:E+8OyyVg0YfFNUmxMx9jYBEhjLYMQSAMzJrUmE934bo=
|
||||||
|
github.com/sagernet/sing-shadowsocks2 v0.1.6-rc.1/go.mod h1:wFkU7sKxyZADS/idtJqBhtc+QBf5iwX9nZO7ymcn6MM=
|
||||||
|
github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
|
||||||
|
github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4=
|
||||||
|
github.com/sagernet/sing-tun v0.2.0-rc.1 h1:CnlxRgrJKAMKYNuJOcKie6TjRz8wremEq1wndLup7cA=
|
||||||
|
github.com/sagernet/sing-tun v0.2.0-rc.1/go.mod h1:hpbL9jNAbYT9G2EHCpCXVIgSrM/2Wgnrm/Hped+8zdY=
|
||||||
|
github.com/sagernet/sing-vmess v0.1.8 h1:XVWad1RpTy9b5tPxdm5MCU8cGfrTGdR8qCq6HV2aCNc=
|
||||||
|
github.com/sagernet/sing-vmess v0.1.8/go.mod h1:vhx32UNzTDUkNwOyIjcZQohre1CaytquC5mPplId8uA=
|
||||||
|
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
|
||||||
|
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
|
||||||
|
github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6 h1:z3SJQhVyU63FT26Wn/UByW6b7q8QKB0ZkPqsyqcz2PI=
|
||||||
|
github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6/go.mod h1:73xRZuxwkFk4aiLw28hG8W6o9cr2UPrGL9pdY2UTbvY=
|
||||||
|
github.com/sagernet/utls v1.5.4 h1:KmsEGbB2dKUtCNC+44NwAdNAqnqQ6GA4pTO0Yik56co=
|
||||||
|
github.com/sagernet/utls v1.5.4/go.mod h1:CTGxPWExIloRipK3XFpYv0OVyhO8kk3XCGW/ieyTh1s=
|
||||||
|
github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8 h1:R0OMYAScomNAVpTfbHFpxqJpvwuhxSRi+g6z7gZhABs=
|
||||||
|
github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8/go.mod h1:K4J7/npM+VAMUeUmTa2JaA02JmyheP0GpRBOUvn3ecc=
|
||||||
|
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
|
||||||
|
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
|
||||||
|
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
||||||
|
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||||
|
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg=
|
||||||
|
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||||
|
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA=
|
||||||
|
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
|
||||||
|
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230420174744-55c8b9515a01 h1:F9xjJm4IH8VjcqG4ujciOF+GIM4mjPkHhWLLzOghPtM=
|
||||||
|
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230420174744-55c8b9515a01/go.mod h1:cAAsePK2e15YDAMJNyOpGYEWNe4sIghTY7gpz4cX/Ik=
|
||||||
|
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||||
|
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=
|
||||||
|
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||||
|
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||||
|
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||||
|
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
|
||||||
|
github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
|
||||||
|
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||||
|
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||||
|
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
||||||
|
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||||
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
|
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||||
|
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||||
|
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||||
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||||
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||||
|
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
|
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||||
|
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
|
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE=
|
||||||
|
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||||
|
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||||
|
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||||
|
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||||
|
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||||
|
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||||
|
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM=
|
||||||
|
golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
|
||||||
|
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||||
|
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
|
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
|
||||||
|
lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
113
mkdocs.yml
Normal file
113
mkdocs.yml
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
site_name: serenity
|
||||||
|
site_author: nekohasekai
|
||||||
|
repo_url: https://github.com/SagerNet/serenity
|
||||||
|
repo_name: SagerNet/serenity
|
||||||
|
copyright: Copyright © 2022 nekohasekai
|
||||||
|
site_description: The configuration generator for sing-box.
|
||||||
|
remote_branch: docs
|
||||||
|
edit_uri: ""
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
logo: assets/icon.svg
|
||||||
|
favicon: assets/icon.svg
|
||||||
|
palette:
|
||||||
|
- scheme: default
|
||||||
|
primary: white
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-7
|
||||||
|
name: Switch to dark mode
|
||||||
|
- scheme: slate
|
||||||
|
primary: black
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-4
|
||||||
|
name: Switch to light mode
|
||||||
|
features:
|
||||||
|
# - navigation.instant
|
||||||
|
- navigation.tracking
|
||||||
|
- navigation.tabs
|
||||||
|
- navigation.indexes
|
||||||
|
- navigation.expand
|
||||||
|
- navigation.sections
|
||||||
|
- header.autohide
|
||||||
|
- content.code.copy
|
||||||
|
- content.code.select
|
||||||
|
- content.code.annotate
|
||||||
|
icon:
|
||||||
|
admonition:
|
||||||
|
question: material/new-box
|
||||||
|
nav:
|
||||||
|
- Home:
|
||||||
|
- index.md
|
||||||
|
- Change Log: changelog.md
|
||||||
|
- Support: support.md
|
||||||
|
- Installation:
|
||||||
|
- Package Manager: installation/package-manager.md
|
||||||
|
- Docker: installation/docker.md
|
||||||
|
- Build from source: installation/build-from-source.md
|
||||||
|
- Configuration:
|
||||||
|
- configuration/index.md
|
||||||
|
- Subscription: configuration/subscription.md
|
||||||
|
- Template: configuration/template.md
|
||||||
|
- Profile: configuration/profile.md
|
||||||
|
- User: configuration/user.md
|
||||||
|
markdown_extensions:
|
||||||
|
- pymdownx.inlinehilite
|
||||||
|
- pymdownx.snippets
|
||||||
|
- pymdownx.superfences
|
||||||
|
- pymdownx.details
|
||||||
|
- pymdownx.critic
|
||||||
|
- pymdownx.caret
|
||||||
|
- pymdownx.keys
|
||||||
|
- pymdownx.mark
|
||||||
|
- pymdownx.tilde
|
||||||
|
- pymdownx.magiclink
|
||||||
|
- admonition
|
||||||
|
- attr_list
|
||||||
|
- md_in_html
|
||||||
|
- footnotes
|
||||||
|
- def_list
|
||||||
|
- pymdownx.highlight:
|
||||||
|
anchor_linenums: true
|
||||||
|
- pymdownx.tabbed:
|
||||||
|
alternate_style: true
|
||||||
|
- pymdownx.tasklist:
|
||||||
|
custom_checkbox: true
|
||||||
|
- pymdownx.emoji:
|
||||||
|
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||||
|
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||||
|
- pymdownx.superfences:
|
||||||
|
custom_fences:
|
||||||
|
- name: mermaid
|
||||||
|
class: mermaid
|
||||||
|
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||||
|
extra:
|
||||||
|
social:
|
||||||
|
- icon: fontawesome/brands/github
|
||||||
|
link: https://github.com/SagerNet/serenity
|
||||||
|
generator: false
|
||||||
|
plugins:
|
||||||
|
- search
|
||||||
|
- i18n:
|
||||||
|
docs_structure: suffix
|
||||||
|
fallback_to_default: true
|
||||||
|
languages:
|
||||||
|
- build: true
|
||||||
|
default: true
|
||||||
|
locale: en
|
||||||
|
name: English
|
||||||
|
- build: true
|
||||||
|
default: false
|
||||||
|
locale: zh
|
||||||
|
name: 简体中文
|
||||||
|
nav_translations:
|
||||||
|
Home: 开始
|
||||||
|
Change Log: 更新日志
|
||||||
|
Support: 支持
|
||||||
|
|
||||||
|
Installation: 安装
|
||||||
|
Package Manager: 包管理器
|
||||||
|
Build from source: 从源代码构建
|
||||||
|
|
||||||
|
Configuration: 配置
|
||||||
|
reconfigure_material: true
|
||||||
|
reconfigure_search: true
|
17
option/message.go
Normal file
17
option/message.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package option
|
||||||
|
|
||||||
|
import "github.com/sagernet/sing/common/json"
|
||||||
|
|
||||||
|
type TypedMessage[T any] struct {
|
||||||
|
Message json.RawMessage
|
||||||
|
Value T
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TypedMessage[T]) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(m.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TypedMessage[T]) UnmarshalJSON(bytes []byte) error {
|
||||||
|
m.Message = bytes
|
||||||
|
return json.Unmarshal(bytes, &m.Value)
|
||||||
|
}
|
80
option/options.go
Normal file
80
option/options.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package option
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
"github.com/sagernet/sing/common/json/badjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type _Options struct {
|
||||||
|
RawMessage json.RawMessage `json:"-"`
|
||||||
|
Log *option.LogOptions `json:"log,omitempty"`
|
||||||
|
Listen string `json:"listen,omitempty"`
|
||||||
|
TLS *option.InboundTLSOptions `json:"tls,omitempty"`
|
||||||
|
CacheFile string `json:"cache_file,omitempty"`
|
||||||
|
|
||||||
|
Outbounds []option.Listable[option.Outbound] `json:"outbounds,omitempty"`
|
||||||
|
Subscriptions []Subscription `json:"subscriptions,omitempty"`
|
||||||
|
Templates []Template `json:"templates,omitempty"`
|
||||||
|
Profiles []Profile `json:"profiles,omitempty"`
|
||||||
|
Users []User `json:"users,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options _Options
|
||||||
|
|
||||||
|
func (o *Options) UnmarshalJSON(content []byte) error {
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(content))
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
err := decoder.Decode((*_Options)(o))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.RawMessage = content
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
Profile option.Listable[string] `json:"profile,omitempty"`
|
||||||
|
DefaultProfile string `json:"default_profile,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultSubscriptionUpdateInterval = 1 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
type Subscription struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
UserAgent string `json:"user_agent,omitempty"`
|
||||||
|
UpdateInterval option.Duration `json:"update_interval,omitempty"`
|
||||||
|
Process option.Listable[OutboundProcessOptions] `json:"process,omitempty"`
|
||||||
|
DeDuplication bool `json:"deduplication,omitempty"`
|
||||||
|
GenerateSelector bool `json:"generate_selector,omitempty"`
|
||||||
|
GenerateURLTest bool `json:"generate_urltest,omitempty"`
|
||||||
|
URLTestTagSuffix string `json:"urltest_suffix,omitempty"`
|
||||||
|
CustomSelector *option.SelectorOutboundOptions `json:"custom_selector,omitempty"`
|
||||||
|
CustomURLTest *option.URLTestOutboundOptions `json:"custom_urltest,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutboundProcessOptions struct {
|
||||||
|
Filter option.Listable[string] `json:"filter,omitempty"`
|
||||||
|
Exclude option.Listable[string] `json:"exclude,omitempty"`
|
||||||
|
FilterOutboundType option.Listable[string] `json:"filter_outbound_type,omitempty"`
|
||||||
|
ExcludeOutboundType option.Listable[string] `json:"exclude_outbound_type,omitempty"`
|
||||||
|
Rename *badjson.TypedMap[string, string] `json:"rename,omitempty"`
|
||||||
|
RemoveEmoji bool `json:"remove_emoji,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Profile struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Template string `json:"template,omitempty"`
|
||||||
|
TemplateForPlatform *badjson.TypedMap[string, string] `json:"template_for_platform,omitempty"`
|
||||||
|
TemplateForUserAgent *badjson.TypedMap[string, string] `json:"template_for_user_agent,omitempty"`
|
||||||
|
Outbound option.Listable[string] `json:"outbound,omitempty"`
|
||||||
|
Subscription option.Listable[string] `json:"subscription,omitempty"`
|
||||||
|
}
|
77
option/template.go
Normal file
77
option/template.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package option
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/serenity/common/semver"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
dns "github.com/sagernet/sing-dns"
|
||||||
|
"github.com/sagernet/sing/common/json/badjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Template struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Global
|
||||||
|
|
||||||
|
DomainStrategy option.DomainStrategy `json:"domain_strategy,omitempty"`
|
||||||
|
DisableTrafficBypass bool `json:"disable_traffic_bypass,omitempty"`
|
||||||
|
DisableRuleSet bool `json:"disable_rule_set,omitempty"`
|
||||||
|
RemoteResolve bool `json:"remote_resolve,omitempty"`
|
||||||
|
|
||||||
|
// DNS
|
||||||
|
DNSDefault string `json:"dns_default,omitempty"`
|
||||||
|
DNSLocal string `json:"dns_local,omitempty"`
|
||||||
|
EnableFakeIP bool `json:"enable_fakeip,omitempty"`
|
||||||
|
PreDNSRules []option.DNSRule `json:"pre_dns_rules,omitempty"`
|
||||||
|
CustomDNSRules []option.DNSRule `json:"custom_dns_rules,omitempty"`
|
||||||
|
|
||||||
|
// Inbound
|
||||||
|
DisableTUN bool `json:"disable_tun,omitempty"`
|
||||||
|
DisableSystemProxy bool `json:"disable_system_proxy,omitempty"`
|
||||||
|
CustomTUN *TypedMessage[option.TunInboundOptions] `json:"custom_tun,omitempty"`
|
||||||
|
CustomMixed *TypedMessage[option.HTTPMixedInboundOptions] `json:"custom_mixed,omitempty"`
|
||||||
|
|
||||||
|
// Outbound
|
||||||
|
ExtraGroups []ExtraGroup `json:"extra_groups,omitempty"`
|
||||||
|
GenerateGlobalURLTest bool `json:"generate_global_urltest,omitempty"`
|
||||||
|
DirectTag string `json:"direct_tag,omitempty"`
|
||||||
|
DefaultTag string `json:"default_tag,omitempty"`
|
||||||
|
URLTestTag string `json:"urltest_tag,omitempty"`
|
||||||
|
CustomDirect *option.DirectOutboundOptions `json:"custom_direct,omitempty"`
|
||||||
|
CustomSelector *option.SelectorOutboundOptions `json:"custom_selector,omitempty"`
|
||||||
|
CustomURLTest *option.URLTestOutboundOptions `json:"custom_urltest,omitempty"`
|
||||||
|
|
||||||
|
// Route
|
||||||
|
DisableDefaultRules bool `json:"disable_default_rules,omitempty"`
|
||||||
|
PreRules []option.Rule `json:"pre_rules,omitempty"`
|
||||||
|
CustomRules []option.Rule `json:"custom_rules,omitempty"`
|
||||||
|
CustomRulesForVersionLessThan badjson.TypedMap[semver.Version, []option.Rule] `json:"custom_rules_for_version_less_than,omitempty"`
|
||||||
|
EnableJSDelivr bool `json:"enable_jsdelivr,omitempty"`
|
||||||
|
CustomGeoIP *option.GeoIPOptions `json:"custom_geoip,omitempty"`
|
||||||
|
CustomGeosite *option.GeositeOptions `json:"custom_geosite,omitempty"`
|
||||||
|
CustomRuleSet []option.RuleSet `json:"custom_rule_set,omitempty"`
|
||||||
|
|
||||||
|
// Experimental
|
||||||
|
DisableCacheFile bool `json:"disable_cache_file,omitempty"`
|
||||||
|
DisableClashMode bool `json:"disable_clash_mode,omitempty"`
|
||||||
|
ClashModeRule string `json:"clash_mode_rule,omitempty"`
|
||||||
|
ClashModeGlobal string `json:"clash_mode_global,omitempty"`
|
||||||
|
ClashModeDirect string `json:"clash_mode_direct,omitempty"`
|
||||||
|
CustomClashAPI *TypedMessage[option.ClashAPIOptions] `json:"custom_clash_api,omitempty"`
|
||||||
|
|
||||||
|
// Debug
|
||||||
|
PProfListen string `json:"pprof_listen,omitempty"`
|
||||||
|
MemoryLimit option.MemoryBytes `json:"memory_limit,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Template) DisableIPv6() bool {
|
||||||
|
return t.DomainStrategy == option.DomainStrategy(dns.DomainStrategyUseIPv4)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExtraGroup struct {
|
||||||
|
Tag string `json:"tag,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Filter option.Listable[string] `json:"filter,omitempty"`
|
||||||
|
Exclude option.Listable[string] `json:"exclude,omitempty"`
|
||||||
|
CustomSelector *option.SelectorOutboundOptions `json:"custom_selector,omitempty"`
|
||||||
|
CustomURLTest *option.URLTestOutboundOptions `json:"custom_urltest,omitempty"`
|
||||||
|
}
|
8
release/config/config.json
Normal file
8
release/config/config.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"listen": ":8080",
|
||||||
|
"users": [],
|
||||||
|
"subscriptions": [],
|
||||||
|
"outbounds": [],
|
||||||
|
"templates": [],
|
||||||
|
"profiles": []
|
||||||
|
}
|
16
release/config/serenity.service
Normal file
16
release/config/serenity.service
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=serenity service
|
||||||
|
Documentation=https://serenity.sagernet.org
|
||||||
|
After=network.target nss-lookup.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||||
|
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||||
|
ExecStart=/usr/bin/serenity -D /var/lib/serenity -C /etc/serenity run
|
||||||
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10s
|
||||||
|
LimitNOFILE=infinity
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
16
release/config/serenity@.service
Normal file
16
release/config/serenity@.service
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=serenity service
|
||||||
|
Documentation=https://serenity.sagernet.org
|
||||||
|
After=network.target nss-lookup.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||||
|
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||||
|
ExecStart=/usr/bin/serenity -D /var/lib/serenity-%i -c /etc/serenity/%i.json run
|
||||||
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10s
|
||||||
|
LimitNOFILE=infinity
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
7
release/local/enable.sh
Executable file
7
release/local/enable.sh
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e -o pipefail
|
||||||
|
|
||||||
|
sudo systemctl enable serenity
|
||||||
|
sudo systemctl start serenity
|
||||||
|
sudo journalctl -u serenity --output cat -f
|
20
release/local/install.sh
Executable file
20
release/local/install.sh
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e -o pipefail
|
||||||
|
|
||||||
|
if [ -d /usr/local/go ]; then
|
||||||
|
export PATH="$PATH:/usr/local/go/bin"
|
||||||
|
fi
|
||||||
|
|
||||||
|
DIR=$(dirname "$0")
|
||||||
|
PROJECT=$DIR/../..
|
||||||
|
|
||||||
|
pushd $PROJECT
|
||||||
|
go install -v -trimpath -ldflags "-s -w -buildid=" ./cmd/serenity
|
||||||
|
popd
|
||||||
|
|
||||||
|
sudo cp $(go env GOPATH)/bin/serenity /usr/local/bin/
|
||||||
|
sudo mkdir -p /usr/local/etc/serenity
|
||||||
|
sudo cp $PROJECT/release/config/config.json /usr/local/etc/serenity/config.json
|
||||||
|
sudo cp $DIR/serenity.service /etc/systemd/system
|
||||||
|
sudo systemctl daemon-reload
|
9
release/local/install_go.sh
Executable file
9
release/local/install_go.sh
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e -o pipefail
|
||||||
|
|
||||||
|
go_version=$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g')
|
||||||
|
curl -Lo go.tar.gz "https://go.dev/dl/go$go_version.linux-amd64.tar.gz"
|
||||||
|
sudo rm -rf /usr/local/go
|
||||||
|
sudo tar -C /usr/local -xzf go.tar.gz
|
||||||
|
rm go.tar.gz
|
18
release/local/reinstall.sh
Executable file
18
release/local/reinstall.sh
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e -o pipefail
|
||||||
|
|
||||||
|
if [ -d /usr/local/go ]; then
|
||||||
|
export PATH="$PATH:/usr/local/go/bin"
|
||||||
|
fi
|
||||||
|
|
||||||
|
DIR=$(dirname "$0")
|
||||||
|
PROJECT=$DIR/../..
|
||||||
|
|
||||||
|
pushd $PROJECT
|
||||||
|
go install -v -trimpath -ldflags "-s -w -buildid=" ./cmd/serenity
|
||||||
|
popd
|
||||||
|
|
||||||
|
sudo systemctl stop serenity
|
||||||
|
sudo cp $(go env GOPATH)/bin/serenity /usr/local/bin/
|
||||||
|
sudo systemctl start serenity
|
16
release/local/serenity.service
Normal file
16
release/local/serenity.service
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=serenity service
|
||||||
|
Documentation=https://serenity.sagernet.org
|
||||||
|
After=network.target nss-lookup.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||||
|
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||||
|
ExecStart=/usr/local/bin/serenity -D /var/lib/serenity -C /usr/local/etc/serenity run
|
||||||
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10s
|
||||||
|
LimitNOFILE=infinity
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
8
release/local/uninstall.sh
Executable file
8
release/local/uninstall.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
sudo systemctl stop serenity
|
||||||
|
sudo rm -rf /var/lib/serenity
|
||||||
|
sudo rm -rf /usr/local/bin/serenity
|
||||||
|
sudo rm -rf /usr/local/etc/serenity
|
||||||
|
sudo rm -rf /etc/systemd/system/serenity.service
|
||||||
|
sudo systemctl daemon-reload
|
14
release/local/update.sh
Executable file
14
release/local/update.sh
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e -o pipefail
|
||||||
|
|
||||||
|
DIR=$(dirname "$0")
|
||||||
|
PROJECT=$DIR/../..
|
||||||
|
|
||||||
|
pushd $PROJECT
|
||||||
|
git fetch
|
||||||
|
git reset FETCH_HEAD --hard
|
||||||
|
git clean -fdx
|
||||||
|
popd
|
||||||
|
|
||||||
|
$DIR/reinstall.sh
|
144
server/profile.go
Normal file
144
server/profile.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/sagernet/serenity/common/metadata"
|
||||||
|
"github.com/sagernet/serenity/option"
|
||||||
|
"github.com/sagernet/serenity/subscription"
|
||||||
|
"github.com/sagernet/serenity/template"
|
||||||
|
boxOption "github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProfileManager struct {
|
||||||
|
ctx context.Context
|
||||||
|
logger logger.Logger
|
||||||
|
subscription *subscription.Manager
|
||||||
|
outbounds [][]boxOption.Outbound
|
||||||
|
profiles []*Profile
|
||||||
|
defaultProfile *Profile
|
||||||
|
}
|
||||||
|
|
||||||
|
type Profile struct {
|
||||||
|
option.Profile
|
||||||
|
manager *ProfileManager
|
||||||
|
template *template.Template
|
||||||
|
templateForPlatform map[metadata.Platform]*template.Template
|
||||||
|
templateForUserAgent map[*regexp.Regexp]*template.Template
|
||||||
|
groups []ExtraGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExtraGroup struct {
|
||||||
|
option.ExtraGroup
|
||||||
|
filterRegex []*regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProfileManager(
|
||||||
|
ctx context.Context,
|
||||||
|
logger logger.Logger,
|
||||||
|
subscriptionManager *subscription.Manager,
|
||||||
|
templateManager *template.Manager,
|
||||||
|
outbounds [][]boxOption.Outbound,
|
||||||
|
rawProfiles []option.Profile,
|
||||||
|
) (*ProfileManager, error) {
|
||||||
|
manager := &ProfileManager{
|
||||||
|
ctx: ctx,
|
||||||
|
logger: logger,
|
||||||
|
subscription: subscriptionManager,
|
||||||
|
outbounds: outbounds,
|
||||||
|
}
|
||||||
|
for profileIndex, profile := range rawProfiles {
|
||||||
|
if profile.Name == "" {
|
||||||
|
return nil, E.New("initialize profile[", profileIndex, "]: missing name")
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
defaultTemplate *template.Template
|
||||||
|
templateForPlatform = make(map[metadata.Platform]*template.Template)
|
||||||
|
templateForUserAgent = make(map[*regexp.Regexp]*template.Template)
|
||||||
|
)
|
||||||
|
if profile.Template != "" {
|
||||||
|
defaultTemplate = templateManager.TemplateByName(profile.Template)
|
||||||
|
if defaultTemplate == nil {
|
||||||
|
return nil, E.New("initialize profile[", profile.Name, "]: template not found: ", profile.Template)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
defaultTemplate = template.Default
|
||||||
|
}
|
||||||
|
if profile.TemplateForPlatform != nil {
|
||||||
|
for templateIndex, entry := range profile.TemplateForPlatform.Entries() {
|
||||||
|
platform, err := metadata.ParsePlatform(entry.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initialize profile[", profile.Name, "]: parse template_for_platform[", templateIndex, "]")
|
||||||
|
}
|
||||||
|
customTemplate := templateManager.TemplateByName(entry.Value)
|
||||||
|
if customTemplate == nil {
|
||||||
|
return nil, E.New("initialize profile[", profile.Name, "]: parse template_for_platform[", entry.Key, "]: template not found: ", entry.Value)
|
||||||
|
}
|
||||||
|
templateForPlatform[platform] = customTemplate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if profile.TemplateForUserAgent != nil {
|
||||||
|
for templateIndex, entry := range profile.TemplateForUserAgent.Entries() {
|
||||||
|
regex, err := regexp.Compile(entry.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initialize profile[", profile.Name, "]: parse template_for_user_agent[", templateIndex, "]")
|
||||||
|
}
|
||||||
|
customTemplate := templateManager.TemplateByName(entry.Value)
|
||||||
|
if customTemplate == nil {
|
||||||
|
return nil, E.New("initialize profile[", profile.Name, "]: parse template_for_user_agent[", entry.Key, "]: template not found: ", entry.Value)
|
||||||
|
}
|
||||||
|
templateForUserAgent[regex] = customTemplate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
manager.profiles = append(manager.profiles, &Profile{
|
||||||
|
Profile: profile,
|
||||||
|
manager: manager,
|
||||||
|
template: defaultTemplate,
|
||||||
|
templateForPlatform: templateForPlatform,
|
||||||
|
templateForUserAgent: templateForUserAgent,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(manager.profiles) > 0 {
|
||||||
|
manager.defaultProfile = manager.profiles[0]
|
||||||
|
}
|
||||||
|
return manager, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ProfileManager) ProfileByName(name string) *Profile {
|
||||||
|
for _, it := range m.profiles {
|
||||||
|
if it.Name == name {
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ProfileManager) DefaultProfile() *Profile {
|
||||||
|
return m.defaultProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Profile) Render(metadata metadata.Metadata) (*boxOption.Options, error) {
|
||||||
|
selectedTemplate, loaded := p.templateForPlatform[metadata.Platform]
|
||||||
|
if !loaded {
|
||||||
|
for regex, it := range p.templateForUserAgent {
|
||||||
|
if regex.MatchString(metadata.UserAgent) {
|
||||||
|
selectedTemplate = it
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if selectedTemplate == nil {
|
||||||
|
selectedTemplate = p.template
|
||||||
|
}
|
||||||
|
outbounds := common.Filter(p.manager.outbounds, func(it []boxOption.Outbound) bool {
|
||||||
|
return common.Contains(p.Outbound, it[0].Tag)
|
||||||
|
})
|
||||||
|
subscriptions := common.Filter(p.manager.subscription.Subscriptions(), func(it *subscription.Subscription) bool {
|
||||||
|
return common.Contains(p.Subscription, it.Name)
|
||||||
|
})
|
||||||
|
return selectedTemplate.Render(metadata, p.Name, outbounds, subscriptions)
|
||||||
|
}
|
192
server/server.go
Normal file
192
server/server.go
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/serenity/common/cachefile"
|
||||||
|
"github.com/sagernet/serenity/option"
|
||||||
|
"github.com/sagernet/serenity/subscription"
|
||||||
|
"github.com/sagernet/serenity/template"
|
||||||
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
boxOption "github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
createdAt time.Time
|
||||||
|
ctx context.Context
|
||||||
|
logFactory log.Factory
|
||||||
|
logger log.Logger
|
||||||
|
chiRouter chi.Router
|
||||||
|
httpServer *http.Server
|
||||||
|
tlsConfig tls.ServerConfig
|
||||||
|
cacheFile *cachefile.CacheFile
|
||||||
|
subscription *subscription.Manager
|
||||||
|
template *template.Manager
|
||||||
|
profile *ProfileManager
|
||||||
|
users []option.User
|
||||||
|
userMap map[string][]option.User
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, options option.Options) (*Server, error) {
|
||||||
|
ctx = service.ContextWithDefaultRegistry(ctx)
|
||||||
|
createdAt := time.Now()
|
||||||
|
logFactory, err := log.New(log.Options{
|
||||||
|
Options: common.PtrValueOrDefault(options.Log),
|
||||||
|
DefaultWriter: os.Stderr,
|
||||||
|
BaseTime: createdAt,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "create log factory")
|
||||||
|
}
|
||||||
|
chiRouter := chi.NewRouter()
|
||||||
|
httpServer := &http.Server{
|
||||||
|
Addr: options.Listen,
|
||||||
|
Handler: chiRouter,
|
||||||
|
}
|
||||||
|
if httpServer.Addr == "" {
|
||||||
|
if options.TLS != nil && options.TLS.Enabled {
|
||||||
|
httpServer.Addr = ":443"
|
||||||
|
} else {
|
||||||
|
httpServer.Addr = ":80"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var tlsConfig tls.ServerConfig
|
||||||
|
if options.TLS != nil {
|
||||||
|
tlsConfig, err = tls.NewServer(ctx, logFactory.NewLogger("tls"), common.PtrValueOrDefault(options.TLS))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var cacheFilePath string
|
||||||
|
if options.CacheFile != "" {
|
||||||
|
cacheFilePath = options.CacheFile
|
||||||
|
} else {
|
||||||
|
cacheFilePath = "cache.db"
|
||||||
|
}
|
||||||
|
cacheFile := cachefile.New(cacheFilePath)
|
||||||
|
subscriptionManager, err := subscription.NewSubscriptionManager(
|
||||||
|
ctx,
|
||||||
|
logFactory.NewLogger("subscription"),
|
||||||
|
cacheFile,
|
||||||
|
options.Subscriptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
templateManager, err := template.NewManager(
|
||||||
|
ctx,
|
||||||
|
logFactory.NewLogger("template"),
|
||||||
|
options.Templates)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
profileManager, err := NewProfileManager(
|
||||||
|
ctx,
|
||||||
|
logFactory.NewLogger("profile"),
|
||||||
|
subscriptionManager,
|
||||||
|
templateManager,
|
||||||
|
common.Map(options.Outbounds, func(it boxOption.Listable[boxOption.Outbound]) []boxOption.Outbound {
|
||||||
|
return it
|
||||||
|
}),
|
||||||
|
options.Profiles,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
userMap := make(map[string][]option.User)
|
||||||
|
for _, user := range options.Users {
|
||||||
|
userMap[user.Name] = append(userMap[user.Name], user)
|
||||||
|
}
|
||||||
|
return &Server{
|
||||||
|
createdAt: createdAt,
|
||||||
|
ctx: ctx,
|
||||||
|
logFactory: logFactory,
|
||||||
|
logger: logFactory.Logger(),
|
||||||
|
chiRouter: chiRouter,
|
||||||
|
httpServer: httpServer,
|
||||||
|
tlsConfig: tlsConfig,
|
||||||
|
cacheFile: cacheFile,
|
||||||
|
subscription: subscriptionManager,
|
||||||
|
template: templateManager,
|
||||||
|
profile: profileManager,
|
||||||
|
users: options.Users,
|
||||||
|
userMap: userMap,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Start() error {
|
||||||
|
s.initializeRoutes()
|
||||||
|
err := s.cacheFile.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = s.subscription.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
listener, err := net.Listen("tcp", s.httpServer.Addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s.tlsConfig != nil {
|
||||||
|
err = s.tlsConfig.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = http2.ConfigureServer(s.httpServer, new(http2.Server))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stdConfig, err := s.tlsConfig.Config()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.httpServer.TLSConfig = stdConfig
|
||||||
|
}
|
||||||
|
s.logger.Info("server started at ", listener.Addr())
|
||||||
|
go func() {
|
||||||
|
if s.httpServer.TLSConfig != nil {
|
||||||
|
err = s.httpServer.ServeTLS(listener, "", "")
|
||||||
|
} else {
|
||||||
|
err = s.httpServer.Serve(listener)
|
||||||
|
}
|
||||||
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
s.logger.Error("server serve error: ", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
err = s.postStart()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.logger.Info("serenity started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) postStart() error {
|
||||||
|
err := s.subscription.PostStart()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "post-start subscription manager")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Close() error {
|
||||||
|
return common.Close(
|
||||||
|
s.logFactory,
|
||||||
|
common.PtrOrNil(s.httpServer),
|
||||||
|
s.tlsConfig,
|
||||||
|
common.PtrOrNil(s.cacheFile),
|
||||||
|
)
|
||||||
|
}
|
124
server/server_render.go
Normal file
124
server/server_render.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
M "github.com/sagernet/serenity/common/metadata"
|
||||||
|
"github.com/sagernet/serenity/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/cors"
|
||||||
|
"github.com/go-chi/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) initializeRoutes() {
|
||||||
|
s.chiRouter.Use(cors.New(cors.Options{
|
||||||
|
AllowedOrigins: []string{"*"},
|
||||||
|
AllowedMethods: []string{"GET"},
|
||||||
|
AllowedHeaders: []string{"Content-Type", "Authorization"},
|
||||||
|
}).Handler)
|
||||||
|
s.chiRouter.Get("/", s.render)
|
||||||
|
s.chiRouter.Get("/{profileName}", s.render)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) render(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
profileName := chi.URLParam(request, "profileName")
|
||||||
|
if profileName == "" {
|
||||||
|
// compatibility with legacy versions
|
||||||
|
profileName = request.URL.Query().Get("profile")
|
||||||
|
}
|
||||||
|
var profile *Profile
|
||||||
|
if len(s.users) == 0 {
|
||||||
|
if profileName == "" {
|
||||||
|
profile = s.profile.DefaultProfile()
|
||||||
|
} else {
|
||||||
|
profile = s.profile.ProfileByName(profileName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
user := s.authorization(request)
|
||||||
|
if user == nil {
|
||||||
|
writer.WriteHeader(http.StatusUnauthorized)
|
||||||
|
s.accessLog(request, http.StatusUnauthorized, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(user.Profile) == 0 {
|
||||||
|
writer.WriteHeader(http.StatusNotFound)
|
||||||
|
s.accessLog(request, http.StatusNotFound, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if profileName == "" {
|
||||||
|
profileName = user.DefaultProfile
|
||||||
|
}
|
||||||
|
if profileName == "" {
|
||||||
|
profileName = user.Profile[0]
|
||||||
|
}
|
||||||
|
if !common.Contains(user.Profile, profileName) {
|
||||||
|
writer.WriteHeader(http.StatusNotFound)
|
||||||
|
s.accessLog(request, http.StatusNotFound, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
profile = s.profile.ProfileByName(profileName)
|
||||||
|
}
|
||||||
|
if profile == nil {
|
||||||
|
writer.WriteHeader(http.StatusNotFound)
|
||||||
|
s.accessLog(request, http.StatusNotFound, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
metadata := M.Detect(request.Header.Get("User-Agent"))
|
||||||
|
options, err := profile.Render(metadata)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error(E.Cause(err, "render options"))
|
||||||
|
render.Status(request, http.StatusInternalServerError)
|
||||||
|
render.PlainText(writer, request, err.Error())
|
||||||
|
s.accessLog(request, http.StatusInternalServerError, len(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
encoder := json.NewEncoder(&buffer)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
err = encoder.Encode(&options)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error(E.Cause(err, "marshal options"))
|
||||||
|
render.Status(request, http.StatusInternalServerError)
|
||||||
|
render.PlainText(writer, request, err.Error())
|
||||||
|
s.accessLog(request, http.StatusInternalServerError, len(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writer.Header().Set("Content-Type", "application/json")
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
writer.Write(buffer.Bytes())
|
||||||
|
s.accessLog(request, http.StatusOK, buffer.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) accessLog(request *http.Request, responseCode int, responseLen int) {
|
||||||
|
var userString string
|
||||||
|
if username, password, ok := request.BasicAuth(); ok {
|
||||||
|
if responseCode == http.StatusUnauthorized {
|
||||||
|
userString = username + ":" + password
|
||||||
|
} else {
|
||||||
|
userString = username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.logger.Debug("accepted ", request.RemoteAddr, " - ", userString, " \"", request.Method, " ", request.URL, " ", request.Proto, "\" ", responseCode, " ", responseLen, " \"", request.UserAgent(), "\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) authorization(request *http.Request) *option.User {
|
||||||
|
username, password, ok := request.BasicAuth()
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
users, loaded := s.userMap[username]
|
||||||
|
if !loaded {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, user := range users {
|
||||||
|
if user.Password == password {
|
||||||
|
return &user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
87
subscription/deduplication.go
Normal file
87
subscription/deduplication.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package subscription
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing-dns"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/common/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Deduplication(ctx context.Context, servers []option.Outbound) []option.Outbound {
|
||||||
|
resolveCtx := &resolveContext{
|
||||||
|
ctx: ctx,
|
||||||
|
dnsClient: dns.NewClient(dns.ClientOptions{
|
||||||
|
DisableExpire: true,
|
||||||
|
Logger: log.NewNOPFactory().Logger(),
|
||||||
|
}),
|
||||||
|
dnsTransport: common.Must1(dns.NewTLSTransport("google", ctx, N.SystemDialer, M.ParseSocksaddr("1.1.1.1"))),
|
||||||
|
}
|
||||||
|
uniqueServers := make([]netip.AddrPort, len(servers))
|
||||||
|
var (
|
||||||
|
resolveGroup task.Group
|
||||||
|
resultAccess sync.Mutex
|
||||||
|
)
|
||||||
|
for index, server := range servers {
|
||||||
|
currentIndex := index
|
||||||
|
currentServer := server
|
||||||
|
resolveGroup.Append0(func(ctx context.Context) error {
|
||||||
|
destination := resolveDestination(resolveCtx, currentServer)
|
||||||
|
if destination.IsValid() {
|
||||||
|
resultAccess.Lock()
|
||||||
|
uniqueServers[currentIndex] = destination
|
||||||
|
resultAccess.Unlock()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
resolveGroup.Concurrency(5)
|
||||||
|
_ = resolveGroup.Run(ctx)
|
||||||
|
}
|
||||||
|
uniqueServerMap := make(map[netip.AddrPort]bool)
|
||||||
|
var newServers []option.Outbound
|
||||||
|
for index, server := range servers {
|
||||||
|
destination := uniqueServers[index]
|
||||||
|
if destination.IsValid() {
|
||||||
|
if uniqueServerMap[destination] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
uniqueServerMap[destination] = true
|
||||||
|
}
|
||||||
|
newServers = append(newServers, server)
|
||||||
|
}
|
||||||
|
return newServers
|
||||||
|
}
|
||||||
|
|
||||||
|
type resolveContext struct {
|
||||||
|
ctx context.Context
|
||||||
|
dnsClient *dns.Client
|
||||||
|
dnsTransport dns.Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveDestination(ctx *resolveContext, server option.Outbound) netip.AddrPort {
|
||||||
|
rawOptions, err := server.RawOptions()
|
||||||
|
if err != nil {
|
||||||
|
return netip.AddrPort{}
|
||||||
|
}
|
||||||
|
serverOptionsWrapper, loaded := rawOptions.(option.ServerOptionsWrapper)
|
||||||
|
if !loaded {
|
||||||
|
return netip.AddrPort{}
|
||||||
|
}
|
||||||
|
serverOptions := serverOptionsWrapper.TakeServerOptions().Build()
|
||||||
|
if serverOptions.IsIP() {
|
||||||
|
return serverOptions.AddrPort()
|
||||||
|
}
|
||||||
|
if serverOptions.IsFqdn() {
|
||||||
|
addresses, lookupErr := ctx.dnsClient.Lookup(ctx.ctx, ctx.dnsTransport, serverOptions.Fqdn, dns.DomainStrategyPreferIPv4)
|
||||||
|
if lookupErr == nil && len(addresses) > 0 {
|
||||||
|
return netip.AddrPortFrom(addresses[0], serverOptions.Port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return netip.AddrPort{}
|
||||||
|
}
|
283
subscription/parser/clash.go
Normal file
283
subscription/parser/clash.go
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/format"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/adapter"
|
||||||
|
clash_outbound "github.com/Dreamacro/clash/adapter/outbound"
|
||||||
|
"github.com/Dreamacro/clash/common/structure"
|
||||||
|
"github.com/Dreamacro/clash/config"
|
||||||
|
"github.com/Dreamacro/clash/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseClashSubscription(content string) ([]option.Outbound, error) {
|
||||||
|
config, err := config.UnmarshalRawConfig([]byte(content))
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse clash config")
|
||||||
|
}
|
||||||
|
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true})
|
||||||
|
var outbounds []option.Outbound
|
||||||
|
for i, proxyMapping := range config.Proxy {
|
||||||
|
proxy, err := adapter.ParseProxy(proxyMapping)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse proxy ", i)
|
||||||
|
}
|
||||||
|
var outbound option.Outbound
|
||||||
|
outbound.Tag = proxy.Name()
|
||||||
|
switch proxy.Type() {
|
||||||
|
case constant.Shadowsocks:
|
||||||
|
ssOption := &clash_outbound.ShadowSocksOption{}
|
||||||
|
err = decoder.Decode(proxyMapping, ssOption)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
outbound.Type = C.TypeShadowsocks
|
||||||
|
outbound.ShadowsocksOptions = option.ShadowsocksOutboundOptions{
|
||||||
|
ServerOptions: option.ServerOptions{
|
||||||
|
Server: ssOption.Server,
|
||||||
|
ServerPort: uint16(ssOption.Port),
|
||||||
|
},
|
||||||
|
Password: ssOption.Password,
|
||||||
|
Method: clashShadowsocksCipher(ssOption.Cipher),
|
||||||
|
Plugin: clashPluginName(ssOption.Plugin),
|
||||||
|
PluginOptions: clashPluginOptions(ssOption.Plugin, ssOption.PluginOpts),
|
||||||
|
Network: clashNetworks(ssOption.UDP),
|
||||||
|
}
|
||||||
|
case constant.ShadowsocksR:
|
||||||
|
ssrOption := &clash_outbound.ShadowSocksROption{}
|
||||||
|
err = decoder.Decode(proxyMapping, ssrOption)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
outbound.Type = C.TypeShadowsocksR
|
||||||
|
outbound.ShadowsocksROptions = option.ShadowsocksROutboundOptions{
|
||||||
|
ServerOptions: option.ServerOptions{
|
||||||
|
Server: ssrOption.Server,
|
||||||
|
ServerPort: uint16(ssrOption.Port),
|
||||||
|
},
|
||||||
|
Password: ssrOption.Password,
|
||||||
|
Method: clashShadowsocksCipher(ssrOption.Cipher),
|
||||||
|
Protocol: ssrOption.Protocol,
|
||||||
|
ProtocolParam: ssrOption.ProtocolParam,
|
||||||
|
Obfs: ssrOption.Obfs,
|
||||||
|
ObfsParam: ssrOption.ObfsParam,
|
||||||
|
Network: clashNetworks(ssrOption.UDP),
|
||||||
|
}
|
||||||
|
case constant.Trojan:
|
||||||
|
trojanOption := &clash_outbound.TrojanOption{}
|
||||||
|
err = decoder.Decode(proxyMapping, trojanOption)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
outbound.Type = C.TypeTrojan
|
||||||
|
outbound.TrojanOptions = option.TrojanOutboundOptions{
|
||||||
|
ServerOptions: option.ServerOptions{
|
||||||
|
Server: trojanOption.Server,
|
||||||
|
ServerPort: uint16(trojanOption.Port),
|
||||||
|
},
|
||||||
|
Password: trojanOption.Password,
|
||||||
|
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||||
|
TLS: &option.OutboundTLSOptions{
|
||||||
|
Enabled: true,
|
||||||
|
ALPN: trojanOption.ALPN,
|
||||||
|
ServerName: trojanOption.SNI,
|
||||||
|
Insecure: trojanOption.SkipCertVerify,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Transport: clashTransport(trojanOption.Network, clash_outbound.HTTPOptions{}, clash_outbound.HTTP2Options{}, trojanOption.GrpcOpts, trojanOption.WSOpts),
|
||||||
|
Network: clashNetworks(trojanOption.UDP),
|
||||||
|
}
|
||||||
|
case constant.Vmess:
|
||||||
|
vmessOption := &clash_outbound.VmessOption{}
|
||||||
|
err = decoder.Decode(proxyMapping, vmessOption)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
outbound.Type = C.TypeVMess
|
||||||
|
outbound.VMessOptions = option.VMessOutboundOptions{
|
||||||
|
ServerOptions: option.ServerOptions{
|
||||||
|
Server: vmessOption.Server,
|
||||||
|
ServerPort: uint16(vmessOption.Port),
|
||||||
|
},
|
||||||
|
UUID: vmessOption.UUID,
|
||||||
|
Security: vmessOption.Cipher,
|
||||||
|
AlterId: vmessOption.AlterID,
|
||||||
|
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||||
|
TLS: &option.OutboundTLSOptions{
|
||||||
|
Enabled: vmessOption.TLS,
|
||||||
|
ServerName: vmessOption.ServerName,
|
||||||
|
Insecure: vmessOption.SkipCertVerify,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Transport: clashTransport(vmessOption.Network, vmessOption.HTTPOpts, vmessOption.HTTP2Opts, vmessOption.GrpcOpts, vmessOption.WSOpts),
|
||||||
|
Network: clashNetworks(vmessOption.UDP),
|
||||||
|
}
|
||||||
|
case constant.Socks5:
|
||||||
|
socks5Option := &clash_outbound.Socks5Option{}
|
||||||
|
err = decoder.Decode(proxyMapping, socks5Option)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if socks5Option.TLS {
|
||||||
|
// TODO: print warning
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
outbound.Type = C.TypeSOCKS
|
||||||
|
outbound.SocksOptions = option.SocksOutboundOptions{
|
||||||
|
ServerOptions: option.ServerOptions{
|
||||||
|
Server: socks5Option.Server,
|
||||||
|
ServerPort: uint16(socks5Option.Port),
|
||||||
|
},
|
||||||
|
Username: socks5Option.UserName,
|
||||||
|
Password: socks5Option.Password,
|
||||||
|
Network: clashNetworks(socks5Option.UDP),
|
||||||
|
}
|
||||||
|
case constant.Http:
|
||||||
|
httpOption := &clash_outbound.HttpOption{}
|
||||||
|
err = decoder.Decode(proxyMapping, httpOption)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if httpOption.TLS {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
outbound.Type = C.TypeHTTP
|
||||||
|
outbound.HTTPOptions = option.HTTPOutboundOptions{
|
||||||
|
ServerOptions: option.ServerOptions{
|
||||||
|
Server: httpOption.Server,
|
||||||
|
ServerPort: uint16(httpOption.Port),
|
||||||
|
},
|
||||||
|
Username: httpOption.UserName,
|
||||||
|
Password: httpOption.Password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outbounds = append(outbounds, outbound)
|
||||||
|
}
|
||||||
|
if len(outbounds) > 0 {
|
||||||
|
return outbounds, nil
|
||||||
|
}
|
||||||
|
return nil, E.New("no servers found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func clashShadowsocksCipher(cipher string) string {
|
||||||
|
switch cipher {
|
||||||
|
case "dummy":
|
||||||
|
return "none"
|
||||||
|
}
|
||||||
|
return cipher
|
||||||
|
}
|
||||||
|
|
||||||
|
func clashNetworks(udpEnabled bool) option.NetworkList {
|
||||||
|
if !udpEnabled {
|
||||||
|
return N.NetworkTCP
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func clashPluginName(plugin string) string {
|
||||||
|
switch plugin {
|
||||||
|
case "obfs":
|
||||||
|
return "obfs-local"
|
||||||
|
}
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
type shadowsocksPluginOptionsBuilder map[string]any
|
||||||
|
|
||||||
|
func (o shadowsocksPluginOptionsBuilder) Build() string {
|
||||||
|
var opts []string
|
||||||
|
for key, value := range o {
|
||||||
|
if value == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
opts = append(opts, format.ToString(key, "=", value))
|
||||||
|
}
|
||||||
|
return strings.Join(opts, ";")
|
||||||
|
}
|
||||||
|
|
||||||
|
func clashPluginOptions(plugin string, opts map[string]any) string {
|
||||||
|
options := shadowsocksPluginOptionsBuilder(opts)
|
||||||
|
switch plugin {
|
||||||
|
case "obfs":
|
||||||
|
options["mode"] = opts["mode"]
|
||||||
|
options["host"] = opts["host"]
|
||||||
|
case "v2ray-plugin":
|
||||||
|
options["mode"] = opts["mode"]
|
||||||
|
options["tls"] = opts["tls"]
|
||||||
|
options["host"] = opts["host"]
|
||||||
|
options["path"] = opts["path"]
|
||||||
|
}
|
||||||
|
return options.Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
func clashTransport(network string, httpOpts clash_outbound.HTTPOptions, h2Opts clash_outbound.HTTP2Options, grpcOpts clash_outbound.GrpcOptions, wsOpts clash_outbound.WSOptions) *option.V2RayTransportOptions {
|
||||||
|
switch network {
|
||||||
|
case "http":
|
||||||
|
var headers map[string]option.Listable[string]
|
||||||
|
for key, values := range httpOpts.Headers {
|
||||||
|
if headers == nil {
|
||||||
|
headers = make(map[string]option.Listable[string])
|
||||||
|
}
|
||||||
|
headers[key] = values
|
||||||
|
}
|
||||||
|
return &option.V2RayTransportOptions{
|
||||||
|
Type: C.V2RayTransportTypeHTTP,
|
||||||
|
HTTPOptions: option.V2RayHTTPOptions{
|
||||||
|
Method: httpOpts.Method,
|
||||||
|
Path: clashStringList(httpOpts.Path),
|
||||||
|
Headers: headers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case "h2":
|
||||||
|
return &option.V2RayTransportOptions{
|
||||||
|
Type: C.V2RayTransportTypeHTTP,
|
||||||
|
HTTPOptions: option.V2RayHTTPOptions{
|
||||||
|
Path: h2Opts.Path,
|
||||||
|
Host: h2Opts.Host,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case "grpc":
|
||||||
|
return &option.V2RayTransportOptions{
|
||||||
|
Type: C.V2RayTransportTypeGRPC,
|
||||||
|
GRPCOptions: option.V2RayGRPCOptions{
|
||||||
|
ServiceName: grpcOpts.GrpcServiceName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case "ws":
|
||||||
|
var headers map[string]option.Listable[string]
|
||||||
|
for key, value := range wsOpts.Headers {
|
||||||
|
if headers == nil {
|
||||||
|
headers = make(map[string]option.Listable[string])
|
||||||
|
}
|
||||||
|
headers[key] = []string{value}
|
||||||
|
}
|
||||||
|
return &option.V2RayTransportOptions{
|
||||||
|
Type: C.V2RayTransportTypeWebsocket,
|
||||||
|
WebsocketOptions: option.V2RayWebsocketOptions{
|
||||||
|
Path: wsOpts.Path,
|
||||||
|
Headers: headers,
|
||||||
|
MaxEarlyData: uint32(wsOpts.MaxEarlyData),
|
||||||
|
EarlyDataHeaderName: wsOpts.EarlyDataHeaderName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clashStringList(list []string) string {
|
||||||
|
if len(list) > 0 {
|
||||||
|
return list[0]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
22
subscription/parser/link.go
Normal file
22
subscription/parser/link.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseSubscriptionLink(link string) (option.Outbound, error) {
|
||||||
|
schemeIndex := strings.Index(link, "://")
|
||||||
|
if schemeIndex == -1 {
|
||||||
|
return option.Outbound{}, E.New("not a link")
|
||||||
|
}
|
||||||
|
scheme := link[:schemeIndex]
|
||||||
|
switch scheme {
|
||||||
|
case "ss":
|
||||||
|
return ParseShadowsocksLink(link)
|
||||||
|
default:
|
||||||
|
return option.Outbound{}, E.New("unsupported scheme: ", scheme)
|
||||||
|
}
|
||||||
|
}
|
67
subscription/parser/link_shadowsocks.go
Normal file
67
subscription/parser/link_shadowsocks.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseShadowsocksLink(link string) (option.Outbound, error) {
|
||||||
|
linkURL, err := url.Parse(link)
|
||||||
|
if err != nil {
|
||||||
|
return option.Outbound{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if linkURL.User == nil {
|
||||||
|
return option.Outbound{}, E.New("missing user info")
|
||||||
|
}
|
||||||
|
|
||||||
|
var options option.ShadowsocksOutboundOptions
|
||||||
|
options.ServerOptions.Server = linkURL.Host
|
||||||
|
options.ServerOptions.ServerPort = portFromString(linkURL.Port())
|
||||||
|
if password, _ := linkURL.User.Password(); password != "" {
|
||||||
|
options.Method = linkURL.User.Username()
|
||||||
|
options.Password = password
|
||||||
|
} else {
|
||||||
|
userAndPassword, _ := decodeBase64URLSafe(linkURL.User.Username())
|
||||||
|
userAndPasswordParts := strings.Split(userAndPassword, ":")
|
||||||
|
if len(userAndPasswordParts) != 2 {
|
||||||
|
return option.Outbound{}, E.New("bad user info")
|
||||||
|
}
|
||||||
|
options.Method = userAndPasswordParts[0]
|
||||||
|
options.Password = userAndPasswordParts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin := linkURL.Query().Get("plugin")
|
||||||
|
options.Plugin = shadowsocksPluginName(plugin)
|
||||||
|
options.PluginOptions = shadowsocksPluginOptions(plugin)
|
||||||
|
|
||||||
|
var outbound option.Outbound
|
||||||
|
outbound.Type = C.TypeShadowsocks
|
||||||
|
outbound.Tag = linkURL.Fragment
|
||||||
|
outbound.ShadowsocksOptions = options
|
||||||
|
return outbound, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func portFromString(portString string) uint16 {
|
||||||
|
port, _ := strconv.ParseUint(portString, 10, 16)
|
||||||
|
return uint16(port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func shadowsocksPluginName(plugin string) string {
|
||||||
|
if index := strings.Index(plugin, ";"); index != -1 {
|
||||||
|
return plugin[:index]
|
||||||
|
}
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
func shadowsocksPluginOptions(plugin string) string {
|
||||||
|
if index := strings.Index(plugin, ";"); index != -1 {
|
||||||
|
return plugin[index+1:]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
25
subscription/parser/parser.go
Normal file
25
subscription/parser/parser.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
var subscriptionParsers = []func(string) ([]option.Outbound, error){
|
||||||
|
ParseBoxSubscription,
|
||||||
|
ParseClashSubscription,
|
||||||
|
ParseSIP008Subscription,
|
||||||
|
ParseRawSubscription,
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseSubscription(content string) ([]option.Outbound, error) {
|
||||||
|
var pErr error
|
||||||
|
for _, parser := range subscriptionParsers {
|
||||||
|
servers, err := parser(content)
|
||||||
|
if len(servers) > 0 {
|
||||||
|
return servers, nil
|
||||||
|
}
|
||||||
|
pErr = E.Errors(pErr, err)
|
||||||
|
}
|
||||||
|
return nil, E.Cause(pErr, "no servers found")
|
||||||
|
}
|
43
subscription/parser/raw.go
Normal file
43
subscription/parser/raw.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseRawSubscription(content string) ([]option.Outbound, error) {
|
||||||
|
if base64Content, err := decodeBase64URLSafe(content); err == nil {
|
||||||
|
servers, _ := parseRawSubscription(base64Content)
|
||||||
|
if len(servers) > 0 {
|
||||||
|
return servers, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parseRawSubscription(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRawSubscription(content string) ([]option.Outbound, error) {
|
||||||
|
var servers []option.Outbound
|
||||||
|
content = strings.ReplaceAll(content, "\r\n", "\n")
|
||||||
|
linkList := strings.Split(content, "\n")
|
||||||
|
for _, linkLine := range linkList {
|
||||||
|
if server, err := ParseSubscriptionLink(linkLine); err == nil {
|
||||||
|
servers = append(servers, server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(servers) == 0 {
|
||||||
|
return nil, E.New("no servers found")
|
||||||
|
}
|
||||||
|
return servers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeBase64URLSafe(content string) (string, error) {
|
||||||
|
content = strings.ReplaceAll(content, " ", "-")
|
||||||
|
content = strings.ReplaceAll(content, "/", "_")
|
||||||
|
content = strings.ReplaceAll(content, "+", "-")
|
||||||
|
content = strings.ReplaceAll(content, "=", "")
|
||||||
|
result, err := base64.StdEncoding.DecodeString(content)
|
||||||
|
return string(result), err
|
||||||
|
}
|
28
subscription/parser/sing_box.go
Normal file
28
subscription/parser/sing_box.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseBoxSubscription(content string) ([]option.Outbound, error) {
|
||||||
|
options, err := json.UnmarshalExtended[option.Options]([]byte(content))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
options.Outbounds = common.Filter(options.Outbounds, func(it option.Outbound) bool {
|
||||||
|
switch it.Type {
|
||||||
|
case C.TypeDirect, C.TypeBlock, C.TypeDNS, C.TypeSelector, C.TypeURLTest:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if len(options.Outbounds) == 0 {
|
||||||
|
return nil, E.New("no servers found")
|
||||||
|
}
|
||||||
|
return options.Outbounds, nil
|
||||||
|
}
|
51
subscription/parser/sip008.go
Normal file
51
subscription/parser/sip008.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ShadowsocksDocument struct {
|
||||||
|
Version int `json:"version"`
|
||||||
|
Servers []ShadowsocksServerDocument `json:"servers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShadowsocksServerDocument struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Remarks string `json:"remarks"`
|
||||||
|
Server string `json:"server"`
|
||||||
|
ServerPort int `json:"server_port"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
Plugin string `json:"plugin"`
|
||||||
|
PluginOpts string `json:"plugin_opts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseSIP008Subscription(content string) ([]option.Outbound, error) {
|
||||||
|
var document ShadowsocksDocument
|
||||||
|
err := json.Unmarshal([]byte(content), &document)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse SIP008 document")
|
||||||
|
}
|
||||||
|
|
||||||
|
var servers []option.Outbound
|
||||||
|
for _, server := range document.Servers {
|
||||||
|
servers = append(servers, option.Outbound{
|
||||||
|
Type: C.TypeShadowsocks,
|
||||||
|
Tag: server.Remarks,
|
||||||
|
ShadowsocksOptions: option.ShadowsocksOutboundOptions{
|
||||||
|
ServerOptions: option.ServerOptions{
|
||||||
|
Server: server.Server,
|
||||||
|
ServerPort: uint16(server.ServerPort),
|
||||||
|
},
|
||||||
|
Password: server.Password,
|
||||||
|
Method: server.Method,
|
||||||
|
Plugin: server.Plugin,
|
||||||
|
PluginOptions: server.PluginOpts,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return servers, nil
|
||||||
|
}
|
150
subscription/process.go
Normal file
150
subscription/process.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
package subscription
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/serenity/option"
|
||||||
|
boxOption "github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProcessOptions struct {
|
||||||
|
option.OutboundProcessOptions
|
||||||
|
filter []*regexp.Regexp
|
||||||
|
exclude []*regexp.Regexp
|
||||||
|
rename []*Rename
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rename struct {
|
||||||
|
From *regexp.Regexp
|
||||||
|
To string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProcessOptions(options option.OutboundProcessOptions) (*ProcessOptions, error) {
|
||||||
|
var (
|
||||||
|
filter []*regexp.Regexp
|
||||||
|
exclude []*regexp.Regexp
|
||||||
|
rename []*Rename
|
||||||
|
)
|
||||||
|
for regexIndex, it := range options.Filter {
|
||||||
|
regex, err := regexp.Compile(it)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse filter[", regexIndex, "]")
|
||||||
|
}
|
||||||
|
filter = append(filter, regex)
|
||||||
|
}
|
||||||
|
for regexIndex, it := range options.Exclude {
|
||||||
|
regex, err := regexp.Compile(it)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse exclude[", regexIndex, "]")
|
||||||
|
}
|
||||||
|
exclude = append(exclude, regex)
|
||||||
|
}
|
||||||
|
if options.Rename != nil {
|
||||||
|
for renameIndex, entry := range options.Rename.Entries() {
|
||||||
|
regex, err := regexp.Compile(entry.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse rename[", renameIndex, "]: parse ", entry.Key)
|
||||||
|
}
|
||||||
|
rename = append(rename, &Rename{
|
||||||
|
From: regex,
|
||||||
|
To: entry.Value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &ProcessOptions{
|
||||||
|
OutboundProcessOptions: options,
|
||||||
|
filter: filter,
|
||||||
|
exclude: exclude,
|
||||||
|
rename: rename,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ProcessOptions) Process(outbounds []boxOption.Outbound) []boxOption.Outbound {
|
||||||
|
newOutbounds := make([]boxOption.Outbound, 0, len(outbounds))
|
||||||
|
renameResult := make(map[string]string)
|
||||||
|
for _, outbound := range outbounds {
|
||||||
|
if len(o.filter) > 0 {
|
||||||
|
if !common.Any(o.filter, func(it *regexp.Regexp) bool {
|
||||||
|
return it.MatchString(outbound.Tag)
|
||||||
|
}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(o.FilterOutboundType) > 0 {
|
||||||
|
if !common.Contains(o.FilterOutboundType, outbound.Type) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(o.exclude) > 0 {
|
||||||
|
if common.Any(o.exclude, func(it *regexp.Regexp) bool {
|
||||||
|
return it.MatchString(outbound.Tag)
|
||||||
|
}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(o.ExcludeOutboundType) > 0 {
|
||||||
|
if common.Contains(o.ExcludeOutboundType, outbound.Type) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
originTag := outbound.Tag
|
||||||
|
if len(o.rename) > 0 {
|
||||||
|
for _, rename := range o.rename {
|
||||||
|
outbound.Tag = rename.From.ReplaceAllString(outbound.Tag, rename.To)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if o.RemoveEmoji {
|
||||||
|
outbound.Tag = removeEmojis(outbound.Tag)
|
||||||
|
}
|
||||||
|
outbound.Tag = strings.TrimSpace(outbound.Tag)
|
||||||
|
if originTag != outbound.Tag {
|
||||||
|
renameResult[originTag] = outbound.Tag
|
||||||
|
}
|
||||||
|
newOutbounds = append(newOutbounds, outbound)
|
||||||
|
}
|
||||||
|
if len(renameResult) > 0 {
|
||||||
|
for i, outbound := range newOutbounds {
|
||||||
|
rawOptions, err := outbound.RawOptions()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if dialerOptionsWrapper, containsDialerOptions := rawOptions.(boxOption.DialerOptionsWrapper); containsDialerOptions {
|
||||||
|
dialerOptions := dialerOptionsWrapper.TakeDialerOptions()
|
||||||
|
if dialerOptions.Detour == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newTag, loaded := renameResult[dialerOptions.Detour]
|
||||||
|
if !loaded {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dialerOptions.Detour = newTag
|
||||||
|
dialerOptionsWrapper.ReplaceDialerOptions(dialerOptions)
|
||||||
|
newOutbounds[i] = outbound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newOutbounds
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeEmojis(s string) string {
|
||||||
|
var runes []rune
|
||||||
|
for _, r := range s {
|
||||||
|
if !(r >= 0x1F600 && r <= 0x1F64F || // Emoticons
|
||||||
|
r >= 0x1F300 && r <= 0x1F5FF || // Symbols & Pictographs
|
||||||
|
r >= 0x1F680 && r <= 0x1F6FF || // Transport & Map Symbols
|
||||||
|
r >= 0x1F1E0 && r <= 0x1F1FF || // Flags
|
||||||
|
r >= 0x2600 && r <= 0x26FF || // Misc symbols
|
||||||
|
r >= 0x2700 && r <= 0x27BF || // Dingbats
|
||||||
|
r >= 0xFE00 && r <= 0xFE0F || // Variation Selectors
|
||||||
|
r >= 0x1F900 && r <= 0x1F9FF || // Supplemental Symbols and Pictographs
|
||||||
|
r >= 0x1F018 && r <= 0x1F270 || // Various asian characters
|
||||||
|
r >= 0x238C && r <= 0x2454 || // Misc items
|
||||||
|
r >= 0x20D0 && r <= 0x20FF) { // Combining Diacritical Marks for Symbols
|
||||||
|
runes = append(runes, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(runes)
|
||||||
|
}
|
211
subscription/subscription.go
Normal file
211
subscription/subscription.go
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
package subscription
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/serenity/common/cachefile"
|
||||||
|
C "github.com/sagernet/serenity/constant"
|
||||||
|
"github.com/sagernet/serenity/option"
|
||||||
|
"github.com/sagernet/serenity/subscription/parser"
|
||||||
|
boxOption "github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
logger logger.Logger
|
||||||
|
cacheFile *cachefile.CacheFile
|
||||||
|
subscriptions []*Subscription
|
||||||
|
updateInterval time.Duration
|
||||||
|
updateTicker *time.Ticker
|
||||||
|
httpClient http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type Subscription struct {
|
||||||
|
option.Subscription
|
||||||
|
rawServers []boxOption.Outbound
|
||||||
|
processes []*ProcessOptions
|
||||||
|
Servers []boxOption.Outbound
|
||||||
|
LastUpdated time.Time
|
||||||
|
LastEtag string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSubscriptionManager(ctx context.Context, logger logger.Logger, cacheFile *cachefile.CacheFile, rawSubscriptions []option.Subscription) (*Manager, error) {
|
||||||
|
var (
|
||||||
|
subscriptions []*Subscription
|
||||||
|
interval time.Duration
|
||||||
|
)
|
||||||
|
for index, subscription := range rawSubscriptions {
|
||||||
|
if subscription.Name == "" {
|
||||||
|
return nil, E.New("initialize subscription[", index, "]: missing name")
|
||||||
|
}
|
||||||
|
var processes []*ProcessOptions
|
||||||
|
if interval == 0 || time.Duration(subscription.UpdateInterval) < interval {
|
||||||
|
interval = time.Duration(subscription.UpdateInterval)
|
||||||
|
}
|
||||||
|
for processIndex, process := range subscription.Process {
|
||||||
|
processOptions, err := NewProcessOptions(process)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initialize subscription[", subscription.Name, "]: parse process[", processIndex, "]")
|
||||||
|
}
|
||||||
|
processes = append(processes, processOptions)
|
||||||
|
}
|
||||||
|
subscriptions = append(subscriptions, &Subscription{
|
||||||
|
Subscription: subscription,
|
||||||
|
processes: processes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if interval == 0 {
|
||||||
|
interval = option.DefaultSubscriptionUpdateInterval
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
return &Manager{
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
logger: logger,
|
||||||
|
cacheFile: cacheFile,
|
||||||
|
subscriptions: subscriptions,
|
||||||
|
updateInterval: interval,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Start() error {
|
||||||
|
for _, subscription := range m.subscriptions {
|
||||||
|
savedSubscription := m.cacheFile.LoadSubscription(subscription.Name)
|
||||||
|
if savedSubscription != nil {
|
||||||
|
subscription.rawServers = savedSubscription.Content
|
||||||
|
subscription.LastUpdated = savedSubscription.LastUpdated
|
||||||
|
subscription.LastEtag = savedSubscription.LastEtag
|
||||||
|
m.processSubscription(subscription, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) processSubscription(s *Subscription, onUpdate bool) {
|
||||||
|
servers := s.rawServers
|
||||||
|
for _, process := range s.processes {
|
||||||
|
servers = process.Process(servers)
|
||||||
|
}
|
||||||
|
if s.DeDuplication {
|
||||||
|
originLen := len(servers)
|
||||||
|
servers = Deduplication(m.ctx, servers)
|
||||||
|
if onUpdate && originLen != len(servers) {
|
||||||
|
m.logger.Info("excluded ", originLen-len(servers), " duplicated servers in ", s.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Servers = servers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) PostStart() error {
|
||||||
|
m.updateAll()
|
||||||
|
m.updateTicker = time.NewTicker(m.updateInterval)
|
||||||
|
go m.loopUpdate()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Close() error {
|
||||||
|
if m.updateTicker != nil {
|
||||||
|
m.updateTicker.Stop()
|
||||||
|
}
|
||||||
|
m.cancel()
|
||||||
|
m.httpClient.CloseIdleConnections()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Subscriptions() []*Subscription {
|
||||||
|
return m.subscriptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) loopUpdate() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-m.updateTicker.C:
|
||||||
|
m.updateAll()
|
||||||
|
case <-m.ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) updateAll() {
|
||||||
|
for _, subscription := range m.subscriptions {
|
||||||
|
if time.Since(subscription.LastUpdated) < m.updateInterval {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err := m.update(subscription)
|
||||||
|
if err != nil {
|
||||||
|
m.logger.Error(E.Cause(err, "update subscription ", subscription.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) update(subscription *Subscription) error {
|
||||||
|
request, err := http.NewRequest("GET", subscription.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if subscription.UserAgent != "" {
|
||||||
|
request.Header.Set("User-Agent", subscription.UserAgent)
|
||||||
|
} else {
|
||||||
|
request.Header.Set("User-Agent", F.ToString("serenity/", C.Version, " (sing-box ", C.CoreVersion(), "; Clash compatible)"))
|
||||||
|
}
|
||||||
|
if subscription.LastEtag != "" {
|
||||||
|
request.Header.Set("If-None-Match", subscription.LastEtag)
|
||||||
|
}
|
||||||
|
response, err := m.httpClient.Do(request.WithContext(m.ctx))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch response.StatusCode {
|
||||||
|
case http.StatusOK:
|
||||||
|
case http.StatusNotModified:
|
||||||
|
subscription.LastUpdated = time.Now()
|
||||||
|
err = m.cacheFile.StoreSubscription(subscription.Name, &cachefile.Subscription{
|
||||||
|
Content: subscription.rawServers,
|
||||||
|
LastUpdated: subscription.LastUpdated,
|
||||||
|
LastEtag: subscription.LastEtag,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.logger.Info("updated subscription ", subscription.Name, ": not modified")
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return E.New("unexpected status: ", response.Status)
|
||||||
|
}
|
||||||
|
content, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
response.Body.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rawServers, err := parser.ParseSubscription(string(content))
|
||||||
|
if err != nil {
|
||||||
|
response.Body.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
response.Body.Close()
|
||||||
|
subscription.rawServers = rawServers
|
||||||
|
m.processSubscription(subscription, true)
|
||||||
|
eTagHeader := response.Header.Get("Etag")
|
||||||
|
if eTagHeader != "" {
|
||||||
|
subscription.LastEtag = eTagHeader
|
||||||
|
}
|
||||||
|
subscription.LastUpdated = time.Now()
|
||||||
|
err = m.cacheFile.StoreSubscription(subscription.Name, &cachefile.Subscription{
|
||||||
|
Content: subscription.rawServers,
|
||||||
|
LastUpdated: subscription.LastUpdated,
|
||||||
|
LastEtag: subscription.LastEtag,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.logger.Info("updated subscription ", subscription.Name, ": ", len(subscription.rawServers), " servers")
|
||||||
|
return nil
|
||||||
|
}
|
16
template/filter/filter.go
Normal file
16
template/filter/filter.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/serenity/common/metadata"
|
||||||
|
boxOption "github.com/sagernet/sing-box/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OptionsFilter func(metadata metadata.Metadata, options *boxOption.Options)
|
||||||
|
|
||||||
|
var filters []OptionsFilter
|
||||||
|
|
||||||
|
func Filter(metadata metadata.Metadata, options *boxOption.Options) {
|
||||||
|
for _, filter := range filters {
|
||||||
|
filter(metadata, options)
|
||||||
|
}
|
||||||
|
}
|
90
template/filter/filter_170.go
Normal file
90
template/filter/filter_170.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/serenity/common/metadata"
|
||||||
|
"github.com/sagernet/serenity/common/semver"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
filters = append(filters, filter170)
|
||||||
|
}
|
||||||
|
|
||||||
|
func filter170(metadata metadata.Metadata, options *option.Options) {
|
||||||
|
if metadata.Version == nil || metadata.Version.GreaterThanOrEqual(semver.Version{Major: 1, Minor: 7}) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newInbounds := make([]option.Inbound, 0, len(options.Inbounds))
|
||||||
|
for _, inbound := range options.Inbounds {
|
||||||
|
switch inbound.Type {
|
||||||
|
case C.TypeTun:
|
||||||
|
inbound.TunOptions = filter170Tun(inbound.TunOptions)
|
||||||
|
inbound.TunOptions.InboundOptions = filter170InboundOptions(inbound.TunOptions.InboundOptions)
|
||||||
|
case C.TypeRedirect:
|
||||||
|
inbound.RedirectOptions.InboundOptions = filter170InboundOptions(inbound.RedirectOptions.InboundOptions)
|
||||||
|
case C.TypeTProxy:
|
||||||
|
inbound.TProxyOptions.InboundOptions = filter170InboundOptions(inbound.TProxyOptions.InboundOptions)
|
||||||
|
case C.TypeDirect:
|
||||||
|
inbound.DirectOptions.InboundOptions = filter170InboundOptions(inbound.DirectOptions.InboundOptions)
|
||||||
|
case C.TypeSOCKS:
|
||||||
|
inbound.SocksOptions.InboundOptions = filter170InboundOptions(inbound.SocksOptions.InboundOptions)
|
||||||
|
case C.TypeHTTP:
|
||||||
|
inbound.HTTPOptions.InboundOptions = filter170InboundOptions(inbound.HTTPOptions.InboundOptions)
|
||||||
|
case C.TypeMixed:
|
||||||
|
inbound.MixedOptions.InboundOptions = filter170InboundOptions(inbound.MixedOptions.InboundOptions)
|
||||||
|
case C.TypeShadowsocks:
|
||||||
|
inbound.ShadowsocksOptions.InboundOptions = filter170InboundOptions(inbound.ShadowsocksOptions.InboundOptions)
|
||||||
|
inbound.ShadowsocksOptions.Multiplex = nil
|
||||||
|
case C.TypeVMess:
|
||||||
|
inbound.VMessOptions.InboundOptions = filter170InboundOptions(inbound.VMessOptions.InboundOptions)
|
||||||
|
inbound.VMessOptions.Multiplex = nil
|
||||||
|
case C.TypeTrojan:
|
||||||
|
inbound.TrojanOptions.InboundOptions = filter170InboundOptions(inbound.TrojanOptions.InboundOptions)
|
||||||
|
inbound.TrojanOptions.Multiplex = nil
|
||||||
|
case C.TypeNaive:
|
||||||
|
inbound.NaiveOptions.InboundOptions = filter170InboundOptions(inbound.NaiveOptions.InboundOptions)
|
||||||
|
case C.TypeHysteria:
|
||||||
|
inbound.HysteriaOptions.InboundOptions = filter170InboundOptions(inbound.HysteriaOptions.InboundOptions)
|
||||||
|
case C.TypeShadowTLS:
|
||||||
|
inbound.ShadowTLSOptions.InboundOptions = filter170InboundOptions(inbound.ShadowTLSOptions.InboundOptions)
|
||||||
|
case C.TypeVLESS:
|
||||||
|
inbound.VLESSOptions.InboundOptions = filter170InboundOptions(inbound.VLESSOptions.InboundOptions)
|
||||||
|
inbound.VLESSOptions.Multiplex = nil
|
||||||
|
case C.TypeTUIC:
|
||||||
|
inbound.TUICOptions.InboundOptions = filter170InboundOptions(inbound.TUICOptions.InboundOptions)
|
||||||
|
case C.TypeHysteria2:
|
||||||
|
inbound.Hysteria2Options.InboundOptions = filter170InboundOptions(inbound.Hysteria2Options.InboundOptions)
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newInbounds = append(newInbounds, inbound)
|
||||||
|
}
|
||||||
|
options.Inbounds = newInbounds
|
||||||
|
if options.Route != nil {
|
||||||
|
options.Route.Rules = common.Filter(options.Route.Rules, filter170Rule)
|
||||||
|
}
|
||||||
|
if options.DNS != nil {
|
||||||
|
options.DNS.Rules = common.Filter(options.DNS.Rules, filter170DNSRule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func filter170Tun(options option.TunInboundOptions) option.TunInboundOptions {
|
||||||
|
options.Inet4RouteExcludeAddress = nil
|
||||||
|
options.Inet6RouteExcludeAddress = nil
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func filter170InboundOptions(options option.InboundOptions) option.InboundOptions {
|
||||||
|
options.UDPDisableDomainUnmapping = false
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func filter170Rule(it option.Rule) bool {
|
||||||
|
return !hasRule([]option.Rule{it}, isWIFIRule)
|
||||||
|
}
|
||||||
|
|
||||||
|
func filter170DNSRule(it option.DNSRule) bool {
|
||||||
|
return !hasDNSRule([]option.DNSRule{it}, isWIFIDNSRule)
|
||||||
|
}
|
58
template/filter/filter_180.go
Normal file
58
template/filter/filter_180.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/serenity/common/metadata"
|
||||||
|
"github.com/sagernet/serenity/common/semver"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
filters = append(filters, filter180)
|
||||||
|
}
|
||||||
|
|
||||||
|
func filter180(metadata metadata.Metadata, options *option.Options) {
|
||||||
|
if metadata.Version == nil || metadata.Version.GreaterThanOrEqual(semver.ParseVersion("1.8.0-alpha.10")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for index, outbound := range options.Outbounds {
|
||||||
|
switch outbound.Type {
|
||||||
|
case C.TypeURLTest:
|
||||||
|
options.Outbounds[index].URLTestOptions = filter180a10URLTest(outbound.URLTestOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if metadata.Version.GreaterThanOrEqual(semver.ParseVersion("1.8.0-alpha.5")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
options.Route.RuleSet = nil
|
||||||
|
if options.Route != nil {
|
||||||
|
options.Route.Rules = common.Filter(options.Route.Rules, filter180Rule)
|
||||||
|
}
|
||||||
|
if options.DNS != nil {
|
||||||
|
options.DNS.Rules = common.Filter(options.DNS.Rules, filter180DNSRule)
|
||||||
|
}
|
||||||
|
if metadata.Version.GreaterThanOrEqual(semver.ParseVersion("1.8.0-alpha.1")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if options.Route != nil {
|
||||||
|
options.Route.Rules = common.Filter(options.Route.Rules, filter180a5Rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func filter180Rule(it option.Rule) bool {
|
||||||
|
return !hasRule([]option.Rule{it}, isRuleSetRule)
|
||||||
|
}
|
||||||
|
|
||||||
|
func filter180DNSRule(it option.DNSRule) bool {
|
||||||
|
return !hasDNSRule([]option.DNSRule{it}, isRuleSetDNSRule)
|
||||||
|
}
|
||||||
|
|
||||||
|
func filter180a5Rule(it option.Rule) bool {
|
||||||
|
return !hasRule([]option.Rule{it}, isIPIsPrivateRule)
|
||||||
|
}
|
||||||
|
|
||||||
|
func filter180a10URLTest(options option.URLTestOutboundOptions) option.URLTestOutboundOptions {
|
||||||
|
options.IdleTimeout = 0
|
||||||
|
return options
|
||||||
|
}
|
58
template/filter/filter_rule.go
Normal file
58
template/filter/filter_rule.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool {
|
||||||
|
for _, rule := range rules {
|
||||||
|
switch rule.Type {
|
||||||
|
case C.RuleTypeDefault:
|
||||||
|
if cond(rule.DefaultOptions) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case C.RuleTypeLogical:
|
||||||
|
if hasRule(rule.LogicalOptions.Rules, cond) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bool) bool {
|
||||||
|
for _, rule := range rules {
|
||||||
|
switch rule.Type {
|
||||||
|
case C.RuleTypeDefault:
|
||||||
|
if cond(rule.DefaultOptions) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case C.RuleTypeLogical:
|
||||||
|
if hasDNSRule(rule.LogicalOptions.Rules, cond) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWIFIRule(rule option.DefaultRule) bool {
|
||||||
|
return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWIFIDNSRule(rule option.DefaultDNSRule) bool {
|
||||||
|
return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRuleSetRule(rule option.DefaultRule) bool {
|
||||||
|
return len(rule.RuleSet) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRuleSetDNSRule(rule option.DefaultDNSRule) bool {
|
||||||
|
return len(rule.RuleSet) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIPIsPrivateRule(rule option.DefaultRule) bool {
|
||||||
|
return rule.IPIsPrivate
|
||||||
|
}
|
80
template/manager.go
Normal file
80
template/manager.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/sagernet/serenity/option"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
ctx context.Context
|
||||||
|
logger logger.Logger
|
||||||
|
templates []*Template
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager(ctx context.Context, logger logger.Logger, rawTemplates []option.Template) (*Manager, error) {
|
||||||
|
var templates []*Template
|
||||||
|
for templateIndex, template := range rawTemplates {
|
||||||
|
if template.Name == "" {
|
||||||
|
return nil, E.New("initialize template[", templateIndex, "]: missing name")
|
||||||
|
}
|
||||||
|
var groups []*ExtraGroup
|
||||||
|
for groupIndex, group := range template.ExtraGroups {
|
||||||
|
if group.Tag == "" {
|
||||||
|
return nil, E.New("initialize template[", template.Name, "]: extra_group[", groupIndex, "]: missing tag")
|
||||||
|
}
|
||||||
|
switch group.Type {
|
||||||
|
case C.TypeSelector, C.TypeURLTest:
|
||||||
|
case "":
|
||||||
|
return nil, E.New("initialize template[", template.Name, "]: extra_group[", group.Tag, "]: missing type")
|
||||||
|
default:
|
||||||
|
return nil, E.New("initialize template[", template.Name, "]: extra_group[", group.Tag, "]: invalid group type: ", group.Type)
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
filter []*regexp.Regexp
|
||||||
|
exclude []*regexp.Regexp
|
||||||
|
)
|
||||||
|
for filterIndex, it := range group.Filter {
|
||||||
|
regex, err := regexp.Compile(it)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initialize template[", template.Name, "]: parse extra_group[", group.Tag, "]: parse filter[", filterIndex, "]: ", it)
|
||||||
|
}
|
||||||
|
filter = append(filter, regex)
|
||||||
|
}
|
||||||
|
for excludeIndex, it := range group.Exclude {
|
||||||
|
regex, err := regexp.Compile(it)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initialize template[", template.Name, "]: parse extra_group[", group.Tag, "]: parse exclude[", excludeIndex, "]: ", it)
|
||||||
|
}
|
||||||
|
exclude = append(exclude, regex)
|
||||||
|
}
|
||||||
|
groups = append(groups, &ExtraGroup{
|
||||||
|
ExtraGroup: group,
|
||||||
|
filter: filter,
|
||||||
|
exclude: exclude,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
templates = append(templates, &Template{
|
||||||
|
Template: template,
|
||||||
|
groups: groups,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return &Manager{
|
||||||
|
ctx: ctx,
|
||||||
|
logger: logger,
|
||||||
|
templates: templates,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) TemplateByName(name string) *Template {
|
||||||
|
for _, template := range m.templates {
|
||||||
|
if template.Name == name {
|
||||||
|
return template
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
186
template/render_dns.go
Normal file
186
template/render_dns.go
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
M "github.com/sagernet/serenity/common/metadata"
|
||||||
|
"github.com/sagernet/serenity/common/semver"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing-dns"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
BM "github.com/sagernet/sing/common/metadata"
|
||||||
|
|
||||||
|
mDNS "github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *Template) renderDNS(metadata M.Metadata, options *option.Options) error {
|
||||||
|
var domainStrategy option.DomainStrategy
|
||||||
|
if t.DomainStrategy != option.DomainStrategy(dns.DomainStrategyAsIS) {
|
||||||
|
domainStrategy = t.DomainStrategy
|
||||||
|
} else {
|
||||||
|
domainStrategy = option.DomainStrategy(dns.DomainStrategyPreferIPv4)
|
||||||
|
}
|
||||||
|
options.DNS = &option.DNSOptions{
|
||||||
|
ReverseMapping: !t.DisableTrafficBypass && !metadata.Platform.IsApple(),
|
||||||
|
DNSClientOptions: option.DNSClientOptions{
|
||||||
|
Strategy: domainStrategy,
|
||||||
|
IndependentCache: t.EnableFakeIP,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dnsDefault := t.DNSDefault
|
||||||
|
if dnsDefault == "" {
|
||||||
|
dnsDefault = DefaultDNS
|
||||||
|
}
|
||||||
|
dnsLocal := t.DNSLocal
|
||||||
|
if dnsLocal == "" {
|
||||||
|
dnsLocal = DefaultDNSLocal
|
||||||
|
}
|
||||||
|
defaultDNSOptions := option.DNSServerOptions{
|
||||||
|
Tag: DNSDefaultTag,
|
||||||
|
Address: dnsDefault,
|
||||||
|
}
|
||||||
|
if dnsDefaultUrl, err := url.Parse(dnsDefault); err == nil && BM.IsDomainName(dnsDefaultUrl.Hostname()) {
|
||||||
|
defaultDNSOptions.AddressResolver = DNSLocalTag
|
||||||
|
}
|
||||||
|
options.DNS.Servers = append(options.DNS.Servers, defaultDNSOptions)
|
||||||
|
var (
|
||||||
|
localDNSOptions option.DNSServerOptions
|
||||||
|
localDNSIsDomain bool
|
||||||
|
)
|
||||||
|
if t.DisableTrafficBypass {
|
||||||
|
localDNSOptions = option.DNSServerOptions{
|
||||||
|
Tag: DNSLocalTag,
|
||||||
|
Address: "local",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
localDNSOptions = option.DNSServerOptions{
|
||||||
|
Tag: DNSLocalTag,
|
||||||
|
Address: dnsLocal,
|
||||||
|
Detour: DefaultDirectTag,
|
||||||
|
}
|
||||||
|
if dnsLocalUrl, err := url.Parse(dnsLocal); err == nil && BM.IsDomainName(dnsLocalUrl.Hostname()) {
|
||||||
|
localDNSOptions.AddressResolver = DNSLocalSetupTag
|
||||||
|
localDNSIsDomain = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
options.DNS.Servers = append(options.DNS.Servers, localDNSOptions)
|
||||||
|
if localDNSIsDomain {
|
||||||
|
options.DNS.Servers = append(options.DNS.Servers, option.DNSServerOptions{
|
||||||
|
Tag: DNSLocalSetupTag,
|
||||||
|
Address: "local",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if t.EnableFakeIP {
|
||||||
|
options.DNS.FakeIP = &option.DNSFakeIPOptions{
|
||||||
|
Enabled: true,
|
||||||
|
Inet4Range: common.Ptr(netip.MustParsePrefix("198.18.0.0/15")),
|
||||||
|
}
|
||||||
|
if !t.DisableIPv6() {
|
||||||
|
options.DNS.FakeIP.Inet6Range = common.Ptr(netip.MustParsePrefix("fc00::/18"))
|
||||||
|
}
|
||||||
|
options.DNS.Servers = append(options.DNS.Servers, option.DNSServerOptions{
|
||||||
|
Tag: DNSFakeIPTag,
|
||||||
|
Address: "fakeip",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
options.DNS.Rules = []option.DNSRule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultDNSRule{
|
||||||
|
Outbound: []string{"any"},
|
||||||
|
Server: DNSLocalTag,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !t.DisableClashMode {
|
||||||
|
options.DNS.Rules = append(options.DNS.Rules, option.DNSRule{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultDNSRule{
|
||||||
|
ClashMode: "Direct",
|
||||||
|
Server: DNSLocalTag,
|
||||||
|
},
|
||||||
|
}, option.DNSRule{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultDNSRule{
|
||||||
|
ClashMode: "Global",
|
||||||
|
Server: DNSDefaultTag,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
options.DNS.Rules = append(options.DNS.Rules, t.PreDNSRules...)
|
||||||
|
if len(t.CustomDNSRules) == 0 {
|
||||||
|
if !t.DisableTrafficBypass {
|
||||||
|
if t.DisableRuleSet || (metadata.Version == nil || metadata.Version.LessThan(semver.ParseVersion("1.8.0-alpha.10"))) {
|
||||||
|
options.DNS.Rules = append(options.DNS.Rules, option.DNSRule{
|
||||||
|
Type: C.RuleTypeLogical,
|
||||||
|
LogicalOptions: option.LogicalDNSRule{
|
||||||
|
Mode: C.LogicalTypeAnd,
|
||||||
|
Rules: []option.DNSRule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultDNSRule{
|
||||||
|
Geosite: []string{"geolocation-!cn"},
|
||||||
|
Invert: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultDNSRule{
|
||||||
|
Geosite: []string{
|
||||||
|
"cn",
|
||||||
|
"category-companies@cn",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Server: DNSLocalTag,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
options.DNS.Rules = append(options.DNS.Rules, option.DNSRule{
|
||||||
|
Type: C.RuleTypeLogical,
|
||||||
|
LogicalOptions: option.LogicalDNSRule{
|
||||||
|
Mode: C.LogicalTypeAnd,
|
||||||
|
Rules: []option.DNSRule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultDNSRule{
|
||||||
|
RuleSet: []string{"geosite-geolocation-!cn"},
|
||||||
|
Invert: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultDNSRule{
|
||||||
|
RuleSet: []string{
|
||||||
|
"geosite-cn",
|
||||||
|
"geosite-category-companies@cn",
|
||||||
|
},
|
||||||
|
DomainSuffix: []string{"download.jetbrains.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Server: DNSLocalTag,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
options.DNS.Rules = append(options.DNS.Rules, t.CustomDNSRules...)
|
||||||
|
}
|
||||||
|
if t.EnableFakeIP {
|
||||||
|
options.DNS.Rules = append(options.DNS.Rules, option.DNSRule{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultDNSRule{
|
||||||
|
QueryType: []option.DNSQueryType{
|
||||||
|
option.DNSQueryType(mDNS.TypeA),
|
||||||
|
option.DNSQueryType(mDNS.TypeAAAA),
|
||||||
|
},
|
||||||
|
Server: DNSFakeIPTag,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
66
template/render_experimental.go
Normal file
66
template/render_experimental.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
M "github.com/sagernet/serenity/common/metadata"
|
||||||
|
"github.com/sagernet/serenity/common/semver"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/json/badjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *Template) renderExperimental(metadata M.Metadata, options *option.Options, profileName string) error {
|
||||||
|
if t.DisableCacheFile && t.DisableClashMode && t.CustomClashAPI == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
options.Experimental = &option.ExperimentalOptions{}
|
||||||
|
disable18Features := metadata.Version == nil || metadata.Version.LessThan(semver.ParseVersion("1.8.0-alpha.10"))
|
||||||
|
if !t.DisableCacheFile {
|
||||||
|
if disable18Features {
|
||||||
|
//nolint:staticcheck
|
||||||
|
//goland:noinspection GoDeprecation
|
||||||
|
options.Experimental.ClashAPI = &option.ClashAPIOptions{
|
||||||
|
CacheID: profileName,
|
||||||
|
StoreMode: true,
|
||||||
|
StoreSelected: true,
|
||||||
|
StoreFakeIP: t.EnableFakeIP,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
options.Experimental.CacheFile = &option.CacheFileOptions{
|
||||||
|
Enabled: true,
|
||||||
|
CacheID: profileName,
|
||||||
|
StoreFakeIP: t.EnableFakeIP,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.CustomClashAPI != nil {
|
||||||
|
newClashOptions, err := badjson.MergeFromDestination(options.Experimental.ClashAPI, t.CustomClashAPI.Message)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
options.Experimental.ClashAPI = newClashOptions
|
||||||
|
} else {
|
||||||
|
if options.Experimental.ClashAPI == nil {
|
||||||
|
options.Experimental.ClashAPI = &option.ClashAPIOptions{}
|
||||||
|
}
|
||||||
|
options.Experimental.ClashAPI.ExternalController = "127.0.0.1:9090"
|
||||||
|
}
|
||||||
|
|
||||||
|
if !t.DisableClashMode {
|
||||||
|
options.Experimental.ClashAPI.DefaultMode = t.ClashModeRule
|
||||||
|
}
|
||||||
|
if t.PProfListen != "" {
|
||||||
|
if options.Experimental.Debug == nil {
|
||||||
|
options.Experimental.Debug = &option.DebugOptions{}
|
||||||
|
}
|
||||||
|
options.Experimental.Debug.Listen = t.PProfListen
|
||||||
|
}
|
||||||
|
if t.MemoryLimit > 0 && !metadata.Platform.IsNetworkExtensionMemoryLimited() {
|
||||||
|
if options.Experimental.Debug == nil {
|
||||||
|
options.Experimental.Debug = &option.DebugOptions{}
|
||||||
|
}
|
||||||
|
options.Experimental.Debug.MemoryLimit = t.MemoryLimit
|
||||||
|
options.Experimental.Debug.OOMKiller = common.Ptr(true)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
99
template/render_geo_resources.go
Normal file
99
template/render_geo_resources.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
M "github.com/sagernet/serenity/common/metadata"
|
||||||
|
"github.com/sagernet/serenity/common/semver"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *Template) renderGeoResources(metadata M.Metadata, options *option.Options) {
|
||||||
|
if t.DisableRuleSet || (metadata.Version == nil || metadata.Version.LessThan(semver.ParseVersion("1.8.0-alpha.10"))) {
|
||||||
|
var (
|
||||||
|
geoipDownloadURL string
|
||||||
|
geositeDownloadURL string
|
||||||
|
downloadDetour string
|
||||||
|
)
|
||||||
|
if t.EnableJSDelivr {
|
||||||
|
geoipDownloadURL = "https://testingcf.jsdelivr.net/gh/SagerNet/sing-geoip@release/geoip-cn.db"
|
||||||
|
geositeDownloadURL = "https://testingcf.jsdelivr.net/gh/SagerNet/sing-geosite@release/geosite-cn.db"
|
||||||
|
if t.DirectTag != "" {
|
||||||
|
downloadDetour = t.DirectTag
|
||||||
|
} else {
|
||||||
|
downloadDetour = DefaultDirectTag
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
geoipDownloadURL = "https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip-cn.db"
|
||||||
|
geositeDownloadURL = "https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite-cn.db"
|
||||||
|
}
|
||||||
|
if t.CustomGeoIP == nil {
|
||||||
|
options.Route.GeoIP = &option.GeoIPOptions{
|
||||||
|
DownloadURL: geoipDownloadURL,
|
||||||
|
DownloadDetour: downloadDetour,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.CustomGeosite == nil {
|
||||||
|
options.Route.Geosite = &option.GeositeOptions{
|
||||||
|
DownloadURL: geositeDownloadURL,
|
||||||
|
DownloadDetour: downloadDetour,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if len(t.CustomDNSRules) == 0 {
|
||||||
|
var (
|
||||||
|
downloadURL string
|
||||||
|
downloadDetour string
|
||||||
|
branchSplit string
|
||||||
|
)
|
||||||
|
if t.EnableJSDelivr {
|
||||||
|
downloadURL = "https://testingcf.jsdelivr.net/gh/"
|
||||||
|
if t.DirectTag != "" {
|
||||||
|
downloadDetour = t.DirectTag
|
||||||
|
} else {
|
||||||
|
downloadDetour = DefaultDirectTag
|
||||||
|
}
|
||||||
|
branchSplit = "@"
|
||||||
|
} else {
|
||||||
|
downloadURL = "https://raw.githubusercontent.com/"
|
||||||
|
branchSplit = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
options.Route.RuleSet = []option.RuleSet{
|
||||||
|
{
|
||||||
|
Type: C.RuleSetTypeRemote,
|
||||||
|
Tag: "geoip-cn",
|
||||||
|
Format: C.RuleSetFormatBinary,
|
||||||
|
RemoteOptions: option.RemoteRuleSet{
|
||||||
|
URL: downloadURL + "SagerNet/sing-geoip" + branchSplit + "rule-set/geoip-cn.srs",
|
||||||
|
DownloadDetour: downloadDetour,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.RuleSetTypeRemote,
|
||||||
|
Tag: "geosite-cn",
|
||||||
|
Format: C.RuleSetFormatBinary,
|
||||||
|
RemoteOptions: option.RemoteRuleSet{
|
||||||
|
URL: downloadURL + "SagerNet/sing-geosite" + branchSplit + "rule-set/geosite-cn.srs",
|
||||||
|
DownloadDetour: downloadDetour,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.RuleSetTypeRemote,
|
||||||
|
Tag: "geosite-geolocation-!cn",
|
||||||
|
Format: C.RuleSetFormatBinary,
|
||||||
|
RemoteOptions: option.RemoteRuleSet{
|
||||||
|
URL: downloadURL + "SagerNet/sing-geosite" + branchSplit + "rule-set/geosite-geolocation-!cn.srs",
|
||||||
|
DownloadDetour: downloadDetour,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.RuleSetTypeRemote,
|
||||||
|
Tag: "geosite-category-companies@cn",
|
||||||
|
Format: C.RuleSetFormatBinary,
|
||||||
|
RemoteOptions: option.RemoteRuleSet{
|
||||||
|
URL: downloadURL + "SagerNet/sing-geosite" + branchSplit + "rule-set/geosite-category-companies@cn.srs",
|
||||||
|
DownloadDetour: downloadDetour,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
107
template/render_inbounds.go
Normal file
107
template/render_inbounds.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
M "github.com/sagernet/serenity/common/metadata"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing-dns"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/json/badjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *Template) renderInbounds(metadata M.Metadata, options *option.Options) error {
|
||||||
|
var needSniff bool
|
||||||
|
if !t.DisableTrafficBypass {
|
||||||
|
needSniff = true
|
||||||
|
}
|
||||||
|
var domainStrategy option.DomainStrategy
|
||||||
|
if !t.RemoteResolve {
|
||||||
|
if t.DomainStrategy != option.DomainStrategy(dns.DomainStrategyAsIS) {
|
||||||
|
domainStrategy = t.DomainStrategy
|
||||||
|
} else {
|
||||||
|
domainStrategy = option.DomainStrategy(dns.DomainStrategyPreferIPv4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disableTun := t.DisableTUN && !metadata.Platform.TunOnly()
|
||||||
|
if !disableTun {
|
||||||
|
if options.Route == nil {
|
||||||
|
options.Route = &option.RouteOptions{}
|
||||||
|
}
|
||||||
|
options.Route.AutoDetectInterface = true
|
||||||
|
|
||||||
|
var inet6Address []netip.Prefix
|
||||||
|
if !t.DisableIPv6() {
|
||||||
|
inet6Address = []netip.Prefix{netip.MustParsePrefix("fdfe:dcba:9876::1/126")}
|
||||||
|
}
|
||||||
|
tunInbound := option.Inbound{
|
||||||
|
Type: C.TypeTun,
|
||||||
|
TunOptions: option.TunInboundOptions{
|
||||||
|
Inet4Address: []netip.Prefix{netip.MustParsePrefix("172.19.0.1/30")},
|
||||||
|
Inet6Address: inet6Address,
|
||||||
|
AutoRoute: true,
|
||||||
|
InboundOptions: option.InboundOptions{
|
||||||
|
SniffEnabled: needSniff,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if t.EnableFakeIP {
|
||||||
|
tunInbound.TunOptions.InboundOptions.DomainStrategy = domainStrategy
|
||||||
|
}
|
||||||
|
if metadata.Platform == M.PlatformUnknown {
|
||||||
|
tunInbound.TunOptions.StrictRoute = true
|
||||||
|
}
|
||||||
|
if !t.DisableSystemProxy && metadata.Platform != M.PlatformUnknown {
|
||||||
|
var httpPort uint16
|
||||||
|
if t.CustomMixed != nil {
|
||||||
|
httpPort = t.CustomMixed.Value.ListenPort
|
||||||
|
}
|
||||||
|
if httpPort == 0 {
|
||||||
|
httpPort = DefaultMixedPort
|
||||||
|
}
|
||||||
|
tunInbound.TunOptions.Platform = &option.TunPlatformOptions{
|
||||||
|
HTTPProxy: &option.HTTPProxyOptions{
|
||||||
|
Enabled: true,
|
||||||
|
ServerOptions: option.ServerOptions{
|
||||||
|
Server: "127.0.0.1",
|
||||||
|
ServerPort: httpPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.CustomTUN != nil {
|
||||||
|
newTUNOptions, err := badjson.MergeFromDestination(tunInbound.TunOptions, t.CustomTUN.Message)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "merge custom tun options")
|
||||||
|
}
|
||||||
|
tunInbound.TunOptions = newTUNOptions
|
||||||
|
}
|
||||||
|
options.Inbounds = append(options.Inbounds, tunInbound)
|
||||||
|
}
|
||||||
|
if disableTun || !t.DisableSystemProxy {
|
||||||
|
mixedInbound := option.Inbound{
|
||||||
|
Type: C.TypeMixed,
|
||||||
|
MixedOptions: option.HTTPMixedInboundOptions{
|
||||||
|
ListenOptions: option.ListenOptions{
|
||||||
|
Listen: option.NewListenAddress(netip.AddrFrom4([4]byte{127, 0, 0, 1})),
|
||||||
|
ListenPort: DefaultMixedPort,
|
||||||
|
InboundOptions: option.InboundOptions{
|
||||||
|
SniffEnabled: needSniff,
|
||||||
|
DomainStrategy: domainStrategy,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SetSystemProxy: metadata.Platform == M.PlatformUnknown && disableTun && !t.DisableSystemProxy,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if t.CustomMixed != nil {
|
||||||
|
newMixedOptions, err := badjson.MergeFromDestination(mixedInbound.MixedOptions, t.CustomMixed.Message)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "merge custom mixed options")
|
||||||
|
}
|
||||||
|
mixedInbound.MixedOptions = newMixedOptions
|
||||||
|
}
|
||||||
|
options.Inbounds = append(options.Inbounds, mixedInbound)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
183
template/render_outbounds.go
Normal file
183
template/render_outbounds.go
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
M "github.com/sagernet/serenity/common/metadata"
|
||||||
|
"github.com/sagernet/serenity/subscription"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *Template) renderOutbounds(metadata M.Metadata, options *option.Options, outbounds [][]option.Outbound, subscriptions []*subscription.Subscription) error {
|
||||||
|
defaultTag := t.DefaultTag
|
||||||
|
if defaultTag == "" {
|
||||||
|
defaultTag = DefaultDefaultTag
|
||||||
|
}
|
||||||
|
if options.Route == nil {
|
||||||
|
options.Route = &option.RouteOptions{}
|
||||||
|
}
|
||||||
|
options.Route.Final = defaultTag
|
||||||
|
directTag := t.DirectTag
|
||||||
|
if directTag == "" {
|
||||||
|
directTag = DefaultDirectTag
|
||||||
|
}
|
||||||
|
options.Outbounds = []option.Outbound{
|
||||||
|
{
|
||||||
|
Tag: directTag,
|
||||||
|
Type: C.TypeDirect,
|
||||||
|
DirectOptions: common.PtrValueOrDefault(t.CustomDirect),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Tag: BlockTag,
|
||||||
|
Type: C.TypeBlock,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Tag: DNSTag,
|
||||||
|
Type: C.TypeDNS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Tag: defaultTag,
|
||||||
|
Type: C.TypeSelector,
|
||||||
|
SelectorOptions: common.PtrValueOrDefault(t.CustomSelector),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
urlTestTag := t.URLTestTag
|
||||||
|
if urlTestTag == "" {
|
||||||
|
urlTestTag = DefaultURLTestTag
|
||||||
|
}
|
||||||
|
if t.GenerateGlobalURLTest {
|
||||||
|
options.Outbounds = append(options.Outbounds, option.Outbound{
|
||||||
|
Tag: urlTestTag,
|
||||||
|
Type: C.TypeURLTest,
|
||||||
|
URLTestOptions: common.PtrValueOrDefault(t.CustomURLTest),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
globalJoin := func(groupOutbounds ...string) {
|
||||||
|
options.Outbounds = groupJoin(options.Outbounds, defaultTag, groupOutbounds...)
|
||||||
|
if t.GenerateGlobalURLTest {
|
||||||
|
options.Outbounds = groupJoin(options.Outbounds, urlTestTag, groupOutbounds...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalOutbounds []option.Outbound
|
||||||
|
if len(outbounds) > 0 {
|
||||||
|
for _, outbound := range outbounds {
|
||||||
|
options.Outbounds = append(options.Outbounds, outbound...)
|
||||||
|
}
|
||||||
|
globalOutbounds = common.Map(outbounds, func(it []option.Outbound) option.Outbound {
|
||||||
|
return it[0]
|
||||||
|
})
|
||||||
|
globalJoin(common.Map(globalOutbounds, func(it option.Outbound) string {
|
||||||
|
return it.Tag
|
||||||
|
})...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var allGroups []option.Outbound
|
||||||
|
var allGroupOutbounds []option.Outbound
|
||||||
|
|
||||||
|
for _, it := range subscriptions {
|
||||||
|
if len(it.Servers) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
joinOutbounds := common.Map(it.Servers, func(it option.Outbound) string {
|
||||||
|
return it.Tag
|
||||||
|
})
|
||||||
|
if it.GenerateSelector {
|
||||||
|
selectorOutbound := option.Outbound{
|
||||||
|
Type: C.TypeSelector,
|
||||||
|
Tag: it.Name,
|
||||||
|
SelectorOptions: common.PtrValueOrDefault(t.CustomSelector),
|
||||||
|
}
|
||||||
|
selectorOutbound.SelectorOptions.Outbounds = append(selectorOutbound.SelectorOptions.Outbounds, joinOutbounds...)
|
||||||
|
allGroups = append(allGroups, selectorOutbound)
|
||||||
|
globalJoin(it.Name)
|
||||||
|
}
|
||||||
|
if it.GenerateURLTest {
|
||||||
|
var urltestTag string
|
||||||
|
if !it.GenerateSelector {
|
||||||
|
urltestTag = it.Name
|
||||||
|
} else if it.URLTestTagSuffix != "" {
|
||||||
|
urltestTag = it.Name + " " + it.URLTestTagSuffix
|
||||||
|
} else {
|
||||||
|
urltestTag = it.Name + " - URLTest"
|
||||||
|
}
|
||||||
|
urltestOutbound := option.Outbound{
|
||||||
|
Type: C.TypeURLTest,
|
||||||
|
Tag: urltestTag,
|
||||||
|
URLTestOptions: common.PtrValueOrDefault(t.CustomURLTest),
|
||||||
|
}
|
||||||
|
urltestOutbound.URLTestOptions.Outbounds = append(urltestOutbound.URLTestOptions.Outbounds, joinOutbounds...)
|
||||||
|
allGroups = append(allGroups, urltestOutbound)
|
||||||
|
globalJoin(urltestTag)
|
||||||
|
}
|
||||||
|
if !it.GenerateSelector && !it.GenerateURLTest {
|
||||||
|
globalJoin(joinOutbounds...)
|
||||||
|
}
|
||||||
|
allGroupOutbounds = append(allGroupOutbounds, it.Servers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
globalOutbounds = append(globalOutbounds, allGroups...)
|
||||||
|
globalOutbounds = append(globalOutbounds, allGroupOutbounds...)
|
||||||
|
|
||||||
|
for _, group := range t.groups {
|
||||||
|
var extraTags []string
|
||||||
|
for _, groupOutbound := range globalOutbounds {
|
||||||
|
if len(group.filter) > 0 {
|
||||||
|
if !common.Any(group.filter, func(it *regexp.Regexp) bool {
|
||||||
|
return it.MatchString(groupOutbound.Tag)
|
||||||
|
}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(group.exclude) > 0 {
|
||||||
|
if common.Any(group.exclude, func(it *regexp.Regexp) bool {
|
||||||
|
return it.MatchString(groupOutbound.Tag)
|
||||||
|
}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extraTags = append(extraTags, groupOutbound.Tag)
|
||||||
|
}
|
||||||
|
if len(extraTags) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
groupOutbound := option.Outbound{
|
||||||
|
Tag: group.Tag,
|
||||||
|
Type: group.Type,
|
||||||
|
SelectorOptions: common.PtrValueOrDefault(group.CustomSelector),
|
||||||
|
URLTestOptions: common.PtrValueOrDefault(group.CustomURLTest),
|
||||||
|
}
|
||||||
|
switch group.Type {
|
||||||
|
case C.TypeSelector:
|
||||||
|
groupOutbound.SelectorOptions.Outbounds = extraTags
|
||||||
|
case C.TypeURLTest:
|
||||||
|
groupOutbound.URLTestOptions.Outbounds = extraTags
|
||||||
|
}
|
||||||
|
options.Outbounds = append(options.Outbounds, groupOutbound)
|
||||||
|
}
|
||||||
|
|
||||||
|
options.Outbounds = append(options.Outbounds, allGroups...)
|
||||||
|
options.Outbounds = append(options.Outbounds, allGroupOutbounds...)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupJoin(outbounds []option.Outbound, groupTag string, groupOutbounds ...string) []option.Outbound {
|
||||||
|
groupIndex := common.Index(outbounds, func(it option.Outbound) bool {
|
||||||
|
return it.Tag == groupTag
|
||||||
|
})
|
||||||
|
if groupIndex == -1 {
|
||||||
|
return outbounds
|
||||||
|
}
|
||||||
|
groupOutbound := outbounds[groupIndex]
|
||||||
|
switch groupOutbound.Type {
|
||||||
|
case C.TypeSelector:
|
||||||
|
groupOutbound.SelectorOptions.Outbounds = append(groupOutbound.SelectorOptions.Outbounds, groupOutbounds...)
|
||||||
|
case C.TypeURLTest:
|
||||||
|
groupOutbound.URLTestOptions.Outbounds = append(groupOutbound.URLTestOptions.Outbounds, groupOutbounds...)
|
||||||
|
}
|
||||||
|
outbounds[groupIndex] = groupOutbound
|
||||||
|
return outbounds
|
||||||
|
}
|
177
template/render_route.go
Normal file
177
template/render_route.go
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
M "github.com/sagernet/serenity/common/metadata"
|
||||||
|
"github.com/sagernet/serenity/common/semver"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *Template) renderRoute(metadata M.Metadata, options *option.Options) error {
|
||||||
|
if options.Route == nil {
|
||||||
|
options.Route = &option.RouteOptions{
|
||||||
|
GeoIP: t.CustomGeoIP,
|
||||||
|
Geosite: t.CustomGeosite,
|
||||||
|
RuleSet: t.CustomRuleSet,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !t.DisableTrafficBypass {
|
||||||
|
t.renderGeoResources(metadata, options)
|
||||||
|
}
|
||||||
|
disable18Features := metadata.Version == nil || metadata.Version.LessThan(semver.ParseVersion("1.8.0-alpha.10"))
|
||||||
|
options.Route.Rules = []option.Rule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeLogical,
|
||||||
|
LogicalOptions: option.LogicalRule{
|
||||||
|
Mode: C.LogicalTypeOr,
|
||||||
|
Rules: []option.Rule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
Network: []string{N.NetworkUDP},
|
||||||
|
Port: []uint16{53},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
Protocol: []string{C.ProtocolDNS},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outbound: DNSTag,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !t.DisableTrafficBypass && !t.DisableDefaultRules {
|
||||||
|
options.Route.Rules = append(options.Route.Rules, option.Rule{
|
||||||
|
Type: C.RuleTypeLogical,
|
||||||
|
LogicalOptions: option.LogicalRule{
|
||||||
|
Mode: C.LogicalTypeOr,
|
||||||
|
Rules: []option.Rule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
Network: []string{N.NetworkUDP},
|
||||||
|
Port: []uint16{443},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
Protocol: []string{C.ProtocolSTUN},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outbound: BlockTag,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
directTag := t.DirectTag
|
||||||
|
defaultTag := t.DefaultTag
|
||||||
|
if directTag == "" {
|
||||||
|
directTag = DefaultDirectTag
|
||||||
|
}
|
||||||
|
if defaultTag == "" {
|
||||||
|
defaultTag = DefaultDefaultTag
|
||||||
|
}
|
||||||
|
if disable18Features {
|
||||||
|
options.Route.Rules = append(options.Route.Rules, option.Rule{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
GeoIP: []string{"private"},
|
||||||
|
Outbound: directTag,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
options.Route.Rules = append(options.Route.Rules, option.Rule{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
IPIsPrivate: true,
|
||||||
|
Outbound: directTag,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if !t.DisableClashMode {
|
||||||
|
modeGlobal := t.ClashModeGlobal
|
||||||
|
modeDirect := t.ClashModeDirect
|
||||||
|
if modeGlobal == "" {
|
||||||
|
modeGlobal = "Global"
|
||||||
|
}
|
||||||
|
if modeDirect == "" {
|
||||||
|
modeDirect = "Direct"
|
||||||
|
}
|
||||||
|
options.Route.Rules = append(options.Route.Rules, option.Rule{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
ClashMode: modeGlobal,
|
||||||
|
Outbound: defaultTag,
|
||||||
|
},
|
||||||
|
}, option.Rule{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
ClashMode: modeDirect,
|
||||||
|
Outbound: directTag,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
options.Route.Rules = append(options.Route.Rules, t.PreRules...)
|
||||||
|
if len(t.CustomRules) == 0 {
|
||||||
|
if !t.DisableTrafficBypass {
|
||||||
|
if t.DisableRuleSet || disable18Features {
|
||||||
|
options.Route.Rules = append(options.Route.Rules, option.Rule{
|
||||||
|
Type: C.RuleTypeLogical,
|
||||||
|
LogicalOptions: option.LogicalRule{
|
||||||
|
Mode: C.LogicalTypeAnd,
|
||||||
|
Rules: []option.Rule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
Geosite: []string{"geolocation-!cn"},
|
||||||
|
Invert: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
GeoIP: []string{"cn"},
|
||||||
|
Geosite: []string{"cn", "category-companies@cn"},
|
||||||
|
Domain: []string{"download.jetbrains.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outbound: directTag,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
options.Route.Rules = append(options.Route.Rules, option.Rule{
|
||||||
|
Type: C.RuleTypeLogical,
|
||||||
|
LogicalOptions: option.LogicalRule{
|
||||||
|
Mode: C.LogicalTypeAnd,
|
||||||
|
Rules: []option.Rule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
RuleSet: []string{"geosite-geolocation-!cn"},
|
||||||
|
Invert: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
RuleSet: []string{"geoip-cn", "geosite-cn", "geosite-category-companies@cn"},
|
||||||
|
Domain: []string{"download.jetbrains.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outbound: "direct",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
options.Route.Rules = append(options.Route.Rules, t.CustomRules...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
66
template/template.go
Normal file
66
template/template.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
M "github.com/sagernet/serenity/common/metadata"
|
||||||
|
"github.com/sagernet/serenity/option"
|
||||||
|
"github.com/sagernet/serenity/subscription"
|
||||||
|
"github.com/sagernet/serenity/template/filter"
|
||||||
|
boxOption "github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultMixedPort = 8080
|
||||||
|
DNSDefaultTag = "default"
|
||||||
|
DNSLocalTag = "local"
|
||||||
|
DNSLocalSetupTag = "local_setup"
|
||||||
|
DNSFakeIPTag = "remote"
|
||||||
|
DefaultDNS = "tls://8.8.8.8"
|
||||||
|
DefaultDNSLocal = "114.114.114.114"
|
||||||
|
DefaultDefaultTag = "Default"
|
||||||
|
DefaultDirectTag = "direct"
|
||||||
|
BlockTag = "block"
|
||||||
|
DNSTag = "dns"
|
||||||
|
DefaultURLTestTag = "URLTest"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Default = new(Template)
|
||||||
|
|
||||||
|
type Template struct {
|
||||||
|
option.Template
|
||||||
|
groups []*ExtraGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExtraGroup struct {
|
||||||
|
option.ExtraGroup
|
||||||
|
filter []*regexp.Regexp
|
||||||
|
exclude []*regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Template) Render(metadata M.Metadata, profileName string, outbounds [][]boxOption.Outbound, subscriptions []*subscription.Subscription) (*boxOption.Options, error) {
|
||||||
|
var options boxOption.Options
|
||||||
|
err := t.renderDNS(metadata, &options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "render dns")
|
||||||
|
}
|
||||||
|
err = t.renderInbounds(metadata, &options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "render inbounds")
|
||||||
|
}
|
||||||
|
err = t.renderOutbounds(metadata, &options, outbounds, subscriptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "render outbounds")
|
||||||
|
}
|
||||||
|
err = t.renderRoute(metadata, &options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "render route")
|
||||||
|
}
|
||||||
|
err = t.renderExperimental(metadata, &options, profileName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "render experimental")
|
||||||
|
}
|
||||||
|
filter.Filter(metadata, &options)
|
||||||
|
return &options, nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user