Compare commits

...
Author SHA1 Message Date
Sergei Maklagin 3bd162ed6f Add new admin panel, failover, dns fallback, providers, limiters. Update XHTTP 2026-05-11 00:59:35 +03:00
Sergei Maklagin 652e0baf57 Fix go.sum 2026-05-05 12:59:07 +03:00
Sergei Maklagin 8be5c8fabe Fix legacy build 2026-05-05 12:58:01 +03:00
Sergei Maklagin eb36c934a7 Fix examples 2026-05-03 04:14:59 +03:00
Sergei Maklagin 52edfdb059 Remove IPBlocklist and IPAllowlist 2026-05-03 04:04:16 +03:00
Sergei Maklagin eecab479fa Fix Dockerfile 2026-05-02 15:27:46 +03:00
Sergei Maklagin d8670a742a Merge branch 'extended' of https://github.com/shtorm-7/sing-box-extended into extended 2026-05-02 15:00:57 +03:00
Sergei Maklagin e2ef7d83a1 Fix creating config for WARP 2026-05-02 14:54:26 +03:00
ShtormandGitHub af75b8074f Update README.md
Signed-off-by: Shtorm <108103062+shtorm-7@users.noreply.github.com>
2026-04-30 23:39:22 +03:00
Sergei Maklagin 2e0306ae41 Disable Wireguard ICMP 2026-04-30 19:20:23 +03:00
Sergei Maklagin ee2945ac8f Fix OverrideGateway 2026-04-30 19:19:40 +03:00
Sergei Maklagin b2503ca860 Update chi 2026-04-30 19:18:50 +03:00
Sergei Maklagin dcb1da8683 Fix examples 2026-04-30 13:20:45 +03:00
Sergei Maklagin 22aeb48794 Merge branch 'extended' of https://github.com/shtorm-7/sing-box-extended into extended 2026-04-30 13:14:37 +03:00
Sergei Maklagin 019103587b Update examples 2026-04-30 13:14:17 +03:00
Sergei Maklagin 3139f18bf1 Update android build tags 2026-04-30 12:17:37 +03:00
ShtormandGitHub 493538c743 Update README.md
Signed-off-by: Shtorm <108103062+shtorm-7@users.noreply.github.com>
2026-04-30 11:15:15 +03:00
ShtormandGitHub 578bc159fb Update README.md
Signed-off-by: Shtorm <108103062+shtorm-7@users.noreply.github.com>
2026-04-30 01:15:21 +03:00
ShtormandGitHub f0c317cb0b Update README.md
Signed-off-by: Shtorm <108103062+shtorm-7@users.noreply.github.com>
2026-04-30 01:12:28 +03:00
ShtormandGitHub 32bc1a9fce Update README.md
Signed-off-by: Shtorm <108103062+shtorm-7@users.noreply.github.com>
2026-04-30 01:08:36 +03:00
Sergei Maklagin 41922ba731 Update README.md 2026-04-29 22:40:15 +03:00
Sergei Maklagin bf8fe79a7a Fix go.mod 2026-04-29 22:16:10 +03:00
Sergei Maklagin 398b6387ab Resolve conflicts 2026-04-29 22:14:30 +03:00
Sergei Maklagin 04908a6a67 Add MTProxy, MASQUE, VPN, Link parser. Update AmneziaWG. Remove Tunneling 2026-04-29 22:11:30 +03:00
Sergei Maklagin 88a80e961b Fix README.md 2026-04-27 11:38:09 +03:00
Sergei Maklagin 09f9f114aa Update sing-box core 2026-04-22 19:23:23 +03:00
Sergei Maklagin 1995ba4279 Update sing-box core 2026-04-17 01:10:31 +03:00
Sergei Maklagin 2d33dee415 Update sing-box core 2026-04-06 20:54:24 +03:00
Sergei Maklagin 20bf40e822 Update dependencies 2026-03-11 17:32:12 +03:00
Sergei Maklagin 861aff60f0 Update dependencies 2026-03-11 06:45:24 +03:00
Sergei Maklagin 2df1cffb0f Resolve conflicts 2026-03-10 08:43:01 +03:00
Sergei Maklagin bc6ca6e2ea Update sing-box core 2026-03-10 04:50:32 +03:00
Sergei Maklagin 0503006f48 Add Kmutex 2026-03-03 00:51:20 +03:00
Sergei Maklagin 517f5152e7 Add migrate 2026-03-03 00:51:04 +03:00
Sergei Maklagin b1b7aa81cd Update Amnezia H1-H4 format 2026-03-02 21:48:54 +03:00
Sergei Maklagin 195e941c35 Fix typo 2026-03-02 21:27:47 +03:00
Sergei Maklagin 35bc351564 Fix failover 2026-03-02 19:48:06 +03:00
Sergei Maklagin 290dbed7b8 Fix failover 2026-03-02 19:33:59 +03:00
Sergei Maklagin d7a8207f44 Update AmneziaWG 2026-03-02 19:33:07 +03:00
Sergei Maklagin 57c5ca13eb Fix bond outbound 2026-03-02 19:31:23 +03:00
Sergei Maklagin 7fc33134fb Update AmneziaWG 2026-03-01 16:42:21 +03:00
Sergei Maklagin 881ab6d436 Fix examples 2026-02-27 00:47:22 +03:00
Sergei Maklagin 0443b93328 Update README.md 2026-02-27 00:19:37 +03:00
Sergei Maklagin 75557830a8 Merge branch 'extended' into extended-next 2026-02-26 22:58:59 +03:00
Sergei Maklagin 9d5273ba1e Resolve conflicts 2026-02-26 22:58:45 +03:00
Sergei Maklagin 5f2a65f01b Add examples 2026-02-26 22:57:44 +03:00
Sergei Maklagin 06a519db27 Resolve conflicts 2026-02-26 22:57:25 +03:00
Sergei Maklagin 65e73fe817 Add examples 2026-02-26 22:55:24 +03:00
Sergei Maklagin c0aa3480c5 Add admin panel, manager, node_manager, bandwidth limiter, connection limiter, bonding, failover, vless encryption, mkcp transport 2026-02-26 22:44:31 +03:00
Sergei Maklagin 69f6c75dd7 Add vless encryption 2026-02-26 18:03:59 +03:00
407 changed files with 54995 additions and 1530 deletions
+2
View File
@@ -21,3 +21,5 @@
CLAUDE.md
AGENTS.md
/.claude/
dist
logs
-6
View File
@@ -1,6 +0,0 @@
[submodule "clients/apple"]
path = clients/apple
url = https://github.com/SagerNet/sing-box-for-apple.git
[submodule "clients/android"]
path = clients/android
url = https://github.com/SagerNet/sing-box-for-android.git
+138
View File
@@ -0,0 +1,138 @@
version: 2
project_name: sing-box
builds:
- &template
id: main
main: ./cmd/sing-box
flags:
- -v
- -trimpath
ldflags:
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
- -s
- -buildid=
tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_utls
- with_acme
- with_clash_api
- with_tailscale
- with_masque
- with_mtproxy
- with_manager
- with_admin_panel
env:
- CGO_ENABLED=0
- GOTOOLCHAIN=local
targets:
- linux_386
- linux_amd64_v1
- linux_arm64
- linux_arm_6
- linux_arm_7
- linux_s390x
- linux_riscv64
- windows_amd64_v1
- windows_386
- windows_arm64
- darwin_amd64_v1
- darwin_arm64
mod_timestamp: '{{ .CommitTimestamp }}'
- id: mips
<<: *template
tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_utls
- with_acme
- with_clash_api
- with_tailscale
- with_masque
- with_mtproxy
targets:
- linux_mips
- linux_mips_softfloat
- linux_mipsle
- linux_mipsle_softfloat
- linux_mips64
- linux_mips64le
- id: android
<<: *template
env:
- CGO_ENABLED=1
- GOTOOLCHAIN=local
overrides:
- goos: android
goarch: arm
goarm: 7
env:
- CC=armv7a-linux-androideabi21-clang
- CXX=armv7a-linux-androideabi21-clang++
- goos: android
goarch: arm64
env:
- CC=aarch64-linux-android21-clang
- CXX=aarch64-linux-android21-clang++
- goos: android
goarch: 386
env:
- CC=i686-linux-android21-clang
- CXX=i686-linux-android21-clang++
- goos: android
goarch: amd64
goamd64: v1
env:
- CC=x86_64-linux-android21-clang
- CXX=x86_64-linux-android21-clang++
targets:
- android_arm_7
- android_arm64
- android_386
- android_amd64
archives:
- &template
id: archive
builds:
- main
- mips
- android
formats:
- tar.gz
format_overrides:
- goos: windows
formats:
- zip
wrap_in_directory: true
files:
- LICENSE
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}-{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
- id: archive-legacy
<<: *template
builds:
- legacy
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-legacy'
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: shtorm-7
name: sing-box-extended
draft: true
prerelease: auto
mode: replace
ids:
- archive
- package
skip_upload: true
+1 -1
View File
@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
LABEL maintainer="shtorm-7"
COPY . /go/src/github.com/sagernet/sing-box
WORKDIR /go/src/github.com/sagernet/sing-box
+34 -6
View File
@@ -14,12 +14,35 @@ PREFIX ?= $(shell go env GOPATH)
SING_FFI ?= sing-ffi
LIBBOX_FFI_CONFIG ?= ./experimental/libbox/ffi.json
ADMIN_PANEL_DIR = service/admin_panel
ADMIN_PANEL_WEB = $(ADMIN_PANEL_DIR)/web
ADMIN_PANEL_DIST = $(ADMIN_PANEL_DIR)/dist
ADMIN_PANEL_TAGS = $(TAGS),with_admin_panel
DOCKER_IMAGE ?= shtorm7/sing-box-extended
DOCKER_PLATFORMS ?= linux/amd64,linux/arm64
.PHONY: test release docs build
build:
export GOTOOLCHAIN=local && \
go build $(MAIN_PARAMS) $(MAIN)
admin_panel_web:
cd $(ADMIN_PANEL_WEB) && \
npm install --no-fund --no-audit && \
npm run build
admin_panel_pack:
go run ./cmd/internal/admin_panel_pack \
-dir $(ADMIN_PANEL_DIST)
admin_panel_regen: admin_panel_web admin_panel_pack
build_admin_panel:
export GOTOOLCHAIN=local && \
go build $(PARAMS) -tags "$(ADMIN_PANEL_TAGS)" $(MAIN)
race:
export GOTOOLCHAIN=local && \
go build -race $(MAIN_PARAMS) $(MAIN)
@@ -70,14 +93,10 @@ update_certificates:
go run ./cmd/internal/update_certificates
release:
go run ./cmd/internal/build goreleaser release --clean --skip publish
go run ./cmd/internal/build goreleaser release --skip=validate --clean -p 3 --skip publish
mkdir dist/release
mv dist/*.tar.gz \
dist/*.zip \
dist/*.deb \
dist/*.rpm \
dist/*_amd64.pkg.tar.zst \
dist/*_arm64.pkg.tar.zst \
dist/release
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
rm -r dist/release
@@ -88,6 +107,15 @@ release_repo:
release_install:
go install -v github.com/tcnksm/ghr@latest
release_docker:
sudo docker buildx build \
--platform $(DOCKER_PLATFORMS) \
-t $(DOCKER_IMAGE):latest \
-t $(DOCKER_IMAGE):$(VERSION) \
--push \
--network=host \
.
update_android_version:
go run ./cmd/internal/update_android_version
@@ -101,7 +129,7 @@ upload_android:
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
rm -rf dist/release_android
release_android: lib_android update_android_version build_android upload_android
release_android: lib_android update_android_version build_android
publish_android:
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop
+52 -15
View File
@@ -1,30 +1,67 @@
<<<<<<< HEAD
# sing-box-extended
Sing-box with extended features.
## Features
## 🔥 Features
* Amnezia 1.5
* WARP
* Tunneling
* Mieru
* XHTTP
* SDNS (DNSCrypt)
* Extended Wireguard options
* Unified delay
=======
# sing-box
>>>>>>> v1.13.11
### Outbounds
- **WARP** — Cloudflare WARP integration through WireGuard
- **MASQUE** — Cloudflare MASQUE proxy over QUIC / HTTP-2
- **MTProxy** — Telegram MTProxy server with FakeTLS and domain fronting
- **Mieru** — Secure, hard to classify, hard to probe network protocol
- **VPN** — Routed tunnel over any TCP sing-box protocol
- **Bond** — Link aggregation for increasing throughput
- **Fallback** — Outbound group with priority-based switching
- **Failover** — Automatic outbound switching with session recovery for high availability
## Examples
### DNS
- **SDNS (DNSCrypt)** — Encrypted DNS queries for enhanced privacy
- **DNS Fallback** — Sequential / parallel queries across upstream resolvers
### Limiters
- **Bandwidth Limiter** — Upload / download / bidirectional rate limiting
- **Connection Limiter** — Concurrent connection control
- **Traffic Limiter** — Per-user traffic quotas
- **Rate Limiter** — Request rate limiting
### Encryption & Obfuscation
- **Amnezia 2.0** — WireGuard traffic obfuscation
- **VLESS encryption** — XRAY encryption for VLESS protocol
### Transports
- **mKCP** — Reliable UDP-based transport
- **XHTTP** — Modern XRAY transport
### Services
- **Admin Panel** — Web-based management interface
- **Manager** — Management service for configuring users, nodes, limiters
- **Manager API (HTTP/gRPC)** — HTTP and gRPC API for the Manager
- **Node Manager API** — Service for connecting nodes to remote manager
### Miscellaneous
- **Providers** — Outbound subscriptions from local files, inline lists, or remote URLs (sing-box JSON, Clash YAML, SIP008, share links)
- **Link Parser** — Outbound configured from a share link (VLESS, VMess, Shadowsocks, Trojan, Hysteria, Hysteria2, TUIC)
- **Extended WireGuard options** — Advanced configuration capabilities
- **Unified Delay** — Unified latency measurement
## 📚 Examples
Configuration examples are available here:
https://github.com/shtorm-7/sing-box-extended/tree/extended/examples
## Support the project
## Support the Project
If you want to support the project, you can donate to the following addresses.
#### Tribute
**[RUB Donate](https://web.tribute.tg/d/JxY)**
**[EUR Donate](https://web.tribute.tg/d/JxZ)**
**[USD Donate](https://web.tribute.tg/d/Jy1)**
#### TRX (Tron)
```
TSWU6VUZ4FcUghYDmbbhK15gRVvhvBgW3F
+5
View File
@@ -48,6 +48,7 @@ type CacheFile interface {
RDRCStore
StoreWARPConfig() bool
StoreMASQUEConfig() bool
LoadMode() string
StoreMode(mode string) error
@@ -59,6 +60,10 @@ type CacheFile interface {
SaveRuleSet(tag string, set *SavedBinary) error
LoadWARPConfig(tag string) *SavedBinary
SaveWARPConfig(tag string, set *SavedBinary) error
LoadMASQUEConfig(tag string) *SavedBinary
SaveMASQUEConfig(tag string, set *SavedBinary) error
LoadSubscription(tag string) *SavedBinary
SaveSubscription(tag string, sub *SavedBinary) error
}
type SavedBinary struct {
+2 -2
View File
@@ -30,6 +30,7 @@ type UDPInjectableInbound interface {
type InboundRegistry interface {
option.InboundOptionsRegistry
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error)
UnsafeCreate(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error)
}
type InboundManager interface {
@@ -47,8 +48,7 @@ type InboundContext struct {
Network string
Source M.Socksaddr
Destination M.Socksaddr
TunnelSource string
TunnelDestination string
Gateway *netip.Addr
User string
Outbound string
+4
View File
@@ -57,6 +57,10 @@ func (m *Registry) CreateOptions(outboundType string) (any, bool) {
func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
m.access.Lock()
defer m.access.Unlock()
return m.UnsafeCreate(ctx, router, logger, tag, outboundType, options)
}
func (m *Registry) UnsafeCreate(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
constructor, loaded := m.constructor[outboundType]
if !loaded {
return nil, E.New("outbound type not found: " + outboundType)
+1
View File
@@ -35,6 +35,7 @@ type DirectRouteOutbound interface {
type OutboundRegistry interface {
option.OutboundOptionsRegistry
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
UnsafeCreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
}
type OutboundManager interface {
+4
View File
@@ -57,6 +57,10 @@ func (r *Registry) CreateOptions(outboundType string) (any, bool) {
func (r *Registry) CreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) {
r.access.Lock()
defer r.access.Unlock()
return r.UnsafeCreateOutbound(ctx, router, logger, tag, outboundType, options)
}
func (r *Registry) UnsafeCreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) {
constructor, loaded := r.constructors[outboundType]
if !loaded {
return nil, E.New("outbound type not found: " + outboundType)
+51
View File
@@ -0,0 +1,51 @@
package adapter
import (
"context"
"time"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/x/list"
)
type Provider interface {
Type() string
Tag() string
Outbounds() []Outbound
Outbound(tag string) (Outbound, bool)
UpdatedAt() time.Time
HealthCheck(ctx context.Context) (map[string]uint16, error)
RegisterCallback(callback ProviderUpdateCallback) *list.Element[ProviderUpdateCallback]
UnregisterCallback(element *list.Element[ProviderUpdateCallback])
}
type ProviderUpdater interface {
Update() error
}
type ProviderSubscriptionInfo interface {
SubscriptionInfo() SubscriptionInfo
}
type ProviderRegistry interface {
option.ProviderOptionsRegistry
CreateProvider(ctx context.Context, router Router, logFactory log.Factory, tag string, providerType string, options any) (Provider, error)
}
type ProviderManager interface {
Lifecycle
Providers() []Provider
Get(tag string) (Provider, bool)
Remove(tag string) error
Create(ctx context.Context, router Router, logFactory log.Factory, tag string, providerType string, options any) error
}
type SubscriptionInfo struct {
Upload int64
Download int64
Total int64
Expire int64
}
type ProviderUpdateCallback = func(tag string) error
+267
View File
@@ -0,0 +1,267 @@
package provider
import (
"context"
"reflect"
"sync"
"sync/atomic"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/batch"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service"
)
type Adapter struct {
ctx context.Context
outbound adapter.OutboundManager
router adapter.Router
logFactory log.Factory
logger log.ContextLogger
providerType string
providerTag string
outbounds []adapter.Outbound
outboundsByTag map[string]adapter.Outbound
ticker *time.Ticker
checking atomic.Bool
history adapter.URLTestHistoryStorage
callbackAccess sync.Mutex
callbacks list.List[adapter.ProviderUpdateCallback]
link string
enabled bool
timeout time.Duration
interval time.Duration
}
func NewAdapter(ctx context.Context, router adapter.Router, outbound adapter.OutboundManager, logFactory log.Factory, logger log.ContextLogger, providerTag string, providerType string, options option.ProviderHealthCheckOptions) Adapter {
timeout := time.Duration(options.Timeout)
if timeout == 0 {
timeout = 3 * time.Second
}
interval := time.Duration(options.Interval)
if interval == 0 {
interval = 10 * time.Minute
}
if interval < time.Minute {
interval = time.Minute
}
return Adapter{
ctx: ctx,
outbound: outbound,
router: router,
logFactory: logFactory,
logger: logger,
providerType: providerType,
providerTag: providerTag,
enabled: options.Enabled,
link: options.URL,
timeout: timeout,
interval: interval,
}
}
func (a *Adapter) Start() error {
a.history = service.FromContext[adapter.URLTestHistoryStorage](a.ctx)
if a.history == nil {
if clashServer := service.FromContext[adapter.ClashServer](a.ctx); clashServer != nil {
a.history = clashServer.HistoryStorage()
} else {
a.history = urltest.NewHistoryStorage()
}
}
go a.loopCheck()
return nil
}
func (a *Adapter) Type() string {
return a.providerType
}
func (a *Adapter) Tag() string {
return a.providerTag
}
func (a *Adapter) Outbounds() []adapter.Outbound {
return a.outbounds
}
func (a *Adapter) Outbound(tag string) (adapter.Outbound, bool) {
if a.outboundsByTag == nil {
return nil, false
}
detour, ok := a.outboundsByTag[tag]
return detour, ok
}
func (a *Adapter) UpdateOutbounds(oldOpts []option.Outbound, newOpts []option.Outbound) {
a.removeUseless(newOpts)
var (
oldOptByTag = make(map[string]option.Outbound)
outbounds = make([]adapter.Outbound, 0, len(newOpts))
outboundsByTag = make(map[string]adapter.Outbound)
)
for _, opt := range oldOpts {
oldOptByTag[opt.Tag] = opt
}
for i, opt := range newOpts {
var tag string
if opt.Tag != "" {
tag = F.ToString(a.providerTag, "/", opt.Tag)
} else {
tag = F.ToString(a.providerTag, "/", i)
}
outbound, exist := a.outbound.Outbound(tag)
if !exist || !reflect.DeepEqual(opt, oldOptByTag[opt.Tag]) {
err := a.outbound.Create(
adapter.WithContext(a.ctx, &adapter.InboundContext{
Outbound: tag,
}),
a.router,
a.logFactory.NewLogger(F.ToString("outbound/", opt.Type, "[", tag, "]")),
tag,
opt.Type,
opt.Options,
)
if err != nil {
a.logger.Warn(err, " in ", tag, ", skip create this outbound")
continue
}
outbound, _ = a.outbound.Outbound(tag)
}
outbounds = append(outbounds, outbound)
outboundsByTag[tag] = outbound
}
if a.enabled && a.history != nil {
go a.HealthCheck(a.ctx)
}
a.outbounds = outbounds
a.outboundsByTag = outboundsByTag
}
func (a *Adapter) HealthCheck(ctx context.Context) (map[string]uint16, error) {
if a.ticker != nil {
a.ticker.Reset(a.interval)
}
return a.healthcheck(ctx)
}
func (a *Adapter) RegisterCallback(callback adapter.ProviderUpdateCallback) *list.Element[adapter.ProviderUpdateCallback] {
a.callbackAccess.Lock()
defer a.callbackAccess.Unlock()
return a.callbacks.PushBack(callback)
}
func (a *Adapter) UnregisterCallback(element *list.Element[adapter.ProviderUpdateCallback]) {
a.callbackAccess.Lock()
defer a.callbackAccess.Unlock()
a.callbacks.Remove(element)
}
func (a *Adapter) UpdateGroups() {
for element := a.callbacks.Front(); element != nil; element = element.Next() {
element.Value(a.providerTag)
}
}
func (a *Adapter) Close() error {
if a.ticker != nil {
a.ticker.Stop()
}
outbounds := a.outbounds
a.outbounds = nil
var err error
for _, ob := range outbounds {
if err2 := a.outbound.Remove(ob.Tag()); err2 != nil {
err = E.Append(err, err2, func(err error) error {
return E.Cause(err, "close outbound [", ob.Tag(), "]")
})
}
}
return err
}
func (a *Adapter) loopCheck() {
if !a.enabled {
return
}
a.ticker = time.NewTicker(a.interval)
a.healthcheck(a.ctx)
for {
select {
case <-a.ctx.Done():
return
case <-a.ticker.C:
a.healthcheck(a.ctx)
}
}
}
func (a *Adapter) healthcheck(ctx context.Context) (map[string]uint16, error) {
result := make(map[string]uint16)
if a.checking.Swap(true) {
return result, nil
}
defer a.checking.Store(false)
b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10))
var resultAccess sync.Mutex
checked := make(map[string]bool)
for _, detour := range a.outbounds {
tag := detour.Tag()
if checked[tag] {
continue
}
checked[tag] = true
b.Go(tag, func() (any, error) {
ctx, cancel := context.WithTimeout(a.ctx, a.timeout)
defer cancel()
t, err := urltest.URLTest(ctx, a.link, detour)
if err != nil {
a.logger.Debug("outbound ", tag, " unavailable: ", err)
a.history.DeleteURLTestHistory(tag)
} else {
a.logger.Debug("outbound ", tag, " available: ", t, "ms")
a.history.StoreURLTestHistory(tag, &adapter.URLTestHistory{
Time: time.Now(),
Delay: t,
})
resultAccess.Lock()
result[tag] = t
resultAccess.Unlock()
}
return nil, nil
})
}
b.Wait()
return result, nil
}
func (a *Adapter) removeUseless(newOpts []option.Outbound) {
if len(a.outbounds) == 0 {
return
}
exists := make(map[string]bool)
for i, opt := range newOpts {
var tag string
if opt.Tag != "" {
tag = F.ToString(a.providerTag, "/", opt.Tag)
} else {
tag = F.ToString(a.providerTag, "/", i)
}
exists[tag] = true
}
for _, opt := range a.outbounds {
if !exists[opt.Tag()] {
if err := a.outbound.Remove(opt.Tag()); err != nil {
a.logger.Error(err, "close outbound [", opt.Tag(), "]")
}
}
}
}
+157
View File
@@ -0,0 +1,157 @@
package provider
import (
"context"
"io"
"os"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
var _ adapter.ProviderManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.ProviderRegistry
access sync.Mutex
started bool
stage adapter.StartStage
providers []adapter.Provider
providerByTag map[string]adapter.Provider
wg sync.WaitGroup
}
func NewManager(logger logger.ContextLogger, registry adapter.ProviderRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
providerByTag: make(map[string]adapter.Provider),
}
}
func (m *Manager) Initialize() {
}
func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
providers := m.providers
m.access.Unlock()
for _, provider := range providers {
err := adapter.LegacyStart(provider, stage)
if err != nil {
return E.Cause(err, stage, " provider/", provider.Type(), "[", provider.Tag(), "]")
}
}
return nil
}
func (m *Manager) Close() error {
monitor := taskmonitor.New(m.logger, C.StopTimeout)
m.access.Lock()
if !m.started {
m.access.Unlock()
return nil
}
m.started = false
providers := m.providers
m.providers = nil
m.access.Unlock()
var err error
for _, provider := range providers {
if closer, isCloser := provider.(io.Closer); isCloser {
monitor.Start("close provider/", provider.Type(), "[", provider.Tag(), "]")
err = E.Append(err, closer.Close(), func(err error) error {
return E.Cause(err, "close provider/", provider.Type(), "[", provider.Tag(), "]")
})
monitor.Finish()
}
}
return nil
}
func (m *Manager) Providers() []adapter.Provider {
m.access.Lock()
defer m.access.Unlock()
return m.providers
}
func (m *Manager) Get(tag string) (adapter.Provider, bool) {
m.access.Lock()
provider, found := m.providerByTag[tag]
m.access.Unlock()
return provider, found
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
provider, found := m.providerByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.providerByTag, tag)
index := common.Index(m.providers, func(it adapter.Provider) bool {
return it == provider
})
if index == -1 {
panic("invalid provider index")
}
m.providers = append(m.providers[:index], m.providers[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return common.Close(provider)
}
return nil
}
func (m *Manager) Create(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, providerType string, options any) error {
if tag == "" {
return os.ErrInvalid
}
provider, err := m.registry.CreateProvider(ctx, router, logFactory, tag, providerType, options)
if err != nil {
return err
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
for _, stage := range adapter.ListStartStages {
err = adapter.LegacyStart(provider, stage)
if err != nil {
return E.Cause(err, stage, " provider/", provider.Type(), "[", provider.Tag(), "]")
}
}
}
if existsProvider, loaded := m.providerByTag[tag]; loaded {
if m.started {
err = common.Close(existsProvider)
if err != nil {
return E.Cause(err, "close provider", provider.Type(), "[", existsProvider.Tag(), "]")
}
}
existsIndex := common.Index(m.providers, func(it adapter.Provider) bool {
return it == existsProvider
})
if existsIndex == -1 {
panic("invalid provider index")
}
m.providers = append(m.providers[:existsIndex], m.providers[existsIndex+1:]...)
}
m.providers = append(m.providers, provider)
m.providerByTag[tag] = provider
return nil
}
+72
View File
@@ -0,0 +1,72 @@
package provider
import (
"context"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, options T) (adapter.Provider, error)
func Register[Options any](registry *Registry, providerType string, constructor ConstructorFunc[Options]) {
registry.register(providerType, func() any {
return new(Options)
}, func(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, rawOptions any) (adapter.Provider, error) {
var options *Options
if rawOptions != nil {
options = rawOptions.(*Options)
}
return constructor(ctx, router, logFactory, tag, common.PtrValueOrDefault(options))
})
}
var _ adapter.ProviderRegistry = (*Registry)(nil)
type (
optionsConstructorFunc func() any
constructorFunc func(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, options any) (adapter.Provider, error)
)
type Registry struct {
access sync.Mutex
optionsType map[string]optionsConstructorFunc
constructors map[string]constructorFunc
}
func NewRegistry() *Registry {
return &Registry{
optionsType: make(map[string]optionsConstructorFunc),
constructors: make(map[string]constructorFunc),
}
}
func (r *Registry) CreateOptions(providerType string) (any, bool) {
r.access.Lock()
defer r.access.Unlock()
optionsConstructor, loaded := r.optionsType[providerType]
if !loaded {
return nil, false
}
return optionsConstructor(), true
}
func (r *Registry) CreateProvider(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, providerType string, options any) (adapter.Provider, error) {
r.access.Lock()
defer r.access.Unlock()
constructor, loaded := r.constructors[providerType]
if !loaded {
return nil, E.New("provider type not found: '" + providerType + "'")
}
return constructor(ctx, router, logFactory, tag, options)
}
func (r *Registry) register(providerType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
r.access.Lock()
defer r.access.Unlock()
r.optionsType[providerType] = optionsConstructor
r.constructors[providerType] = constructor
}
+44 -4
View File
@@ -12,6 +12,7 @@ import (
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/adapter/provider"
boxService "github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/common/certificate"
"github.com/sagernet/sing-box/common/dialer"
@@ -44,6 +45,7 @@ type Box struct {
endpoint *endpoint.Manager
inbound *inbound.Manager
outbound *outbound.Manager
provider *provider.Manager
service *boxService.Manager
dnsTransport *dns.TransportManager
dnsRouter *dns.Router
@@ -64,6 +66,7 @@ func Context(
inboundRegistry adapter.InboundRegistry,
outboundRegistry adapter.OutboundRegistry,
endpointRegistry adapter.EndpointRegistry,
providerRegistry adapter.ProviderRegistry,
dnsTransportRegistry adapter.DNSTransportRegistry,
serviceRegistry adapter.ServiceRegistry,
) context.Context {
@@ -82,6 +85,11 @@ func Context(
ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry)
ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry)
}
if service.FromContext[option.ProviderOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.ProviderRegistry](ctx) == nil {
ctx = service.ContextWith[option.ProviderOptionsRegistry](ctx, providerRegistry)
ctx = service.ContextWith[adapter.ProviderRegistry](ctx, providerRegistry)
}
if service.FromContext[adapter.DNSTransportRegistry](ctx) == nil {
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
@@ -104,6 +112,7 @@ func New(options Options) (*Box, error) {
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
providerRegistry := service.FromContext[adapter.ProviderRegistry](ctx)
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
@@ -116,6 +125,9 @@ func New(options Options) (*Box, error) {
if outboundRegistry == nil {
return nil, E.New("missing outbound registry in context")
}
if providerRegistry == nil {
return nil, E.New("missing provider registry in context")
}
if dnsTransportRegistry == nil {
return nil, E.New("missing DNS transport registry in context")
}
@@ -160,6 +172,7 @@ func New(options Options) (*Box, error) {
if err != nil {
return nil, E.Cause(err, "create log factory")
}
service.MustRegister[log.Factory](ctx, logFactory)
var internalServices []adapter.LifecycleService
certificateOptions := common.PtrValueOrDefault(options.Certificate)
@@ -180,11 +193,13 @@ func New(options Options) (*Box, error) {
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
providerManager := provider.NewManager(logFactory.NewLogger("provider"), providerRegistry)
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
service.MustRegister[adapter.ProviderManager](ctx, providerManager)
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
@@ -275,6 +290,10 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize inbound[", i, "]")
}
}
options.Outbounds = append(options.Outbounds, option.Outbound{
Tag: "Compatible",
Type: C.TypeDirect,
})
for i, outboundOptions := range options.Outbounds {
var tag string
if outboundOptions.Tag != "" {
@@ -301,6 +320,25 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize outbound[", i, "]")
}
}
for i, providerOptions := range options.Providers {
var tag string
if providerOptions.Tag != "" {
tag = providerOptions.Tag
} else {
tag = F.ToString(i)
}
err = providerManager.Create(
ctx,
router,
logFactory,
tag,
providerOptions.Type,
providerOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize provider[", i, "]")
}
}
for i, serviceOptions := range options.Services {
var tag string
if serviceOptions.Tag != "" {
@@ -391,6 +429,7 @@ func New(options Options) (*Box, error) {
endpoint: endpointManager,
inbound: inboundManager,
outbound: outboundManager,
provider: providerManager,
dnsTransport: dnsTransportManager,
service: serviceManager,
dnsRouter: dnsRouter,
@@ -454,11 +493,11 @@ func (s *Box) preStart() error {
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.provider, s.service)
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.provider, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
if err != nil {
return err
}
@@ -478,7 +517,7 @@ func (s *Box) start() error {
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.provider, s.service)
if err != nil {
return err
}
@@ -486,7 +525,7 @@ func (s *Box) start() error {
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.provider, s.service)
if err != nil {
return err
}
@@ -512,6 +551,7 @@ func (s *Box) Close() error {
{"service", s.service},
{"endpoint", s.endpoint},
{"inbound", s.inbound},
{"provider", s.provider},
{"outbound", s.outbound},
{"router", s.router},
{"connection", s.connection},
+194
View File
@@ -0,0 +1,194 @@
// Command admin_panel_pack post-processes a directory of built SPA
// assets so it can be served straight from the Go binary via //go:embed.
// It does *not* generate any Go source any more — that responsibility
// moved to the embed directive in service/admin_panel/service.go.
//
// Three transformations happen here, all in-place inside the supplied
// directory:
//
// 1. Legacy WOFF (1.0) fallback fonts are deleted. Every browser made
// after 2014 reads WOFF2 natively, so shipping both formats roughly
// doubles the embedded font payload for no real-world benefit. The
// matching `,url(*.woff) format("woff")` segments are stripped from
// the bundled CSS in step (2) so the @font-face rules don't reference
// files that aren't shipped.
// 2. Bundled CSS is rewritten to drop those WOFF URL fragments.
// 3. Compressible text assets (.html, .css, .js, .svg, .json, .map) are
// pre-gzipped as companion `*.gz` files. The HTTP handler then either
// passes those bytes through verbatim with Content-Encoding: gzip or
// falls back to the raw file for the rare client that does not
// advertise gzip support — no on-line compression, no surprises.
// Already-compressed formats (.woff2, fonts, images) are skipped: gzip
// can't shrink them and the duplicate would only inflate the binary.
package main
import (
"bytes"
"compress/gzip"
"flag"
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)
func main() {
dir := flag.String("dir", "service/admin_panel/dist", "directory of built SPA assets to post-process in place")
flag.Parse()
woffDropped, err := pruneWoff(*dir)
if err != nil {
fail("prune woff: %v", err)
}
cssRewritten, err := rewriteCSS(*dir)
if err != nil {
fail("rewrite css: %v", err)
}
stats, err := gzipText(*dir)
if err != nil {
fail("gzip text: %v", err)
}
fmt.Fprintf(os.Stderr, "post-processed %s: dropped %d woff, rewrote %d css, gzipped %d files (%d→%d bytes)\n",
*dir, woffDropped, cssRewritten, stats.gzipped, stats.totalRaw, stats.totalGz)
}
// pruneWoff deletes every legacy *.woff (WOFF 1.0) font under dir. The
// bundled CSS still references them on entry; rewriteCSS drops those
// references in a separate pass so the two operations stay independently
// testable.
func pruneWoff(dir string) (int, error) {
var n int
err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.IsDir() || !strings.EqualFold(filepath.Ext(p), ".woff") {
return nil
}
if err := os.Remove(p); err != nil {
return err
}
n++
return nil
})
return n, err
}
// woffRefAfterRE / woffRefBeforeRE match a single WOFF 1.0 entry inside a
// Vite-bundled CSS `src:` declaration. Vite minifies the rule to
// `src:url(./X.woff2) format("woff2"),url(./X.woff) format("woff");` so the
// "after" regex (the common case) eats the *leading* comma + woff entry,
// leaving only the woff2 source. We also handle the rare reverse ordering
// in a second pass.
var (
woffRefAfterRE = regexp.MustCompile(`,\s*url\([^)]*\.woff\)\s*format\(["']woff["']\)`)
woffRefBeforeRE = regexp.MustCompile(`url\([^)]*\.woff\)\s*format\(["']woff["']\)\s*,\s*`)
)
// rewriteCSS drops every reference to a *.woff URL from every *.css file
// under dir. Pairs naturally with pruneWoff: after both passes, no font
// URL in the bundle points at a file that isn't shipped.
func rewriteCSS(dir string) (int, error) {
var n int
err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.IsDir() || !strings.EqualFold(filepath.Ext(p), ".css") {
return nil
}
data, err := os.ReadFile(p)
if err != nil {
return err
}
out := woffRefAfterRE.ReplaceAll(data, nil)
out = woffRefBeforeRE.ReplaceAll(out, nil)
if bytes.Equal(out, data) {
return nil
}
if err := os.WriteFile(p, out, 0o644); err != nil {
return err
}
n++
return nil
})
return n, err
}
// gzipExts is the set of file extensions for which a `.gz` companion is
// generated. Anything not on this list is left alone — woff2/png/jpeg/etc.
// are already compressed, so gzip can only inflate them slightly while
// doubling the embedded payload.
var gzipExts = map[string]bool{
".html": true,
".css": true,
".js": true,
".mjs": true,
".svg": true,
".json": true,
".map": true,
".txt": true,
".xml": true,
".wasm": true,
}
type gzipStats struct {
gzipped int
totalRaw int64
totalGz int64
}
// gzipText produces a `<file>.gz` companion next to every text-like asset
// in dir, using gzip.BestCompression. The companion is dropped if the
// compressed bytes don't save at least 10 % over the raw file — same
// heuristic we used in the previous (Go-source-emitting) generation, just
// applied to disk files now.
func gzipText(dir string) (gzipStats, error) {
var stats gzipStats
err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.IsDir() {
return nil
}
ext := strings.ToLower(filepath.Ext(p))
if ext == ".gz" || !gzipExts[ext] {
return nil
}
raw, err := os.ReadFile(p)
if err != nil {
return err
}
var buf bytes.Buffer
w, err := gzip.NewWriterLevel(&buf, gzip.BestCompression)
if err != nil {
return err
}
// Reproducible: no mtime, no OS marker.
w.ModTime = time.Time{}
w.OS = 0xff
if _, err := w.Write(raw); err != nil {
return err
}
if err := w.Close(); err != nil {
return err
}
if buf.Len() > len(raw)*9/10 {
return nil
}
stats.gzipped++
stats.totalRaw += int64(len(raw))
stats.totalGz += int64(buf.Len())
return os.WriteFile(p+".gz", buf.Bytes(), 0o644)
})
return stats, err
}
func fail(format string, args ...any) {
fmt.Fprintf(os.Stderr, "admin_panel_pack: "+format+"\n", args...)
os.Exit(1)
}
+1 -1
View File
@@ -63,7 +63,7 @@ func init() {
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0")
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0")
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "badlinkname", "tfogo_checklinkname0")
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_masque", "with_mtproxy", "with_utls", "with_naive_outbound", "with_clash_api", "badlinkname", "tfogo_checklinkname0")
darwinTags = append(darwinTags, "with_dhcp", "grpcnotrace")
// memcTags = append(memcTags, "with_tailscale")
sharedTags = append(sharedTags, "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird")
Binary file not shown.
-2
View File
@@ -1,5 +1,3 @@
//go:build with_quic
package main
import (
+1 -1
View File
@@ -29,7 +29,7 @@ type RawHalfConn struct {
func NewRawHalfConn(rawHalfConn reflect.Value, methods *Methods) (*RawHalfConn, error) {
halfConn := &RawHalfConn{
pointer: (unsafe.Pointer)(rawHalfConn.UnsafeAddr()),
pointer: unsafe.Pointer(rawHalfConn.UnsafeAddr()),
methods: methods,
}
+74
View File
@@ -0,0 +1,74 @@
package byteformats
import (
"fmt"
"math"
)
var (
unitNames = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
iUnitNames = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
kUnitNames = []string{"kB", "MB", "GB", "TB", "PB", "EB"}
kiUnitNames = []string{"KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
)
func formatBytes(s uint64, base float64, sizes []string) string {
if s < 10 {
return fmt.Sprintf("%d B", s)
}
e := math.Floor(logn(float64(s), base))
suffix := sizes[int(e)]
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
f := "%.0f %s"
if val < 10 {
f = "%.1f %s"
}
return fmt.Sprintf(f, val, suffix)
}
func formatKBytes(s uint64, base float64, sizes []string) string {
if s == 0 {
return fmt.Sprintf("0 %s", sizes[0])
}
e := math.Floor(logn(float64(s), base))
if e < 1 {
e = 1
}
suffix := sizes[int(e)-1]
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
f := "%.0f %s"
if val < 10 {
f = "%.1f %s"
}
return fmt.Sprintf(f, val, suffix)
}
func logn(n, b float64) float64 {
return math.Log(n) / math.Log(b)
}
func FormatBytes(s uint64) string {
return formatBytes(s, 1000, unitNames)
}
func FormatMemoryBytes(s uint64) string {
return formatBytes(s, 1024, unitNames)
}
func FormatIBytes(s uint64) string {
return formatBytes(s, 1024, iUnitNames)
}
func FormatKBytes(s uint64) string {
return formatKBytes(s, 1000, kUnitNames)
}
func FormatMemoryKBytes(s uint64) string {
return formatKBytes(s, 1024, kUnitNames)
}
func FormatKIBytes(s uint64) string {
return formatKBytes(s, 1024, kiUnitNames)
}
+218
View File
@@ -0,0 +1,218 @@
package byteformats
import (
"encoding/json"
"fmt"
"strconv"
"strings"
)
const (
Byte = 1 << (iota * 10)
KiByte
MiByte
GiByte
TiByte
PiByte
EiByte
)
const (
KByte = Byte * 1000
MByte = KByte * 1000
GByte = MByte * 1000
TByte = GByte * 1000
PByte = TByte * 1000
EByte = PByte * 1000
)
var unitValueTable = map[string]uint64{
"b": Byte,
"k": KByte,
"kb": KByte,
"ki": KiByte,
"kib": KiByte,
"m": MByte,
"mb": MByte,
"mi": MiByte,
"mib": MiByte,
"g": GByte,
"gb": GByte,
"gi": GiByte,
"gib": GiByte,
"t": TByte,
"tb": TByte,
"ti": TiByte,
"tib": TiByte,
"p": PByte,
"pb": PByte,
"pi": PiByte,
"pib": PiByte,
"e": EByte,
"eb": EByte,
"ei": EiByte,
"eib": EiByte,
}
var memoryUnitValueTable = map[string]uint64{
"b": Byte,
"k": KiByte,
"kb": KiByte,
"m": MiByte,
"mb": MiByte,
"g": GiByte,
"gb": GiByte,
"t": TiByte,
"tb": TiByte,
"p": PiByte,
"pb": PiByte,
"e": EiByte,
"eb": EiByte,
}
var networkUnitValueTable = map[string]uint64{
"Bps": Byte,
"Kbps": KByte / 8,
"KBps": KByte,
"Mbps": MByte / 8,
"MBps": MByte,
"Gbps": GByte / 8,
"GBps": GByte,
"Tbps": TByte / 8,
"TBps": TByte,
"Pbps": PByte / 8,
"PBps": PByte,
"Ebps": EByte / 8,
"EBps": EByte,
}
type rawBytes struct {
value uint64
unit string
unitValue uint64
}
func (b rawBytes) MarshalJSON() ([]byte, error) {
if b.unit == "" {
return json.Marshal(b.value)
}
return json.Marshal(strconv.FormatUint(b.value/b.unitValue, 10) + b.unit)
}
func parseUnit(b *rawBytes, unitTable map[string]uint64, caseSensitive bool, bytes []byte) error {
var intValue int64
err := json.Unmarshal(bytes, &intValue)
if err == nil {
b.value = uint64(intValue)
b.unit = ""
b.unitValue = 1
return nil
}
var stringValue string
err = json.Unmarshal(bytes, &stringValue)
if err != nil {
return err
}
if strings.TrimSpace(stringValue) == "" {
b.value = 0
b.unit = ""
b.unitValue = 1
return nil
}
unitIndex := 0
for i, c := range stringValue {
if c < '0' || c > '9' {
unitIndex = i
break
}
}
if unitIndex == 0 {
return fmt.Errorf("invalid format: %s", stringValue)
}
value, err := strconv.ParseUint(stringValue[:unitIndex], 10, 64)
if err != nil {
return fmt.Errorf("parse %s: %w", stringValue[:unitIndex], err)
}
rawUnit := stringValue[unitIndex:]
var unit string
if caseSensitive {
unit = strings.TrimSpace(rawUnit)
} else {
unit = strings.TrimSpace(strings.ToLower(rawUnit))
}
unitValue, loaded := unitTable[unit]
if !loaded {
return fmt.Errorf("unsupported unit: %s", rawUnit)
}
b.value = value * unitValue
b.unit = rawUnit
b.unitValue = unitValue
return nil
}
type Bytes struct {
rawBytes
}
func (b *Bytes) Value() uint64 {
if b == nil {
return 0
}
return b.value
}
func (b *Bytes) UnmarshalJSON(bytes []byte) error {
return parseUnit(&b.rawBytes, unitValueTable, false, bytes)
}
type MemoryBytes struct {
rawBytes
}
func (m *MemoryBytes) Value() uint64 {
if m == nil {
return 0
}
return m.value
}
func (m *MemoryBytes) UnmarshalJSON(bytes []byte) error {
return parseUnit(&m.rawBytes, memoryUnitValueTable, false, bytes)
}
type NetworkBytes struct {
rawBytes
}
func (n *NetworkBytes) Value() uint64 {
if n == nil {
return 0
}
return n.value
}
func (n *NetworkBytes) UnmarshalJSON(bytes []byte) error {
return parseUnit(&n.rawBytes, networkUnitValueTable, true, bytes)
}
type NetworkBytesCompat struct {
rawBytes
}
func (n *NetworkBytesCompat) Value() uint64 {
if n == nil {
return 0
}
return n.value
}
func (n *NetworkBytesCompat) UnmarshalJSON(bytes []byte) error {
err := parseUnit(&n.rawBytes, networkUnitValueTable, true, bytes)
if err != nil {
newErr := parseUnit(&n.rawBytes, unitValueTable, false, bytes)
if newErr == nil {
return nil
}
}
return err
}
+114
View File
@@ -0,0 +1,114 @@
package byteformats_test
import (
"encoding/json"
"testing"
"github.com/sagernet/sing-box/common/byteformats"
"github.com/stretchr/testify/require"
)
func TestNetworkBytes(t *testing.T) {
t.Parallel()
testMap := map[string]uint64{
"1 Bps": byteformats.Byte,
"1 Kbps": byteformats.KByte / 8,
"1 KBps": byteformats.KByte,
"1 Mbps": byteformats.MByte / 8,
"1 MBps": byteformats.MByte,
"1 Gbps": byteformats.GByte / 8,
"1 GBps": byteformats.GByte,
"1 Tbps": byteformats.TByte / 8,
"1 TBps": byteformats.TByte,
"1 Pbps": byteformats.PByte / 8,
"1 PBps": byteformats.PByte,
"1k": byteformats.KByte,
"1m": byteformats.MByte,
}
for k, v := range testMap {
var nb byteformats.NetworkBytesCompat
require.NoError(t, json.Unmarshal([]byte("\""+k+"\""), &nb))
require.Equal(t, v, nb.Value())
b, err := json.Marshal(nb)
require.NoError(t, err)
require.Equal(t, "\""+k+"\"", string(b))
}
}
func TestMemoryBytes(t *testing.T) {
t.Parallel()
testMap := map[string]uint64{
"1 B": byteformats.Byte,
"1 KB": byteformats.KiByte,
"1 MB": byteformats.MiByte,
"1 GB": byteformats.GiByte,
"1 TB": byteformats.TiByte,
"1 PB": byteformats.PiByte,
}
for k, v := range testMap {
var mb byteformats.MemoryBytes
require.NoError(t, json.Unmarshal([]byte("\""+k+"\""), &mb))
require.Equal(t, v, mb.Value())
b, err := json.Marshal(mb)
require.NoError(t, err)
require.Equal(t, "\""+k+"\"", string(b))
}
}
func TestDefaultBytes(t *testing.T) {
t.Parallel()
testMap := map[string]uint64{
"1 B": byteformats.Byte,
"1 KB": byteformats.KByte,
"1 KiB": byteformats.KiByte,
"1 MB": byteformats.MByte,
"1 MiB": byteformats.MiByte,
"1 GB": byteformats.GByte,
"1 GiB": byteformats.GiByte,
"1 TB": byteformats.TByte,
"1 TiB": byteformats.TiByte,
"1 PB": byteformats.PByte,
"1 PiB": byteformats.PiByte,
"1 EB": byteformats.EByte,
"1 EiB": byteformats.EiByte,
"1k": byteformats.KByte,
"1m": byteformats.MByte,
"1g": byteformats.GByte,
"1t": byteformats.TByte,
"1p": byteformats.PByte,
"1e": byteformats.EByte,
"1K": byteformats.KByte,
"1M": byteformats.MByte,
"1G": byteformats.GByte,
"1T": byteformats.TByte,
"1P": byteformats.PByte,
"1E": byteformats.EByte,
"1Ki": byteformats.KiByte,
"1Mi": byteformats.MiByte,
"1Gi": byteformats.GiByte,
"1Ti": byteformats.TiByte,
"1Pi": byteformats.PiByte,
"1Ei": byteformats.EiByte,
"1KiB": byteformats.KiByte,
"1MiB": byteformats.MiByte,
"1GiB": byteformats.GiByte,
"1TiB": byteformats.TiByte,
"1PiB": byteformats.PiByte,
"1EiB": byteformats.EiByte,
"1kB": byteformats.KByte,
"1mB": byteformats.MByte,
"1gB": byteformats.GByte,
"1tB": byteformats.TByte,
"1pB": byteformats.PByte,
"1eB": byteformats.EByte,
}
for k, v := range testMap {
var mb byteformats.Bytes
require.NoError(t, json.Unmarshal([]byte("\""+k+"\""), &mb))
require.Equal(t, v, mb.Value())
b, err := json.Marshal(mb)
require.NoError(t, err)
require.Equal(t, "\""+k+"\"", string(b))
}
}
+64 -24
View File
@@ -1,15 +1,12 @@
package cloudflare
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/tidwall/gjson"
)
type CloudflareApi struct {
@@ -25,50 +22,93 @@ func NewCloudflareApi(opts ...CloudflareApiOption) *CloudflareApi {
}
func (api *CloudflareApi) CreateProfile(ctx context.Context, publicKey string) (*CloudflareProfile, error) {
request, err := http.NewRequest("POST", "https://api.cloudflareclient.com/v0i1909051800/reg", strings.NewReader(
fmt.Sprintf(
"{\"install_id\":\"\",\"tos\":\"%s\",\"key\":\"%s\",\"fcm_token\":\"\",\"type\":\"ios\",\"locale\":\"en_US\"}",
time.Now().Format("2006-01-02T15:04:05.000Z"),
publicKey,
),
))
serial, err := GenerateRandomAndroidSerial()
if err != nil {
return nil, fmt.Errorf("failed to generate serial: %v", err)
}
data := Registration{
Key: publicKey,
InstallID: "",
FcmToken: "",
Tos: TimeAsCfString(time.Now()),
Model: "PC",
Serial: serial,
OsVersion: "",
KeyType: KeyTypeWg,
TunType: TunTypeWg,
Locale: "en-US",
}
jsonData, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("failed to marshal json: %v", err)
}
request, err := http.NewRequest("POST", ApiUrl+"/"+ApiVersion+"/reg", bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
for k, v := range Headers {
request.Header.Set(k, v)
}
response, err := api.client.Do(request.WithContext(ctx))
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != 200 {
return nil, fmt.Errorf("status code is not 200")
}
content, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to register: %v", response.StatusCode)
}
profile := new(CloudflareProfile)
return profile, json.NewDecoder(strings.NewReader(gjson.Get(string(content), "result").Raw)).Decode(profile)
return profile, json.NewDecoder(response.Body).Decode(profile)
}
func (api *CloudflareApi) GetProfile(ctx context.Context, authToken string, id string) (*CloudflareProfile, error) {
request, err := http.NewRequest("GET", "https://api.cloudflareclient.com/v0i1909051800/reg/"+id, nil)
func (api *CloudflareApi) EnrollKey(ctx context.Context, authToken string, id string, keyType, tunType, publicKey string) (*CloudflareProfile, error) {
deviceUpdate := DeviceUpdate{
Name: "PC",
Key: publicKey,
KeyType: keyType,
TunType: tunType,
}
jsonData, err := json.Marshal(deviceUpdate)
if err != nil {
return nil, fmt.Errorf("failed to marshal json: %v", err)
}
request, err := http.NewRequest("PATCH", ApiUrl+"/"+ApiVersion+"/reg/"+id, bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
for k, v := range Headers {
request.Header.Set(k, v)
}
request.Header.Set("Authorization", "Bearer "+authToken)
response, err := api.client.Do(request.WithContext(ctx))
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != 200 {
return nil, fmt.Errorf("status code is not 200")
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to enroll key: %v", response.StatusCode)
}
content, err := io.ReadAll(response.Body)
profile := new(CloudflareProfile)
return profile, json.NewDecoder(response.Body).Decode(profile)
}
func (api *CloudflareApi) GetProfile(ctx context.Context, authToken string, id string) (*CloudflareProfile, error) {
request, err := http.NewRequest("GET", ApiUrl+"/"+ApiVersion+"/reg/"+id, nil)
if err != nil {
return nil, err
}
for k, v := range Headers {
request.Header.Set(k, v)
}
request.Header.Set("Authorization", "Bearer "+authToken)
response, err := api.client.Do(request.WithContext(ctx))
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get profile: %v", response.StatusCode)
}
profile := new(CloudflareProfile)
return profile, json.NewDecoder(strings.NewReader(gjson.Get(string(content), "result").Raw)).Decode(profile)
return profile, json.NewDecoder(response.Body).Decode(profile)
}
+25
View File
@@ -0,0 +1,25 @@
package cloudflare
const (
ApiUrl = "https://api.cloudflareclient.com"
ApiVersion = "v0a4471"
ConnectSNI = "consumer-masque.cloudflareclient.com"
// unused for now
ZeroTierSNI = "zt-masque.cloudflareclient.com"
ConnectURI = "https://cloudflareaccess.com"
DefaultModel = "PC"
KeyTypeWg = "curve25519"
TunTypeWg = "wireguard"
KeyTypeMasque = "secp256r1"
TunTypeMasque = "masque"
DefaultLocale = "en_US"
DefaultEndpointH2V4 = "162.159.198.2"
DefaultEndpointH2V6 = ""
)
var Headers = map[string]string{
"User-Agent": "WARP for Android",
"CF-Client-Version": "a-6.35-4471",
"Content-Type": "application/json; charset=UTF-8",
"Connection": "Keep-Alive",
}
+132
View File
@@ -0,0 +1,132 @@
package cloudflare
import (
"strings"
)
type Registration struct {
Key string `json:"key"`
InstallID string `json:"install_id"`
FcmToken string `json:"fcm_token"`
Tos string `json:"tos"`
Model string `json:"model"`
Serial string `json:"serial_number"`
OsVersion string `json:"os_version"`
KeyType string `json:"key_type"`
TunType string `json:"tunnel_type"`
Locale string `json:"locale"`
}
type CloudflareProfile struct {
ID string `json:"id"`
Type string `json:"type"`
Model string `json:"model"`
Name string `json:"name"`
Key string `json:"key"`
KeyType string `json:"key_type"`
TunType string `json:"tunnel_type"`
Account Account `json:"account"`
Config Config `json:"config"`
// WarpEnabled not set for ZeroTier
WarpEnabled bool `json:"warp_enabled,omitempty"`
// Waitlist not set for ZeroTier
Waitlist bool `json:"waitlist_enabled,omitempty"`
Created string `json:"created"`
Updated string `json:"updated"`
// Tos not set for ZeroTier
Tos string `json:"tos,omitempty"`
// Place not set for ZeroTier
Place int `json:"place,omitempty"`
Locale string `json:"locale"`
// Enabled not set for ZeroTier
Enabled bool `json:"enabled,omitempty"`
InstallID string `json:"install_id"`
// Token only set for /reg call
Token string `json:"token,omitempty"`
FcmToken string `json:"fcm_token"`
// SerialNumber not set for ZeroTier
SerialNumber string `json:"serial_number,omitempty"`
Policy Policy `json:"policy"`
}
type Account struct {
ID string `json:"id"`
AccountType string `json:"account_type"`
// Created not set for ZeroTier
Created string `json:"created,omitempty"`
// Updated not set for ZeroTier
Updated string `json:"updated,omitempty"`
// Managed only set for ZeroTier
Managed string `json:"managed,omitempty"`
// Organization only set for ZeroTier
Organization string `json:"organization,omitempty"`
// PremiumData not set for ZeroTier
PremiumData int `json:"premium_data,omitempty"`
// Quota not set for ZeroTier
Quota int `json:"quota,omitempty"`
// WarpPlus not set for ZeroTier
WarpPlus bool `json:"warp_plus,omitempty"`
// ReferralCode not set for ZeroTier
ReferralCount int `json:"referral_count,omitempty"`
// ReferralRenewalCount not set for ZeroTier
ReferralRenewalCount int `json:"referral_renewal_countdown,omitempty"`
// Role not set for ZeroTier
Role string `json:"role,omitempty"`
// License not set for ZeroTier
License string `json:"license,omitempty"`
}
type Config struct {
ClientID string `json:"client_id"`
Peers []Peer `json:"peers"`
Interface struct {
Addresses struct {
V4 string `json:"v4"`
V6 string `json:"v6"`
} `json:"addresses"`
} `json:"interface"`
Services struct {
HTTPProxy string `json:"http_proxy"`
} `json:"services"`
}
type Peer struct {
PublicKey string `json:"public_key"`
Endpoint struct {
V4 string `json:"v4"`
V6 string `json:"v6"`
Host string `json:"host"`
Ports []int `json:"ports"`
} `json:"endpoint"`
}
type Policy struct {
TunnelProtocol string `json:"tunnel_protocol"`
}
type DeviceUpdate struct {
Key string `json:"key"`
KeyType string `json:"key_type"`
TunType string `json:"tunnel_type"`
Name string `json:"name,omitempty"`
}
type APIError struct {
Result interface{} `json:"result"`
Success bool `json:"success"`
Errors []ErrorInfo `json:"errors"`
Messages []string `json:"messages"`
}
type ErrorInfo struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (e *APIError) Error() string {
errors := make([]string, len(e.Errors))
for i, err := range e.Errors {
errors[i] = err.Message
}
return strings.Join(errors, ",")
}
+2
View File
@@ -4,12 +4,14 @@ import (
"context"
"net"
"net/http"
"time"
)
type CloudflareApiOption func(api *CloudflareApi)
func WithDialContext(dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) CloudflareApiOption {
return func(api *CloudflareApi) {
api.client.Timeout = 30 * time.Second
api.client.Transport = &http.Transport{
DialContext: dialContext,
}
-64
View File
@@ -1,64 +0,0 @@
package cloudflare
import "time"
type CloudflareProfile struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Key string `json:"key"`
Account struct {
ID string `json:"id"`
AccountType string `json:"account_type"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
PremiumData int `json:"premium_data"`
Quota int `json:"quota"`
Usage int `json:"usage"`
WARPPlus bool `json:"warp_plus"`
ReferralCount int `json:"referral_count"`
ReferralRenewalCountdown int `json:"referral_renewal_countdown"`
Role string `json:"role"`
License string `json:"license"`
TTL time.Time `json:"ttl"`
} `json:"account"`
Config struct {
ClientID string `json:"client_id"`
Interface struct {
Addresses struct {
V4 string `json:"v4"`
V6 string `json:"v6"`
} `json:"addresses"`
} `json:"interface"`
Peers []struct {
PublicKey string `json:"public_key"`
Endpoint struct {
V4 string `json:"v4"`
V6 string `json:"v6"`
Host string `json:"host"`
Ports []int `json:"ports"`
} `json:"endpoint"`
} `json:"peers"`
Services struct {
HTTPProxy string `json:"http_proxy"`
} `json:"services"`
Metrics struct {
Ping int `json:"ping"`
Report int `json:"report"`
} `json:"metrics"`
} `json:"config"`
Token string `json:"token"`
WARPEnabled bool `json:"warp_enabled"`
WaitlistEnabled bool `json:"waitlist_enabled"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Tos time.Time `json:"tos"`
Place int `json:"place"`
Locale string `json:"locale"`
Enabled bool `json:"enabled"`
InstallID string `json:"install_id"`
FcmToken string `json:"fcm_token"`
Policy struct {
TunnelProtocol string `json:"tunnel_protocol"`
} `json:"policy"`
}
+19
View File
@@ -0,0 +1,19 @@
package cloudflare
import (
"crypto/rand"
"encoding/hex"
"time"
)
func GenerateRandomAndroidSerial() (string, error) {
serial := make([]byte, 8)
if _, err := rand.Read(serial); err != nil {
return "", err
}
return hex.EncodeToString(serial), nil
}
func TimeAsCfString(t time.Time) string {
return t.Format("2006-01-02T15:04:05.000-07:00")
}
+10
View File
@@ -11,3 +11,13 @@ func ContextWithIsExternalConnection(ctx context.Context) context.Context {
func IsExternalConnectionFromContext(ctx context.Context) bool {
return ctx.Value(contextKeyIsExternalConnection{}) != nil
}
type contextKeyIsProviderConnection struct{}
func ContextWithIsProviderConnection(ctx context.Context) context.Context {
return context.WithValue(ctx, contextKeyIsProviderConnection{}, true)
}
func IsProviderConnectionFromContext(ctx context.Context) bool {
return ctx.Value(contextKeyIsProviderConnection{}) != nil
}
+7 -6
View File
@@ -17,30 +17,31 @@ type Group struct {
type groupConnItem struct {
conn io.Closer
isExternal bool
isProvider bool
}
func NewGroup() *Group {
return &Group{}
}
func (g *Group) NewConn(conn net.Conn, isExternal bool) net.Conn {
func (g *Group) NewConn(conn net.Conn, isExternal bool, isProvider bool) net.Conn {
g.access.Lock()
defer g.access.Unlock()
item := g.connections.PushBack(&groupConnItem{conn, isExternal})
item := g.connections.PushBack(&groupConnItem{conn, isExternal, isProvider})
return &Conn{Conn: conn, group: g, element: item}
}
func (g *Group) NewPacketConn(conn net.PacketConn, isExternal bool) net.PacketConn {
func (g *Group) NewPacketConn(conn net.PacketConn, isExternal bool, isProvider bool) net.PacketConn {
g.access.Lock()
defer g.access.Unlock()
item := g.connections.PushBack(&groupConnItem{conn, isExternal})
item := g.connections.PushBack(&groupConnItem{conn, isExternal, isProvider})
return &PacketConn{PacketConn: conn, group: g, element: item}
}
func (g *Group) NewSingPacketConn(conn N.PacketConn, isExternal bool) N.PacketConn {
func (g *Group) NewSingPacketConn(conn N.PacketConn, isExternal bool, isProvider bool) N.PacketConn {
g.access.Lock()
defer g.access.Unlock()
item := g.connections.PushBack(&groupConnItem{conn, isExternal})
item := g.connections.PushBack(&groupConnItem{conn, isExternal, isProvider})
return &SingPacketConn{PacketConn: conn, group: g, element: item}
}
+54
View File
@@ -0,0 +1,54 @@
package kmutex
import "sync"
type Kmutex[T comparable] struct {
l sync.Locker
s map[T]*klock
}
type klock struct {
cond *sync.Cond
ref uint64
}
func New[T comparable]() *Kmutex[T] {
l := sync.Mutex{}
return &Kmutex[T]{
l: &l,
s: make(map[T]*klock),
}
}
func (km *Kmutex[T]) Unlock(key T) {
km.l.Lock()
defer km.l.Unlock()
kl, ok := km.s[key]
if !ok || kl.ref == 0 {
panic("unlock of unlocked kmutex")
}
kl.ref--
if kl.ref == 0 {
delete(km.s, key)
return
}
kl.cond.Signal()
}
func (km *Kmutex[T]) Lock(key T) {
km.l.Lock()
defer km.l.Unlock()
for {
kl, ok := km.s[key]
if !ok {
km.s[key] = &klock{
cond: sync.NewCond(km.l),
ref: 1,
}
return
}
kl.ref++
kl.cond.Wait()
return
}
}
+96
View File
@@ -0,0 +1,96 @@
package kmutex
import (
"sync"
"testing"
"time"
)
// Number of unique resources to access
const number = 100
func makeIds(count int) []int {
ids := make([]int, count)
for i := 0; i < count; i++ {
ids[i] = i
}
return ids
}
func TestKmutex(t *testing.T) {
km := New[int]()
ids := makeIds(number)
resources := make([]int, number)
wg := sync.WaitGroup{}
lc := make(chan int)
uc := make(chan int)
// Start 10n goroutines accessing n resources 10 times each
for i := 0; i < 10*number; i++ {
wg.Add(1)
go func(k int) {
for j := 0; j < 10; j++ {
lc <- k
km.Lock(ids[k])
// read and write resource to check for race
resources[k] = resources[k] + 1
km.Unlock(ids[k])
uc <- k
}
wg.Done()
}(i % len(ids))
}
to := time.After(time.Second)
counts := make(map[int]int)
var lCount, ulCount int
loop:
for {
select {
case k := <-lc:
counts[k] = counts[k] + 1
lCount++
case k := <-uc:
counts[k] = counts[k] - 1
ulCount++
case <-to:
t.Fatal("timed out waiting for results")
break loop
}
expectCount := 100 * number
if lCount == expectCount && ulCount == expectCount {
// Have all results
break
}
}
for k, c := range counts {
if c != 0 {
t.Errorf("Key %d count != 0: %d\n", k, c)
}
}
wg.Wait()
}
func BenchmarkKmutex1000(b *testing.B) {
km := New[int]()
ids := makeIds(number)
resources := make([]int, number)
wg := sync.WaitGroup{}
// Start 1000 goroutines accessing 100 resources N times each
b.ResetTimer()
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(k int) {
for j := 0; j < b.N; j++ {
km.Lock(ids[k])
// read and write resource to check for race
resources[k] = resources[k] + 1
km.Unlock(ids[k])
}
wg.Done()
}(i % len(ids))
}
wg.Wait()
}
+98
View File
@@ -0,0 +1,98 @@
package source
import (
"io"
"io/fs"
"io/ioutil"
"strconv"
"strings"
"github.com/golang-migrate/migrate/v4/source"
E "github.com/sagernet/sing/common/exceptions"
)
type RawDriver struct {
migrations *source.Migrations
rawMigrations map[string]string
}
func NewRawDriver(rawMigrations map[string]string) *RawDriver {
return &RawDriver{rawMigrations: rawMigrations}
}
func (d *RawDriver) Init() error {
ms := source.NewMigrations()
for key := range d.rawMigrations {
m, err := source.DefaultParse(key)
if err != nil {
continue
}
if !ms.Append(m) {
return source.ErrDuplicateMigration{
Migration: *m,
}
}
}
d.migrations = ms
return nil
}
func (d *RawDriver) Open(url string) (source.Driver, error) {
return nil, E.New("open() cannot be called")
}
func (d *RawDriver) Close() error {
return nil
}
func (d *RawDriver) First() (version uint, err error) {
if version, ok := d.migrations.First(); ok {
return version, nil
}
return 0, &fs.PathError{
Op: "first",
Err: fs.ErrNotExist,
}
}
func (d *RawDriver) Prev(version uint) (prevVersion uint, err error) {
if version, ok := d.migrations.Prev(version); ok {
return version, nil
}
return 0, &fs.PathError{
Op: "prev for version " + strconv.FormatUint(uint64(version), 10),
Err: fs.ErrNotExist,
}
}
func (d *RawDriver) Next(version uint) (nextVersion uint, err error) {
if version, ok := d.migrations.Next(version); ok {
return version, nil
}
return 0, &fs.PathError{
Op: "next for version " + strconv.FormatUint(uint64(version), 10),
Err: fs.ErrNotExist,
}
}
func (d *RawDriver) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
if m, ok := d.migrations.Up(version); ok {
body := ioutil.NopCloser(strings.NewReader(d.rawMigrations[m.Raw]))
return body, m.Identifier, nil
}
return nil, "", &fs.PathError{
Op: "read up for version " + strconv.FormatUint(uint64(version), 10),
Err: fs.ErrNotExist,
}
}
func (d *RawDriver) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
if m, ok := d.migrations.Down(version); ok {
body := ioutil.NopCloser(strings.NewReader(d.rawMigrations[m.Raw]))
return body, m.Identifier, nil
}
return nil, "", &fs.PathError{
Op: "read down for version " + strconv.FormatUint(uint64(version), 10),
Err: fs.ErrNotExist,
}
}
+3
View File
@@ -38,6 +38,9 @@ func NewRouterWithOptions(router adapter.ConnectionRouterEx, logger logger.Conte
}
}
service, err := mux.NewService(mux.ServiceOptions{
NewConnectionContext: func(ctx context.Context, conn net.Conn) context.Context {
return log.ContextWithNewMuxID(ctx)
},
NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context {
return log.ContextWithNewID(ctx)
},
+2 -1
View File
@@ -261,7 +261,8 @@ func getExecPathFromPID(pid uint32) (string, error) {
procpidpathinfo,
0,
uintptr(unsafe.Pointer(&buf[0])),
procpidpathinfosize)
procpidpathinfosize,
)
if errno != 0 {
return "", errno
}
+74
View File
@@ -0,0 +1,74 @@
package tls
import (
"context"
"crypto/ecdsa"
"crypto/tls"
"crypto/x509"
"time"
"github.com/sagernet/quic-go/http3"
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/logger"
)
func NewMASQUEClient(ctx context.Context, logger logger.ContextLogger, serverName string, cert [][]byte, privateKey *ecdsa.PrivateKey, peerPublicKey *ecdsa.PublicKey, options option.MASQUEOutboundTLSOptions) (Config, error) {
var tlsConfig tls.Config
tlsConfig.ServerName = serverName
tlsConfig.InsecureSkipVerify = true
tlsConfig.NextProtos = []string{http3.NextProtoH3}
tlsConfig.Certificates = []tls.Certificate{
{
Certificate: cert,
PrivateKey: privateKey,
},
}
if options.CipherSuites != nil {
find:
for _, cipherSuite := range options.CipherSuites {
for _, tlsCipherSuite := range tls.CipherSuites() {
if cipherSuite == tlsCipherSuite.Name {
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID)
continue find
}
}
return nil, E.New("unknown cipher_suite: ", cipherSuite)
}
}
for _, curve := range options.CurvePreferences {
tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, tls.CurveID(curve))
}
if !options.Insecure {
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return nil
}
cert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return err
}
if _, ok := cert.PublicKey.(*ecdsa.PublicKey); !ok {
return x509.ErrUnsupportedAlgorithm
}
if !cert.PublicKey.(*ecdsa.PublicKey).Equal(peerPublicKey) {
return x509.CertificateInvalidError{Cert: cert, Reason: 10, Detail: "remote endpoint has a different public key than what we trust in config.json"}
}
return nil
}
}
var config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
if options.KernelRx || options.KernelTx {
if !C.IsLinux {
return nil, E.New("kTLS is only supported on Linux")
}
config = &KTLSClientConfig{
Config: config,
logger: logger,
kernelTx: options.KernelTx,
kernelRx: options.KernelRx,
}
}
return config, nil
}
+79
View File
@@ -0,0 +1,79 @@
package common
import (
"encoding/base64"
"encoding/json"
"reflect"
"regexp"
"strconv"
"strings"
"time"
Xbadoption "github.com/sagernet/sing-box/common/xray/json/badoption"
"github.com/sagernet/sing/common/json/badoption"
)
func StringToType[T any](str string) T {
var value T
v := reflect.ValueOf(&value).Elem()
switch any(value).(type) {
case badoption.Duration:
d, err := time.ParseDuration(str)
if err != nil {
v.SetInt(StringToType[int64](str))
} else {
v.Set(reflect.ValueOf(d))
}
return value
case badoption.HTTPHeader:
headers := badoption.HTTPHeader{}
reg := regexp.MustCompile(`^[ \t]*?(\S+?):[ \t]*?(\S+?)[ \t]*?$`)
for _, header := range strings.Split(str, "\n") {
result := reg.FindStringSubmatch(header)
if result != nil {
key := result[1]
headers[key] = strings.Split(result[2], ",")
}
}
v.Set(reflect.ValueOf(headers))
return value
}
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
i, _ := strconv.ParseInt(str, 10, 64)
v.SetInt(i)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
i, _ := strconv.ParseUint(str, 10, 64)
v.SetUint(i)
case reflect.Float32, reflect.Float64:
f, _ := strconv.ParseFloat(str, 64)
v.SetFloat(f)
case reflect.Bool:
b, _ := strconv.ParseBool(str)
v.SetBool(b)
default:
panic("unsupported type")
}
return value
}
func DecodeBase64URLSafe(content string) (string, error) {
s := strings.ReplaceAll(content, " ", "-")
s = strings.ReplaceAll(s, "/", "_")
s = strings.ReplaceAll(s, "+", "-")
s = strings.ReplaceAll(s, "=", "")
result, err := base64.RawURLEncoding.DecodeString(s)
if err != nil {
return content, nil
}
return string(result), nil
}
func ParseXHTTPRange(value string) (Xbadoption.Range, error) {
result := Xbadoption.Range{}
encoded, err := json.Marshal(value)
if err != nil {
return result, err
}
return result, result.UnmarshalJSON(encoded)
}
+25
View File
@@ -0,0 +1,25 @@
package vision
import (
"context"
"net"
)
type Hook func(net.Conn)
type hookKey struct{}
func WithHook(ctx context.Context, hook Hook) context.Context {
if hook == nil {
return ctx
}
return context.WithValue(ctx, hookKey{}, hook)
}
func HookFromContext(ctx context.Context) (Hook, bool) {
if ctx == nil {
return nil, false
}
hook, ok := ctx.Value(hookKey{}).(Hook)
return hook, ok
}
+2 -2
View File
@@ -38,8 +38,8 @@ func MergeMulti(dest MultiBuffer, src MultiBuffer) (MultiBuffer, MultiBuffer) {
// MergeBytes merges the given bytes into MultiBuffer and return the new address of the merged MultiBuffer.
func MergeBytes(dest MultiBuffer, src []byte) MultiBuffer {
n := len(dest)
if n > 0 && !(dest)[n-1].IsFull() {
nBytes, _ := (dest)[n-1].Write(src)
if n > 0 && !dest[n-1].IsFull() {
nBytes, _ := dest[n-1].Write(src)
src = src[nBytes:]
}
+18
View File
@@ -0,0 +1,18 @@
package cpuid
import (
"runtime"
"golang.org/x/sys/cpu"
)
var (
// Keep in sync with crypto/tls/cipher_suites.go.
hasGCMAsmAMD64 = cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ
hasGCMAsmARM64 = cpu.ARM64.HasAES && cpu.ARM64.HasPMULL
hasGCMAsmS390X = cpu.S390X.HasAES && cpu.S390X.HasAESCBC && cpu.S390X.HasGHASH
hasGCMAsmPPC64 = runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le"
// HasAESGCM indicates whether the CPU has AES-GCM hardware acceleration.
HasAESGCM = hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X || hasGCMAsmPPC64
)
+7
View File
@@ -71,6 +71,13 @@ func (c *Range) UnmarshalJSON(content []byte) error {
return nil
}
func (c *Range) String() string {
if c.From == c.To {
return strconv.FormatInt(int64(c.From), 10)
}
return fmt.Sprintf("%d-%d", c.From, c.To)
}
func (c Range) Rand() int32 {
return int32(crypto.RandBetween(int64(c.From), int64(c.To)))
}
+1 -1
View File
@@ -42,7 +42,7 @@ func New(opts ...Option) (*Reader, *Writer) {
}
for _, opt := range opts {
opt(&(p.option))
opt(&p.option)
}
return &Reader{
+244 -16
View File
@@ -1,28 +1,256 @@
package utils
import (
"hash/fnv"
"math"
"math/rand"
"net/http"
"strconv"
"strings"
"time"
"github.com/klauspost/cpuid/v2"
)
func ChromeVersion() int {
// Use only CPU info as seed for PRNG
seed := int64(cpuid.CPU.Family + cpuid.CPU.Model + cpuid.CPU.PhysicalCores + cpuid.CPU.LogicalCores + cpuid.CPU.CacheLine)
rng := rand.New(rand.NewSource(seed))
// Start from Chrome 144 released on 2026.1.13
releaseDate := time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC)
version := 144
now := time.Now()
// Each version has random 25-45 day interval
for releaseDate.Before(now) {
releaseDate = releaseDate.AddDate(0, 0, rng.Intn(21)+25)
version++
}
return version - 1
func GetRandomizer() *rand.Rand {
// Seed the PRNG with the hash of CPU info, increasing the overall probable space.
fnvHash := fnv.New64()
fnvHash.Write([]byte(strconv.Itoa(cpuid.CPU.Family) + strconv.Itoa(cpuid.CPU.Model) + strconv.Itoa(cpuid.CPU.PhysicalCores) + strconv.Itoa(cpuid.CPU.LogicalCores) + strconv.Itoa(cpuid.CPU.CacheLine) + strconv.Itoa(cpuid.CPU.ThreadsPerCore)))
return rand.New(rand.NewSource(int64(fnvHash.Sum64())))
}
// ChromeUA provides default browser User-Agent based on CPU-seeded PRNG.
var ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + strconv.Itoa(ChromeVersion()) + ".0.0.0 Safari/537.36"
var globalRng *rand.Rand = GetRandomizer()
// The Chrome version generator will suffer from deviation of a normal distribution.
func ChromeVersion() int {
// Start from Chrome 144, released on 2026.1.13.
var startVersion int = 144
var timeStart int64 = time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC).Unix() / 86400
var timeCurrent int64 = time.Now().Unix() / 86400
var timeDiff int = int((timeCurrent - timeStart - 35)) - int(math.Floor(math.Pow(globalRng.Float64(), 2)*105))
return startVersion + (timeDiff / 35) // It's 31.15 currently.
}
var safariMinorMap [25]int = [25]int{0, 0, 0, 1, 1,
1, 2, 2, 2, 2, 3, 3, 3, 4, 4,
4, 5, 5, 5, 5, 5, 6, 6, 6, 6}
// The following version generators use deterministic generators, but with the distribution scaled by a curve.
func CurlVersion() string {
// curl 8.0.0 was released on 20/03/2023.
var timeCurrent int64 = time.Now().Unix() / 86400
var timeStart int64 = time.Date(2023, 3, 20, 0, 0, 0, 0, time.UTC).Unix() / 86400
var timeDiff int = int((timeCurrent - timeStart - 60)) - int(math.Floor(math.Pow(globalRng.Float64(), 2)*165))
var minorValue int = int(timeDiff / 57) // The release cadence is actually 56.67 days.
return "8." + strconv.Itoa(minorValue) + ".0"
}
func FirefoxVersion() int {
// Firefox 128 ESR was released on 09/07/2023.
var timeCurrent int64 = time.Now().Unix() / 86400
var timeStart int64 = time.Date(2024, 7, 29, 0, 0, 0, 0, time.UTC).Unix() / 86400
var timeDiff = timeCurrent - timeStart - 25 - int64(math.Floor(math.Pow(globalRng.Float64(), 2)*50))
return int(timeDiff/30) + 128
}
func SafariVersion() string {
var anchoredTime time.Time = time.Now()
var releaseYear int = anchoredTime.Year()
var splitPoint time.Time = time.Date(releaseYear, 9, 23, 0, 0, 0, 0, time.UTC)
var delayedDays = int(math.Floor(math.Pow(globalRng.Float64(), 3) * 75))
splitPoint = splitPoint.AddDate(0, 0, delayedDays)
if anchoredTime.Compare(splitPoint) < 0 {
releaseYear--
splitPoint = time.Date(releaseYear, 9, 23, 0, 0, 0, 0, time.UTC)
splitPoint = splitPoint.AddDate(0, 0, delayedDays)
}
var minorVersion = safariMinorMap[(anchoredTime.Unix()-splitPoint.Unix())/1296000]
return strconv.Itoa(releaseYear-1999) + "." + strconv.Itoa(minorVersion)
}
// The full Chromium brand GREASE implementation
var clientHintGreaseNA = []string{" ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_"}
var clientHintVersionNA = []string{"8", "99", "24"}
var clientHintShuffle3 = [][3]int{{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}}
var clientHintShuffle4 = [][4]int{
{0, 1, 2, 3}, {0, 1, 3, 2}, {0, 2, 1, 3}, {0, 2, 3, 1}, {0, 3, 1, 2}, {0, 3, 2, 1},
{1, 0, 2, 3}, {1, 0, 3, 2}, {1, 2, 0, 3}, {1, 2, 3, 0}, {1, 3, 0, 2}, {1, 3, 2, 0},
{2, 0, 1, 3}, {2, 0, 3, 1}, {2, 1, 0, 3}, {2, 1, 3, 0}, {2, 3, 0, 1}, {2, 3, 1, 0},
{3, 0, 1, 2}, {3, 0, 2, 1}, {3, 1, 0, 2}, {3, 1, 2, 0}, {3, 2, 0, 1}, {3, 2, 1, 0}}
func getGreasedChInvalidBrand(seed int) string {
return "\"Not" + clientHintGreaseNA[seed%len(clientHintGreaseNA)] + "A" + clientHintGreaseNA[(seed+1)%len(clientHintGreaseNA)] + "Brand\";v=\"" + clientHintVersionNA[seed%len(clientHintVersionNA)] + "\""
}
func getGreasedChOrder(brandLength int, seed int) []int {
switch brandLength {
case 1:
return []int{0}
case 2:
return []int{seed % brandLength, (seed + 1) % brandLength}
case 3:
return clientHintShuffle3[seed%len(clientHintShuffle3)][:]
default:
return clientHintShuffle4[seed%len(clientHintShuffle4)][:]
}
//return []int{}
}
func getUngreasedChUa(majorVersion int, forkName string) []string {
// Set the capacity to 4, the maximum allowed brand size, so Go will never allocate memory twice
baseChUa := make([]string, 0, 4)
baseChUa = append(baseChUa, getGreasedChInvalidBrand(majorVersion),
"\"Chromium\";v=\""+strconv.Itoa(majorVersion)+"\"")
switch forkName {
case "chrome":
baseChUa = append(baseChUa, "\"Google Chrome\";v=\""+strconv.Itoa(majorVersion)+"\"")
case "edge":
baseChUa = append(baseChUa, "\"Microsoft Edge\";v=\""+strconv.Itoa(majorVersion)+"\"")
}
return baseChUa
}
func getGreasedChUa(majorVersion int, forkName string) string {
ungreasedCh := getUngreasedChUa(majorVersion, forkName)
shuffleMap := getGreasedChOrder(len(ungreasedCh), majorVersion)
shuffledCh := make([]string, len(ungreasedCh))
for i, e := range shuffleMap {
shuffledCh[e] = ungreasedCh[i]
}
return strings.Join(shuffledCh, ", ")
}
// The code below provides a coherent default browser user agent string based on a CPU-seeded PRNG.
var CurlUA = "curl/" + CurlVersion()
var AnchoredFirefoxVersion = strconv.Itoa(FirefoxVersion())
var FirefoxUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:" + AnchoredFirefoxVersion + ".0) Gecko/20100101 Firefox/" + AnchoredFirefoxVersion + ".0"
var SafariUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/" + SafariVersion() + " Safari/605.1.15"
// Chromium browsers.
var AnchoredChromeVersion = ChromeVersion()
var ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0 Safari/537.36"
var ChromeUACH = getGreasedChUa(AnchoredChromeVersion, "chrome")
var MSEdgeUA = ChromeUA + "Edg/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0"
var MSEdgeUACH = getGreasedChUa(AnchoredChromeVersion, "edge")
func applyMasqueradedHeaders(header http.Header, browser string, variant string) {
// Browser-specific.
switch browser {
case "chrome":
header["Sec-CH-UA"] = []string{ChromeUACH}
header["Sec-CH-UA-Mobile"] = []string{"?0"}
header["Sec-CH-UA-Platform"] = []string{"\"Windows\""}
header["DNT"] = []string{"1"}
header.Set("User-Agent", ChromeUA)
header.Set("Accept-Language", "en-US,en;q=0.9")
case "edge":
header["Sec-CH-UA"] = []string{MSEdgeUACH}
header["Sec-CH-UA-Mobile"] = []string{"?0"}
header["Sec-CH-UA-Platform"] = []string{"\"Windows\""}
header["DNT"] = []string{"1"}
header.Set("User-Agent", MSEdgeUA)
header.Set("Accept-Language", "en-US,en;q=0.9")
case "firefox":
header.Set("User-Agent", FirefoxUA)
header["DNT"] = []string{"1"}
header.Set("Accept-Language", "en-US,en;q=0.5")
case "safari":
header.Set("User-Agent", SafariUA)
header.Set("Accept-Language", "en-US,en;q=0.9")
case "golang":
// Expose the default net/http header.
header.Del("User-Agent")
return
case "curl":
header.Set("User-Agent", CurlUA)
return
}
// Context-specific.
switch variant {
case "nav":
if header.Get("Cache-Control") == "" {
switch browser {
case "chrome", "edge":
header.Set("Cache-Control", "max-age=0")
}
}
header.Set("Upgrade-Insecure-Requests", "1")
if header.Get("Accept") == "" {
switch browser {
case "chrome", "edge":
header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
case "firefox", "safari":
header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
}
}
header.Set("Sec-Fetch-Site", "none")
header.Set("Sec-Fetch-Mode", "navigate")
switch browser {
case "safari":
default:
header.Set("Sec-Fetch-User", "?1")
}
header.Set("Sec-Fetch-Dest", "document")
header.Set("Priority", "u=0, i")
case "ws":
header.Set("Sec-Fetch-Mode", "websocket")
switch browser {
case "safari":
// Safari is NOT web-compliant here!
header.Set("Sec-Fetch-Dest", "websocket")
default:
header.Set("Sec-Fetch-Dest", "empty")
}
header.Set("Sec-Fetch-Site", "same-origin")
if header.Get("Cache-Control") == "" {
header.Set("Cache-Control", "no-cache")
}
if header.Get("Pragma") == "" {
header.Set("Pragma", "no-cache")
}
if header.Get("Accept") == "" {
header.Set("Accept", "*/*")
}
case "fetch":
header.Set("Sec-Fetch-Mode", "cors")
header.Set("Sec-Fetch-Dest", "empty")
header.Set("Sec-Fetch-Site", "same-origin")
if header.Get("Priority") == "" {
switch browser {
case "chrome", "edge":
header.Set("Priority", "u=1, i")
case "firefox":
header.Set("Priority", "u=4")
case "safari":
header.Set("Priority", "u=3, i")
}
}
if header.Get("Cache-Control") == "" {
header.Set("Cache-Control", "no-cache")
}
if header.Get("Pragma") == "" {
header.Set("Pragma", "no-cache")
}
if header.Get("Accept") == "" {
header.Set("Accept", "*/*")
}
}
}
func TryDefaultHeadersWith(header http.Header, variant string) {
// The global UA special value handler for transports. Used to be called HandleTransportUASettings.
// Just a FYI to whoever needing to fix this piece of code after some spontaneous event, I tried to make the two methods separate to let the code be cleaner and more organized.
if len(header.Values("User-Agent")) < 1 {
applyMasqueradedHeaders(header, "chrome", variant)
} else {
switch header.Get("User-Agent") {
case "chrome":
applyMasqueradedHeaders(header, "chrome", variant)
case "firefox":
applyMasqueradedHeaders(header, "firefox", variant)
case "safari":
applyMasqueradedHeaders(header, "safari", variant)
case "edge":
applyMasqueradedHeaders(header, "edge", variant)
case "curl":
applyMasqueradedHeaders(header, "curl", variant)
case "golang":
applyMasqueradedHeaders(header, "golang", variant)
}
}
}
+1
View File
@@ -29,6 +29,7 @@ const (
DNSTypeDHCP = "dhcp"
DNSTypeTailscale = "tailscale"
DNSTypeSDNS = "sdns"
DNSTypeFallback = "fallback"
)
const (
+9
View File
@@ -0,0 +1,9 @@
package constant
const (
ManagerAPIServer = "server"
ManagerAPIClient = "client"
ManagerAPIProtocolHTTP = "http"
ManagerAPIProtocolGrpc = "grpc"
)
+6
View File
@@ -0,0 +1,6 @@
package constant
const (
NodeManagerAPIServer = "server"
NodeManagerAPIClient = "client"
)
+20
View File
@@ -0,0 +1,20 @@
package constant
const (
ProviderTypeInline = "inline"
ProviderTypeLocal = "local"
ProviderTypeRemote = "remote"
)
func ProviderDisplayName(providerType string) string {
switch providerType {
case ProviderTypeInline:
return "Inline"
case ProviderTypeLocal:
return "Local"
case ProviderTypeRemote:
return "Remote"
default:
return "Unknown"
}
}
+44 -6
View File
@@ -16,6 +16,9 @@ const (
TypeNaive = "naive"
TypeWireGuard = "wireguard"
TypeWARP = "warp"
TypeMASQUE = "masque"
TypeMTProxy = "mtproxy"
TypeParser = "parser"
TypeHysteria = "hysteria"
TypeTor = "tor"
TypeSSH = "ssh"
@@ -26,18 +29,31 @@ const (
TypeVLESS = "vless"
TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2"
TypeTunnelClient = "tunnel_client"
TypeTunnelServer = "tunnel_server"
TypeBond = "bond"
TypeFailover = "failover"
TypeVPNServer = "vpn-server"
TypeVPNClient = "vpn-client"
TypeTailscale = "tailscale"
TypeConnectionLimiter = "connection-limiter"
TypeBandwidthLimiter = "bandwidth-limiter"
TypeTrafficLimiter = "traffic-limiter"
TypeRateLimiter = "rate-limiter"
TypeAdminPanel = "admin-panel"
TypeManagerAPI = "manager-api"
TypeNodeManagerAPI = "node-manager-api"
TypeDERP = "derp"
TypeManager = "manager"
TypeNode = "node"
TypeResolved = "resolved"
TypeSSMAPI = "ssm-api"
TypeCCM = "ccm"
TypeOCM = "ocm"
TypeOOMKiller = "oom-killer"
TypeProfiler = "profiler"
)
const (
TypeFallback = "fallback"
TypeSelector = "selector"
TypeURLTest = "urltest"
)
@@ -74,6 +90,12 @@ func ProxyDisplayName(proxyType string) string {
return "WireGuard"
case TypeWARP:
return "WARP"
case TypeMASQUE:
return "MASQUE"
case TypeMTProxy:
return "MTProxy"
case TypeParser:
return "Parser"
case TypeHysteria:
return "Hysteria"
case TypeTor:
@@ -90,20 +112,36 @@ func ProxyDisplayName(proxyType string) string {
return "TUIC"
case TypeHysteria2:
return "Hysteria2"
case TypeBond:
return "Bond"
case TypeFailover:
return "Failover"
case TypeMieru:
return "Mieru"
case TypeAnyTLS:
return "AnyTLS"
case TypeFallback:
return "Fallback"
case TypeTailscale:
return "Tailscale"
case TypeSelector:
return "Selector"
case TypeURLTest:
return "URLTest"
case TypeTunnelClient:
return "Tunnel Client"
case TypeTunnelServer:
return "Tunnel Server"
case TypeConnectionLimiter:
return "Connection Limiter"
case TypeBandwidthLimiter:
return "Bandwidth Limiter"
case TypeTrafficLimiter:
return "Traffic Limiter"
case TypeRateLimiter:
return "Rate Limiter"
case TypeVPNClient:
return "VPN Client"
case TypeVPNServer:
return "VPN Server"
case TypeProfiler:
return "Profiler"
default:
return "Unknown"
}
+1
View File
@@ -7,4 +7,5 @@ const (
V2RayTransportTypeGRPC = "grpc"
V2RayTransportTypeHTTPUpgrade = "httpupgrade"
V2RayTransportTypeXHTTP = "xhttp"
V2RayTransportTypeKCP = "mkcp"
)
-20
View File
@@ -1,20 +0,0 @@
package constant
type WARPConfig struct {
PrivateKey string `json:"private_key"`
Interface struct {
Addresses struct {
V4 string `json:"v4"`
V6 string `json:"v6"`
} `json:"addresses"`
} `json:"interface"`
Peers []struct {
PublicKey string `json:"public_key"`
Endpoint struct {
V4 string `json:"v4"`
V6 string `json:"v6"`
Host string `json:"host"`
Ports []int `json:"ports"`
} `json:"endpoint"`
} `json:"peers"`
}
+7 -11
View File
@@ -1,13 +1,12 @@
package daemon
import (
reflect "reflect"
sync "sync"
unsafe "unsafe"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
@@ -1948,10 +1947,9 @@ func file_daemon_started_service_proto_rawDescGZIP() []byte {
return file_daemon_started_service_proto_rawDescData
}
var (
file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 26)
file_daemon_started_service_proto_goTypes = []any{
var file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
var file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 26)
var file_daemon_started_service_proto_goTypes = []any{
(LogLevel)(0), // 0: daemon.LogLevel
(ConnectionEventType)(0), // 1: daemon.ConnectionEventType
(ServiceStatus_Type)(0), // 2: daemon.ServiceStatus.Type
@@ -1982,9 +1980,7 @@ var (
(*StartedAt)(nil), // 27: daemon.StartedAt
(*Log_Message)(nil), // 28: daemon.Log.Message
(*emptypb.Empty)(nil), // 29: google.protobuf.Empty
}
)
}
var file_daemon_started_service_proto_depIdxs = []int32{
2, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type
28, // 1: daemon.Log.messages:type_name -> daemon.Log.Message
-21
View File
@@ -2,7 +2,6 @@ package daemon
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
@@ -375,83 +374,63 @@ type UnimplementedStartedServiceServer struct{}
func (UnimplementedStartedServiceServer) StopService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method StopService not implemented")
}
func (UnimplementedStartedServiceServer) ReloadService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method ReloadService not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error {
return status.Error(codes.Unimplemented, "method SubscribeServiceStatus not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error {
return status.Error(codes.Unimplemented, "method SubscribeLog not implemented")
}
func (UnimplementedStartedServiceServer) GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error) {
return nil, status.Error(codes.Unimplemented, "method GetDefaultLogLevel not implemented")
}
func (UnimplementedStartedServiceServer) ClearLogs(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method ClearLogs not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error {
return status.Error(codes.Unimplemented, "method SubscribeStatus not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error {
return status.Error(codes.Unimplemented, "method SubscribeGroups not implemented")
}
func (UnimplementedStartedServiceServer) GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error) {
return nil, status.Error(codes.Unimplemented, "method GetClashModeStatus not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeClashMode(*emptypb.Empty, grpc.ServerStreamingServer[ClashMode]) error {
return status.Error(codes.Unimplemented, "method SubscribeClashMode not implemented")
}
func (UnimplementedStartedServiceServer) SetClashMode(context.Context, *ClashMode) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SetClashMode not implemented")
}
func (UnimplementedStartedServiceServer) URLTest(context.Context, *URLTestRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method URLTest not implemented")
}
func (UnimplementedStartedServiceServer) SelectOutbound(context.Context, *SelectOutboundRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SelectOutbound not implemented")
}
func (UnimplementedStartedServiceServer) SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SetGroupExpand not implemented")
}
func (UnimplementedStartedServiceServer) GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error) {
return nil, status.Error(codes.Unimplemented, "method GetSystemProxyStatus not implemented")
}
func (UnimplementedStartedServiceServer) SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SetSystemProxyEnabled not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error {
return status.Error(codes.Unimplemented, "method SubscribeConnections not implemented")
}
func (UnimplementedStartedServiceServer) CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method CloseConnection not implemented")
}
func (UnimplementedStartedServiceServer) CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method CloseAllConnections not implemented")
}
func (UnimplementedStartedServiceServer) GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error) {
return nil, status.Error(codes.Unimplemented, "method GetDeprecatedWarnings not implemented")
}
func (UnimplementedStartedServiceServer) GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) {
return nil, status.Error(codes.Unimplemented, "method GetStartedAt not implemented")
}
+72
View File
@@ -0,0 +1,72 @@
package fallback
import (
"context"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
mDNS "github.com/miekg/dns"
)
func RegisterTransport(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.FallbackDNSServerOptions](registry, C.DNSTypeFallback, NewTransport)
}
var _ adapter.DNSTransport = (*Transport)(nil)
type Transport struct {
dns.TransportAdapter
ctx context.Context
manager adapter.DNSTransportManager
logger logger.ContextLogger
tags []string
strategy ExchangeStrategy
}
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.FallbackDNSServerOptions) (adapter.DNSTransport, error) {
if len(options.Servers) == 0 {
return nil, E.New("missing servers")
}
manager := service.FromContext[adapter.DNSTransportManager](ctx)
servers := make([]adapter.DNSTransport, len(options.Servers))
for i, tag := range options.Servers {
server, loaded := manager.Transport(tag)
if !loaded {
return nil, E.New("server ", tag, " not found")
}
servers[i] = server
}
strategy, err := CreateStrategy(options.Strategy, servers, logger)
if err != nil {
return nil, err
}
return &Transport{
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeFallback, tag, options.Servers),
ctx: ctx,
logger: logger,
tags: options.Servers,
strategy: strategy,
}, nil
}
func (t *Transport) Start(stage adapter.StartStage) error {
return nil
}
func (t *Transport) Close() error {
return nil
}
func (t *Transport) Reset() {
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
return t.strategy(ctx, message)
}
+73
View File
@@ -0,0 +1,73 @@
package fallback
import (
"context"
mDNS "github.com/miekg/dns"
"github.com/sagernet/sing-box/adapter"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
type ExchangeStrategy = func(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error)
func parallelStrategy(servers []adapter.DNSTransport, logger logger.ContextLogger) ExchangeStrategy {
return func(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
queryCtx, cancel := context.WithCancel(ctx)
defer cancel()
type result struct {
response *mDNS.Msg
err error
}
results := make(chan result)
for _, server := range servers {
go func() {
response, err := server.Exchange(queryCtx, message)
select {
case results <- result{response, err}:
case <-queryCtx.Done():
}
}()
}
var lastErr error
for range servers {
select {
case result := <-results:
if result.err != nil {
lastErr = result.err
continue
}
return result.response, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
return nil, lastErr
}
}
func sequentialStrategy(servers []adapter.DNSTransport, logger logger.ContextLogger) ExchangeStrategy {
return func(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
var lastErr error
for _, server := range servers {
response, err := server.Exchange(ctx, message)
if err != nil {
lastErr = err
continue
}
return response, nil
}
return nil, lastErr
}
}
func CreateStrategy(strategy string, servers []adapter.DNSTransport, logger logger.ContextLogger) (ExchangeStrategy, error) {
switch strategy {
case "parallel":
return parallelStrategy(servers, logger), nil
case "", "sequential":
return sequentialStrategy(servers, logger), nil
default:
return nil, E.New("strategy not found: ", strategy)
}
}
-3
View File
@@ -2,8 +2,6 @@
icon: material/alert-decagram
---
<<<<<<< HEAD
=======
#### 1.13.11
* Fix process searcher failure introduced in 1.13.9
@@ -63,7 +61,6 @@ from [SagerNet/go](https://github.com/SagerNet/go).
See [OCM](/configuration/service/ocm).
>>>>>>> v1.13.11
#### 1.13.2
* Fixes and improvements
@@ -0,0 +1,47 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "mixed",
"tag": "mixed-in",
"listen_port": 7897
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "vless",
"tag": "vless-out",
"server": "0.0.0.0",
"server_port": 443,
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
"transport": {
"type": "http"
}
}
],
"route": {
"rules": [
{
"protocol": "dns",
"action": "hijack-dns"
}
],
"final": "vless-out",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}
@@ -0,0 +1,90 @@
{
"log": {
"level": "info"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [],
"outbounds": [
{
"type": "direct",
"tag": "direct-out"
}
],
"route": {
"rules": [
{
"protocol": "dns",
"action": "hijack-dns"
}
],
"final": "direct-out"
},
"services": [
{
"type": "manager",
"tag": "my-manager",
"database": {
"driver": "sqlite",
"dsn": "file:manager.db?_pragma=foreign_keys(on)&_pragma=journal_mode(wal)&_pragma=busy_timeout(5000)" // also supported Postgresql
}
},
{
"type": "manager-api",
"tag": "my-manager-api",
"api_type": "server",
"protocol_type": "http",
"listen": "0.0.0.0",
"listen_port": 8080,
"manager": "my-manager",
"api_key": "change-me-secret",
"cors": {
"allowed_origins": ["*"],
"max_age": 600
},
// Enable TLS for production deployments:
// "tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#inbound
// "enabled": true,
// "server_name": "manager.example.com",
// "certificate_path": "fullchain.pem",
// "key_path": "privkey.pem"
// }
},
{
"type": "node-manager-api",
"tag": "my-node-manager-api",
"api_type": "server",
"listen": "0.0.0.0",
"listen_port": 7000,
"manager": "my-manager",
"api_key": "change-me-secret",
// Enable TLS for production deployments (the node connects via gRPC over h2):
// "tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#inbound
// "enabled": true,
// "server_name": "example.com",
// "alpn": "h2", // h3 for QUIC
// "certificate_path": "fullchain.pem",
// "key_path": "privkey.pem"
// }
},
{
"type": "admin-panel",
"tag": "admin",
"listen": "0.0.0.0",
"listen_port": 8081,
// Enable TLS for production deployments:
// "tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#inbound
// "enabled": true,
// "server_name": "panel.example.com",
// "certificate_path": "fullchain.pem",
// "key_path": "privkey.pem"
// }
}
]
}
@@ -0,0 +1,97 @@
{
"log": {
"level": "debug"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "vless",
"tag": "vless-in",
"listen": "0.0.0.0",
"listen_port": 443,
"transport": {
"type": "http"
}
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct-out"
},
{
"type": "bandwidth-limiter",
"tag": "bandwidth-limiter",
"strategy": "manager",
"route": {
"final": "direct-out"
}
},
{
"type": "rate-limiter",
"tag": "rate-limiter",
"strategy": "manager",
"route": {
"final": "bandwidth-limiter"
}
},
{
"type": "connection-limiter",
"tag": "connection-limiter",
"strategy": "manager",
"route": {
"final": "rate-limiter"
}
},
{
"type": "traffic-limiter",
"tag": "traffic-limiter",
"strategy": "manager",
"route": {
"final": "connection-limiter"
}
},
],
"route": {
"rules": [
{
"protocol": "dns",
"action": "hijack-dns"
}
],
"final": "connection-limiter"
},
"services": [
{
"type": "node",
"tag": "my-node",
"uuid": "e6eceb84-ad66-474b-8641-142499db7c6e",
"manager": "node-manager",
"inbounds": ["vless-in"],
"connection_limiters": ["connection-limiter"],
"bandwidth_limiters": ["bandwidth-limiter"],
"traffic_limiters": ["traffic-limiter"],
"rate_limiters": ["rate-limiter"]
},
{
"type": "node-manager-api",
"tag": "node-manager",
"api_type": "client",
"server": "example.com",
"server_port": 7000,
"api_key": "change-me-secret",
// "tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#outbound
// "enabled": true,
// "server_name": "example.com",
// "alpn": "h2" // h3 for QUIC
// }
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

+4 -2
View File
@@ -15,13 +15,15 @@
"type": "wireguard",
"tag": "wireguard-out",
"mtu": 1408,
"address": null,
"private_key": "",
"address": ["10.0.0.2/32"],
"private_key": "QGg8AFRn6qKfTB7cT3FWH1WGx3np+OKzlNuQUrqIBmI=",
"listen_port": 10000,
"peers": [
{
"address": "example.com",
"port": 10001,
"public_key": "3nk7jdnkcL95Fc/z+GCiH7jOovEKhFkLIGPT+U/uLEQ=",
"allowed_ips": ["0.0.0.0/0"],
"reserved": "AAAA"
}
],
+61
View File
@@ -0,0 +1,61 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "mixed",
"tag": "mixed-in",
"listen_port": 7897
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "bond",
"tag": "bond-out",
"outbounds": [ // sum of download_ratio and upload_ratio must be 100
{
"outbound": {
"type": "vless",
"server": "0.0.0.0",
"server_port": 443,
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
"network": "tcp",
"bind_interface": ""
},
"download_ratio": 50,
"upload_ratio": 50
},
{
"outbound": {
"type": "vless",
"server": "0.0.0.0",
"server_port": 444,
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
"network": "tcp",
"bind_interface": ""
},
"download_ratio": 50,
"upload_ratio": 50
}
]
}
],
"route": {
"final": "bond-out",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}
+49
View File
@@ -0,0 +1,49 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "mixed",
"tag": "mixed-in",
"listen_port": 7897
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "bond",
"tag": "bond-out",
"outbounds": [ // sum of download_ratio and upload_ratio must be 100
{
"outbound": {
"type": "vless",
"server": "0.0.0.0",
"server_port": 443,
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
"network": "tcp",
},
"download_ratio": 20,
"upload_ratio": 20,
"count": 5
}
]
}
],
"route": {
"final": "bond-out",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}
+61
View File
@@ -0,0 +1,61 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "mixed",
"tag": "mixed-in",
"listen_port": 7897
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "bond",
"tag": "bond-out",
"outbounds": [ // sum of download_ratio and upload_ratio must be 100
{
"outbound": {
"type": "vless",
"server": "0.0.0.0",
"server_port": 443,
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
"network": "tcp",
"bind_interface": ""
},
"download_ratio": 100,
"upload_ratio": 0
},
{
"outbound": {
"type": "vless",
"server": "0.0.0.0",
"server_port": 444,
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
"network": "tcp",
"bind_interface": ""
},
"download_ratio": 0,
"upload_ratio": 100
}
]
}
],
"route": {
"final": "bond-out",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}
+54
View File
@@ -0,0 +1,54 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "bond",
"tag": "bond-in",
"inbounds": [
{
"type": "vless",
"listen": "0.0.0.0",
"listen_port": 443,
"users": [
{
"name": "user",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
}
]
},
{
"type": "vless",
"listen": "0.0.0.0",
"listen_port": 444,
"users": [
{
"name": "user",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
}
]
}
]
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
],
"route": {
"final": "direct",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}
+62
View File
@@ -0,0 +1,62 @@
{
"log": {
"level": "debug"
},
"dns": {
"servers": [
{
"type": "udp",
"tag": "dns-cloudflare",
"server": "1.2.3.4"
},
{
"type": "udp",
"tag": "dns-google",
"server": "1.2.3.4"
},
{
"type": "https",
"tag": "dns-quad9-doh",
"server": "1.1.1.1"
},
{
"type": "fallback",
"tag": "dns-fallback",
"servers": [
"dns-cloudflare",
"dns-google",
"dns-quad9-doh"
],
// Strategies:
// - "sequential" (default): query servers in order; on each error move
// to the next one. Returns the first successful response, or the
// last error if all servers failed.
// - "parallel": query all servers concurrently. Returns
// the first successful response (cancelling the rest), or the last
// error if all servers failed.
"strategy": "sequential"
}
],
"disable_cache": true,
"independent_cache": true,
"final": "dns-fallback"
},
"inbounds": [
{
"type": "mixed",
"tag": "mixed-in",
"listen_port": 7897
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
}
],
"route": {
"final": "direct",
"default_domain_resolver": "dns-fallback",
"auto_detect_interface": true
}
}
+65
View File
@@ -0,0 +1,65 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "mixed",
"tag": "mixed-in",
"listen_port": 7897
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "failover",
"tag": "failover-out",
// - "sequential" (default): try outbounds in order; return the last error
// after exhausting them all.
// - "cycle": keep retrying outbounds in round-robin forever
// (useful for transient network outages on user devices).
"strategy": "cycle",
"delay": "2s", // wait between failed attempts; 0 = no delay
"outbounds": [
{
"type": "vless",
"tag": "vless-primary",
"server": "primary.example.com",
"server_port": 443,
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
},
{
"type": "trojan",
"tag": "trojan-secondary",
"server": "secondary.example.com",
"server_port": 443,
"password": "trojan-password"
},
{
"type": "shadowsocks",
"tag": "ss-tertiary",
"server": "tertiary.example.com",
"server_port": 8388,
"method": "aes-128-gcm",
"password": "ss-password"
}
]
}
],
"route": {
"final": "failover-out",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}
+58
View File
@@ -0,0 +1,58 @@
{
"log": {
"level": "info"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
// The "failover" inbound wraps several listeners. If any of them
// panics or fails to accept, the parent supervises and restarts it
// automatically without affecting the rest of the box.
"type": "failover",
"inbounds": [
{
"type": "mixed",
"tag": "socks-in-1",
"listen": "0.0.0.0",
"listen_port": 10001
},
{
"type": "mixed",
"tag": "socks-in-2",
"listen": "0.0.0.0",
"listen_port": 10002
},
{
"type": "vless",
"tag": "vless-in",
"listen": "0.0.0.0",
"listen_port": 8443,
"users": [
{
"name": "user",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
}
]
}
]
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
}
],
"route": {
"final": "direct",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}
+61
View File
@@ -0,0 +1,61 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "mixed",
"tag": "mixed-in",
"listen_port": 7897
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "vless",
"tag": "vless-1-out",
"server": "example1.com",
"server_port": 443,
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
},
{
"type": "vless",
"tag": "vless-2-out",
"server": "example2.com",
"server_port": 443,
"uuid": "294fd6bc-4f89-43e7-9228-7900aba396af"
},
{
"type": "vless",
"tag": "vless-3-out",
"server": "example3.com",
"server_port": 443,
"uuid": "257f20d0-294a-4f07-9f2c-9efee9a37400"
},
{
"type": "fallback",
"tag": "fallback-out",
"outbounds": [
"vless-1-out",
"vless-2-out",
"vless-3-out"
]
}
],
"route": {
"final": "fallback-out",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}
@@ -0,0 +1,58 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "vless",
"tag": "vless-in",
"listen": "0.0.0.0",
"listen_port": 443,
"transport": {
"type": "http"
},
"users": [
{
"name": "user1",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
},
{
"name": "user2",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
}
]
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "bandwidth-limiter",
"tag": "bandwidth-limiter",
"strategy": "connection",
"mode": "bidirectional", // download, upload
"connection_type": "hwid", // mux, ip
"speed": "1MB", // 100KB, 1GB, etc.
"flow_keys": ["user", "destination"], // values: user, destination, ip, hwid, mux
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
"rules": [],
"final": "direct"
}
}
],
"route": {
"final": "bandwidth-limiter",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}
+43
View File
@@ -0,0 +1,43 @@
{
"log": {
"level": "info"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "socks",
"tag": "socks-in",
"listen_port": 7897
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "bandwidth-limiter",
"tag": "bandwidth-limiter",
"strategy": "global",
"mode": "bidirectional", // download, upload
"speed": "2MB", // 100KB, 1GB, etc.
"flow_keys": ["user", "destination"], // values: user, destination, ip, hwid, mux
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
"rules": [],
"final": "direct"
}
}
],
"route": {
"final": "bandwidth-limiter",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}
+69
View File
@@ -0,0 +1,69 @@
{
"log": {
"level": "info"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "vless",
"tag": "vless-in",
"listen": "0.0.0.0",
"listen_port": 443,
"transport": {
"type": "http"
}
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "bandwidth-limiter",
"tag": "bandwidth-limiter",
// "manager" strategy: per-user bandwidth limits are loaded from the
// manager database and updated live (no need to list users in this file).
"strategy": "manager",
"flow_keys": ["user", "destination"], // values: user, destination, ip, hwid, mux
"route": {
"rules": [],
"final": "direct"
}
}
],
"route": {
"rules": [
{
"protocol": "dns",
"action": "hijack-dns"
}
],
"final": "bandwidth-limiter"
},
"services": [
{
"type": "manager",
"tag": "my-manager",
"database": {
"driver": "sqlite",
"dsn": "file:manager.db?_pragma=foreign_keys(on)&_pragma=journal_mode(wal)&_pragma=busy_timeout(5000)"
}
},
{
"type": "node",
"tag": "my-node",
"uuid": "e6eceb84-ad66-474b-8641-142499db7c6e",
"manager": "my-manager",
"inbounds": ["vless-in"],
"bandwidth_limiters": ["bandwidth-limiter"]
}
]
}
+81
View File
@@ -0,0 +1,81 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "vless",
"tag": "vless-in",
"listen": "0.0.0.0",
"listen_port": 443,
"transport": {
"type": "http"
},
"users": [
{
"name": "user1",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
},
{
"name": "user2",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
}
]
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "bandwidth-limiter",
"tag": "bidirectional-bandwidth-limiter",
"strategy": "global",
"mode": "bidirectional",
"speed": "5MB",
"flow_keys": ["user", "destination"], // values: user, destination, ip, hwid, mux
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
"rules": [],
"final": "direct"
}
},
{
"type": "bandwidth-limiter",
"tag": "upload-bandwidth-limiter",
"strategy": "global",
"mode": "upload",
"speed": "3MB",
"flow_keys": ["user", "destination"], // values: user, destination, ip, hwid, mux
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
"rules": [],
"final": "bidirectional-bandwidth-limiter"
}
},
{
"type": "bandwidth-limiter",
"tag": "download-bandwidth-limiter",
"strategy": "global",
"mode": "download",
"speed": "3MB",
"flow_keys": ["user", "destination"], // values: user, destination, ip, hwid, mux
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
"rules": [],
"final": "upload-bandwidth-limiter"
}
}
],
"route": {
"final": "download-bandwidth-limiter",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}
+71
View File
@@ -0,0 +1,71 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "vless",
"tag": "vless-in",
"listen": "0.0.0.0",
"listen_port": 443,
"transport": {
"type": "http"
},
"users": [
{
"name": "user1",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
},
{
"name": "user2",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
}
]
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "bandwidth-limiter",
"tag": "bandwidth-limiter",
"strategy": "users",
"flow_keys": ["user", "destination"], // values: user, destination, ip, hwid, mux
"users": [
{
"name": "user1",
"strategy": "connection", // global
"mode": "bidirectional", // download, upload
"connection_type": "hwid", // mux, ip
"speed": "5MB" // 100KB, 1GB, etc.
},
{
"name": "user2",
"strategy": "connection", // global
"mode": "bidirectional", // download, upload
"connection_type": "hwid", // mux, ip
"speed": "1MB" // 100KB, 1GB, etc.
}
],
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
"rules": [],
"final": "direct"
}
}
],
"route": {
"final": "bandwidth-limiter",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}
@@ -0,0 +1,56 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "vless",
"tag": "vless-in",
"listen": "0.0.0.0",
"listen_port": 443,
"transport": {
"type": "http"
},
"users": [
{
"name": "user1",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
},
{
"name": "user2",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
}
],
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "connection-limiter",
"tag": "connection-limiter",
"strategy": "connection",
"connection_type": "hwid", // mux, source_ip
"count": 5,
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
"rules": [],
"final": "direct"
}
}
],
"route": {
"final": "connection-limiter",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}
+68
View File
@@ -0,0 +1,68 @@
{
"log": {
"level": "info"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "vless",
"tag": "vless-in",
"listen": "0.0.0.0",
"listen_port": 443,
"transport": {
"type": "http"
}
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "connection-limiter",
"tag": "connection-limiter",
// "manager" strategy: per-user connection caps are loaded from the
// manager database and updated live (no need to list users in this file).
"strategy": "manager",
"route": {
"rules": [],
"final": "direct"
}
}
],
"route": {
"rules": [
{
"protocol": "dns",
"action": "hijack-dns"
}
],
"final": "connection-limiter"
},
"services": [
{
"type": "manager",
"tag": "my-manager",
"database": {
"driver": "sqlite",
"dsn": "file:manager.db?_pragma=foreign_keys(on)&_pragma=journal_mode(wal)&_pragma=busy_timeout(5000)"
}
},
{
"type": "node",
"tag": "my-node",
"uuid": "e6eceb84-ad66-474b-8641-142499db7c6e",
"manager": "my-manager",
"inbounds": ["vless-in"],
"connection_limiters": ["connection-limiter"]
}
]
}
+68
View File
@@ -0,0 +1,68 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "vless",
"tag": "vless-in",
"listen": "0.0.0.0",
"listen_port": 5000,
"transport": {
"type": "http"
},
"users": [
{
"name": "user1",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
},
{
"name": "user2",
"uuid": "6c8c7ffc-a909-4699-af34-e9d9bcb3e6d6"
}
],
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "connection-limiter",
"tag": "connection-limiter",
"strategy": "users",
"users": [
{
"name": "user1",
"strategy": "connection",
"connection_type": "hwid", // mux, source_ip
"count": 5
},
{
"name": "user2",
"strategy": "connection",
"connection_type": "hwid", // mux, source_ip
"count": 1
}
],
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
"rules": [],
"final": "direct"
}
}
],
"route": {
"final": "connection-limiter",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}
+53
View File
@@ -0,0 +1,53 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "vless",
"tag": "vless-in",
"listen": "0.0.0.0",
"listen_port": 443,
"transport": {
"type": "http"
},
"users": [
{
"name": "user1",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
}
]
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "rate-limiter",
"tag": "rate-limiter",
"strategy": "token-bucket", // leaky-bucket, sliding-window, fixed-window
"connection_type": "hwid", // mux, source_ip
"count": 20, // max requests per interval per connection
"interval": "1s",
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
"rules": [],
"final": "direct"
}
}
],
"route": {
"final": "rate-limiter",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}
+42
View File
@@ -0,0 +1,42 @@
{
"log": {
"level": "info"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "socks",
"tag": "socks-in",
"listen_port": 7897
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "rate-limiter",
"tag": "rate-limiter",
"strategy": "leaky-bucket", // token-bucket, sliding-window, fixed-window
"count": 10, // max requests per interval
"interval": "1s", // time window
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
"rules": [],
"final": "direct"
}
}
],
"route": {
"final": "rate-limiter",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}
+68
View File
@@ -0,0 +1,68 @@
{
"log": {
"level": "info"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "vless",
"tag": "vless-in",
"listen": "0.0.0.0",
"listen_port": 443,
"transport": {
"type": "http"
}
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "rate-limiter",
"tag": "rate-limiter",
// "manager" strategy: per-user rate limits are loaded from the
// manager database and updated live (no need to list users in this file).
"strategy": "manager",
"route": {
"rules": [],
"final": "direct"
}
}
],
"route": {
"rules": [
{
"protocol": "dns",
"action": "hijack-dns"
}
],
"final": "rate-limiter"
},
"services": [
{
"type": "manager",
"tag": "my-manager",
"database": {
"driver": "sqlite",
"dsn": "file:manager.db?_pragma=foreign_keys(on)&_pragma=journal_mode(wal)&_pragma=busy_timeout(5000)"
}
},
{
"type": "node",
"tag": "my-node",
"uuid": "e6eceb84-ad66-474b-8641-142499db7c6e",
"manager": "my-manager",
"inbounds": ["vless-in"],
"rate_limiters": ["rate-limiter"]
}
]
}
+70
View File
@@ -0,0 +1,70 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "vless",
"tag": "vless-in",
"listen": "0.0.0.0",
"listen_port": 443,
"transport": {
"type": "http"
},
"users": [
{
"name": "user1",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
},
{
"name": "user2",
"uuid": "6c8c7ffc-a909-4699-af34-e9d9bcb3e6d6"
}
]
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "rate-limiter",
"tag": "rate-limiter",
"strategy": "users",
"users": [
{
"name": "user1",
"strategy": "leaky-bucket", // token-bucket, sliding-window, fixed-window
"connection_type": "hwid", // mux, source_ip
"count": 30,
"interval": "1s"
},
{
"name": "user2",
"strategy": "sliding-window",
"connection_type": "source_ip",
"count": 5,
"interval": "1s"
}
],
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
"rules": [],
"final": "direct"
}
}
],
"route": {
"final": "rate-limiter",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}

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