Init commit

This commit is contained in:
世界 2023-12-19 20:00:00 +08:00
commit 1e27e80af9
No known key found for this signature in database
GPG Key ID: CD109927C34A63C4
83 changed files with 5605 additions and 0 deletions

5
.github/update_dependencies.sh vendored Executable file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,11 @@
/.idea/
/vendor/
/*.json
/*.db
/site/
/bin/
/dist/
/build/
.DS_Store
/serenity
/serenity.exe

18
.golangci.yml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
View 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!")
}

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

View 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
}

View 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
View 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
View 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
View File

@ -0,0 +1 @@
serenity.sagernet.org

37
docs/assets/icon.svg Normal file
View 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
View File

@ -0,0 +1,7 @@
---
icon: material/alert-decagram
---
##### 2023/12/12
No changelog before.

View 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/

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

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

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

View 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
View 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
View 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.
```

View 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
```

View 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
```

View 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
```

View 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
```

View 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/

View 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/

View 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

View 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

View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,113 @@
site_name: serenity
site_author: nekohasekai
repo_url: https://github.com/SagerNet/serenity
repo_name: SagerNet/serenity
copyright: Copyright &copy; 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
View 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
View 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
View 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"`
}

View File

@ -0,0 +1,8 @@
{
"listen": ":8080",
"users": [],
"subscriptions": [],
"outbounds": [],
"templates": [],
"profiles": []
}

View 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

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

View 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
View 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
View 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
View 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
View 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
View 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
}

View 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{}
}

View 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 ""
}

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

View 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 ""
}

View 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")
}

View 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
}

View 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
}

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

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

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

View 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
}

View 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
View 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
View 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
}

View 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
}

View 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
View 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
}

View 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
View 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
View 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
}