Compare commits

...

22 Commits

Author SHA1 Message Date
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
147 changed files with 15361 additions and 200 deletions

191
.goreleaser.yaml Normal file
View File

@@ -0,0 +1,191 @@
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
env:
- CGO_ENABLED=0
- GOTOOLCHAIN=local
targets:
- linux_386
- linux_amd64_v1
- linux_arm64
- linux_arm_6
- linux_arm_7
- linux_s390x
- linux_riscv64
- linux_mips
- linux_mips_softfloat
- linux_mipsle
- linux_mipsle_softfloat
- linux_mips64
- linux_mips64le
- windows_amd64_v1
- windows_386
- windows_arm64
- darwin_amd64_v1
- darwin_arm64
mod_timestamp: '{{ .CommitTimestamp }}'
- id: manager
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_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
- linux_mips
- linux_mips_softfloat
- linux_mipsle
- linux_mipsle_softfloat
- linux_mips64
- linux_mips64le
- windows_amd64_v1
- windows_386
- windows_arm64
- darwin_amd64_v1
- darwin_arm64
mod_timestamp: '{{ .CommitTimestamp }}'
- id: legacy
<<: *template
tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_utls
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
targets:
- windows_amd64_v1
- windows_386
- 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
- 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_with_manager
builds:
- manager
formats:
- tar.gz
format_overrides:
- goos: windows
formats:
- zip
wrap_in_directory: true
files:
- LICENSE
name_template: '{{ .ProjectName }}-{{ .Version }}-with-manager-{{ .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

View File

@@ -70,14 +70,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
@@ -101,7 +97,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

View File

@@ -2,22 +2,44 @@
Sing-box with extended features.
## Features
## 🔥 Features
* Amnezia 1.5
* WARP
* Tunneling
* Mieru
* XHTTP
* SDNS (DNSCrypt)
* Extended Wireguard options
* Unified delay
### 🌐 Outbounds
- **WARP** — Cloudflare WARP integration through WireGuard
- **Tunnel** — Protocol for creating tunnels across nodes
- **Bond** — Link aggregation for increased throughput
- **Mieru** — Secure, hard to classify, hard to probe network protocol
- **Failover** — Automatic outbound switching for high availability
## Examples
### 🚦 Limiters
- **Bandwidth Limiter** — Upload / download rate limiting
- **Connection Limiter** — Concurrent connection control
### 🛡 Encryption & Obfuscation
- **Amnezia 1.5** — 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 squads, nodes, users, limiters
- **Node Manager** — Service for connecting nodes to remote manager
### ⚙ Miscellaneous
- **SDNS (DNSCrypt)** — Encrypted DNS queries for enhanced privacy
- **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.
@@ -42,7 +64,7 @@ bc1qqx97p8k4dchqkyd47s4vf74hrqdfnmhqvcja7x
0xAcc5919C22F2B3fAa0ec7E8BaD142da5B375FBF6
```
## License
## 📄 License
```
Copyright (C) 2022 by nekohasekai <contact-sagernet@sekai.icu>

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 {

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)

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 {

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)

1
box.go
View File

@@ -161,6 +161,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)

View File

@@ -1,5 +1,3 @@
//go:build with_quic
package main
import (

57
common/kmutex/mutex.go Normal file
View File

@@ -0,0 +1,57 @@
package kmutex
import "sync"
type Kmutex[T comparable] struct {
l sync.Locker
s map[T]*klock
}
type klock struct {
cond *sync.Cond
ref uint64
}
// Create new Kmutex
func New[T comparable]() *Kmutex[T] {
l := sync.Mutex{}
return &Kmutex[T]{
l: &l,
s: make(map[T]*klock),
}
}
// Unlock Kmutex by unique ID
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()
}
// Lock Kmutex by unique ID
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
}
}

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

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

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

25
common/vision/hook.go Normal file
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
}

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
)

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

View File

@@ -1,43 +1,53 @@
package constant
const (
TypeTun = "tun"
TypeRedirect = "redirect"
TypeTProxy = "tproxy"
TypeDirect = "direct"
TypeBlock = "block"
TypeDNS = "dns"
TypeSOCKS = "socks"
TypeHTTP = "http"
TypeMixed = "mixed"
TypeShadowsocks = "shadowsocks"
TypeVMess = "vmess"
TypeTrojan = "trojan"
TypeNaive = "naive"
TypeWireGuard = "wireguard"
TypeWARP = "warp"
TypeHysteria = "hysteria"
TypeTor = "tor"
TypeSSH = "ssh"
TypeShadowTLS = "shadowtls"
TypeMieru = "mieru"
TypeAnyTLS = "anytls"
TypeShadowsocksR = "shadowsocksr"
TypeVLESS = "vless"
TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2"
TypeTunnelClient = "tunnel_client"
TypeTunnelServer = "tunnel_server"
TypeTailscale = "tailscale"
TypeDERP = "derp"
TypeResolved = "resolved"
TypeSSMAPI = "ssm-api"
TypeCCM = "ccm"
TypeOCM = "ocm"
TypeOOMKiller = "oom-killer"
TypeTun = "tun"
TypeRedirect = "redirect"
TypeTProxy = "tproxy"
TypeDirect = "direct"
TypeBlock = "block"
TypeDNS = "dns"
TypeSOCKS = "socks"
TypeHTTP = "http"
TypeMixed = "mixed"
TypeShadowsocks = "shadowsocks"
TypeVMess = "vmess"
TypeTrojan = "trojan"
TypeNaive = "naive"
TypeWireGuard = "wireguard"
TypeWARP = "warp"
TypeHysteria = "hysteria"
TypeTor = "tor"
TypeSSH = "ssh"
TypeShadowTLS = "shadowtls"
TypeMieru = "mieru"
TypeAnyTLS = "anytls"
TypeShadowsocksR = "shadowsocksr"
TypeVLESS = "vless"
TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2"
TypeBond = "bond"
TypeTunnelServer = "tunnel-server"
TypeTunnelClient = "tunnel-client"
TypeTailscale = "tailscale"
TypeConnectionLimiter = "connection-limiter"
TypeBandwidthLimiter = "bandwidth-limiter"
TypeTrafficLimiter = "traffic-limiter"
TypeAdminPanel = "admin-panel"
TypeNodeManagerServer = "node-manager-server"
TypeNodeManagerClient = "node-manager-client"
TypeDERP = "derp"
TypeManager = "manager"
TypeNode = "node"
TypeResolved = "resolved"
TypeSSMAPI = "ssm-api"
TypeCCM = "ccm"
TypeOCM = "ocm"
TypeOOMKiller = "oom-killer"
)
const (
TypeFailover = "failover"
TypeSelector = "selector"
TypeURLTest = "urltest"
)
@@ -90,10 +100,14 @@ func ProxyDisplayName(proxyType string) string {
return "TUIC"
case TypeHysteria2:
return "Hysteria2"
case TypeBond:
return "Bond"
case TypeMieru:
return "Mieru"
case TypeAnyTLS:
return "AnyTLS"
case TypeFailover:
return "Failover"
case TypeTailscale:
return "Tailscale"
case TypeSelector:
@@ -101,9 +115,9 @@ func ProxyDisplayName(proxyType string) string {
case TypeURLTest:
return "URLTest"
case TypeTunnelClient:
return "Tunnel Client"
return "Tunnel client"
case TypeTunnelServer:
return "Tunnel Server"
return "Tunnel server"
default:
return "Unknown"
}

View File

@@ -7,4 +7,5 @@ const (
V2RayTransportTypeGRPC = "grpc"
V2RayTransportTypeHTTPUpgrade = "httpupgrade"
V2RayTransportTypeXHTTP = "xhttp"
V2RayTransportTypeKCP = "mkcp"
)

View File

@@ -0,0 +1,57 @@
{
"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": "duplex", // 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
}
}

View File

@@ -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": "bandwidth-limiter",
"tag": "bandwidth-limiter",
"strategy": "global",
"mode": "duplex", // download, upload
"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
}
}

View File

@@ -0,0 +1,78 @@
{
"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": "duplex-bandwidth-limiter",
"strategy": "global",
"mode": "duplex",
"speed": "5MB",
"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",
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
"rules": [],
"final": "duplex-bandwidth-limiter"
}
},
{
"type": "bandwidth-limiter",
"tag": "download-bandwidth-limiter",
"strategy": "global",
"mode": "download",
"speed": "3MB",
"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
}
}

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": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
}
]
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "bandwidth-limiter",
"tag": "bandwidth-limiter",
"strategy": "users",
"users": [
{
"name": "user1",
"strategy": "connection", // global
"mode": "duplex", // download, upload
"connection_type": "hwid", // mux, ip
"speed": "5MB", // 100KB, 1GB, etc.
},
{
"name": "user2",
"strategy": "connection", // global
"mode": "duplex", // 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
}
}

61
examples/bond/client.json Normal file
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
}
}

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

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
examples/bond/server.json Normal file
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
}
}

View File

@@ -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, 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
}
}

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, ip
"count": 5,
},
{
"name": "user2",
"strategy": "connection",
"connection_type": "hwid", // mux, 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
}
}

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": "failover",
"tag": "failover-out",
"outbounds": [
"vless-1-out",
"vless-2-out",
"vless-3-out"
]
}
],
"route": {
"final": "failover-out",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}

View File

@@ -0,0 +1,70 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [],
"outbounds": [
{
"type": "direct",
"tag": "direct-out"
},
{
"type": "dns",
"tag": "dns-out"
}
],
"route": {
"rules": [
{
"protocol": "dns",
"outbound": "dns-out"
},
{
"port": 53,
"outbound": "dns-out"
},
],
"final": "direct-out"
},
"services": [
{
"type": "manager",
"tag": "my-manager",
"database": {
"driver": "postgresql",
"dsn": "postgresql://postgres:postgres@localhost:5432/manager?sslmode=disable"
}
},
{ // http://127.0.0.1:8000
// Username: admin
// Password: admin
"type": "admin-panel",
"tag": "my-admin-panel",
"listen_port": 8000,
"manager": "my-manager",
"database": {
"driver": "postgresql",
"dsn": "postgresql://postgres:postgres@localhost:5432/adminpanel?sslmode=disable"
}
},
{
"type": "node-manager-server", // for connecting nodes
"listen_port": 7000,
"manager": "my-manager",
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#inbound
"enabled": true,
"server_name": "example.com",
"certificate_path": "/path/to/fullchain.pem",
"key_path": "/path/to/privkey.pem"
},
}
]
}

View File

@@ -0,0 +1,85 @@
{
"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"
}
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct-out"
},
{
"type": "dns",
"tag": "dns-out"
},
{
"type": "bandwidth-limiter",
"tag": "bandwidth-limiter",
"strategy": "manager",
"route": {
"final": "direct-out"
}
},
{
"type": "connection-limiter",
"tag": "connection-limiter",
"strategy": "manager",
"route": {
"final": "bandwidth-limiter"
}
},
],
"route": {
"rules": [
{
"protocol": "dns",
"outbound": "dns-out"
},
{
"port": 53,
"outbound": "dns-out"
}
],
"final": "connection-limiter"
},
"services": [
{
"type": "node",
"tag": "my-node",
"uuid": "e6eceb84-ad66-474b-8641-142499db7c6e",
"manager": "node-manager",
"inbounds": ["vless-in"],
"bandwidth_limiters": ["bandwidth-limiter"],
"connection_limiters": ["connection-limiter"],
},
{
"type": "node-manager-client",
"tag": "node-manager",
"server": "example.com",
"server_port": 7000,
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#outbound
"enabled": true,
"server_name": "example.com",
"alpn": "h2" // h3 for QUIC
},
}
]
}

43
examples/mkcp/client.json Normal file
View File

@@ -0,0 +1,43 @@
{
"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": "example.com",
"server_port": 443,
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
"packet_encoding": "",
"transport": {
"type": "mkcp",
"mtu": 1500
}
}
],
"route": {
"final": "vless-out",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}

42
examples/mkcp/server.json Normal file
View File

@@ -0,0 +1,42 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "vless",
"tag": "vless-in",
"listen": "0.0.0.0",
"listen_port": 443,
"users": [
{
"name": "user",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
}
],
"transport": {
"type": "mkcp",
"mtu": 1500
}
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
}
],
"route": {
"final": "direct",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}

View File

@@ -1,6 +1,6 @@
{
"log": {
"level": "error"
"level": "info"
},
"dns": {
"servers": [
@@ -12,7 +12,7 @@
},
"endpoints": [
{
"type": "tunnel_client",
"type": "tunnel-client",
"tag": "tunnel",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe",
@@ -30,7 +30,7 @@
{
"type": "mixed",
"tag": "mixed-in",
"listen_port": 7897
"listen_port": 10000
}
],
"outbounds": [
@@ -41,16 +41,22 @@
{
"type": "dns",
"tag": "dns-out"
},
{
"type": "failover",
"tag": "f",
"outbounds": ["tunnel", "direct-out"],
"interrupt_exist_connections": false,
}
],
"route": {
"rules": [
{
"outbound": "tunnel",
"outbound": "f",
"override_tunnel_destination": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13"
}
],
"final": "direct-out",
"final": "f",
"default_domain_resolver": "default",
"auto_detect_interface": true
}

View File

@@ -1,6 +1,6 @@
{
"log": {
"level": "error"
"level": "info"
},
"dns": {
"servers": [
@@ -12,7 +12,7 @@
},
"endpoints": [
{
"type": "tunnel_server",
"type": "tunnel-server",
"tag": "tunnel",
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13",
"users": [

View File

@@ -12,7 +12,7 @@
},
"endpoints": [
{
"type": "tunnel_client",
"type": "tunnel-client",
"tag": "tunnel",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe",

View File

@@ -12,7 +12,7 @@
},
"endpoints": [
{
"type": "tunnel_client",
"type": "tunnel-client",
"tag": "tunnel",
"uuid": "487f6073-3300-4819-a07d-39652e45fb4d",
"key": "3d74d616-2502-4c17-9cc3-92c366550f4f",

View File

@@ -12,7 +12,7 @@
},
"endpoints": [
{
"type": "tunnel_server",
"type": "tunnel-server",
"tag": "tunnel",
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13",
"users": [

View File

@@ -12,7 +12,7 @@
},
"endpoints": [
{
"type": "tunnel_server",
"type": "tunnel-server",
"tag": "tunnel",
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13",
"users": [

View File

@@ -12,7 +12,7 @@
},
"endpoints": [
{
"type": "tunnel_client",
"type": "tunnel-client",
"tag": "tunnel",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe",

View File

@@ -1,6 +1,6 @@
{
"log": {
"level": "error"
"level": "info"
},
"dns": {
"servers": [
@@ -12,7 +12,7 @@
},
"endpoints": [
{
"type": "tunnel_client",
"type": "tunnel-client",
"tag": "tunnel",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe",

View File

@@ -1,6 +1,6 @@
{
"log": {
"level": "error"
"level": "info"
},
"dns": {
"servers": [
@@ -12,7 +12,7 @@
},
"endpoints": [
{
"type": "tunnel_server",
"type": "tunnel-server",
"tag": "tunnel",
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13",
"users": [
@@ -39,7 +39,7 @@
{
"type": "mixed",
"tag": "mixed-in",
"listen_port": 7897
"listen_port": 10000
}
],
"outbounds": [

View File

@@ -0,0 +1,40 @@
{
"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": "example.com",
"server_port": 443,
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
"encryption": "", // xray vlessenc
"packet_encoding": ""
}
],
"route": {
"final": "vless-out",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}

View File

@@ -0,0 +1,39 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "vless",
"tag": "vless-in",
"listen": "0.0.0.0",
"listen_port": 443,
"decryption": "", // xray vlessenc
"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
}
}

52
go.mod
View File

@@ -3,6 +3,8 @@ module github.com/sagernet/sing-box
go 1.25.5
require (
github.com/GoAdminGroup/go-admin v1.2.26
github.com/GoAdminGroup/themes v0.0.48
github.com/anthropics/anthropic-sdk-go v1.26.0
github.com/anytls/sing-anytls v0.0.11
github.com/caddyserver/certmagic v0.25.2
@@ -10,12 +12,18 @@ require (
github.com/cretz/bine v0.2.0
github.com/database64128/tfo-go/v2 v2.3.2
github.com/enfein/mieru/v3 v3.17.1
github.com/go-chi/chi v1.5.5
github.com/go-chi/chi/v5 v5.2.5
github.com/go-chi/render v1.0.3
github.com/go-playground/validator/v10 v10.30.1
github.com/godbus/dbus/v5 v5.2.2
github.com/gofrs/uuid/v5 v5.4.0
github.com/golang-migrate/migrate/v4 v4.19.1
github.com/huandu/go-sqlbuilder v1.39.1
github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91
github.com/jackc/pgx/v5 v5.8.0
github.com/keybase/go-keychain v0.0.1
github.com/lib/pq v1.10.9
github.com/libdns/acmedns v0.5.0
github.com/libdns/alidns v1.0.6
github.com/libdns/cloudflare v0.2.2
@@ -25,6 +33,7 @@ require (
github.com/miekg/dns v1.1.72
github.com/openai/openai-go/v3 v3.24.0
github.com/oschwald/maxminddb-golang v1.13.1
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
github.com/sagernet/cors v1.2.1
@@ -64,7 +73,10 @@ require (
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/360EntSecGroup-Skylar/excelize v1.4.1 // indirect
github.com/AdguardTeam/golibs v0.32.7 // indirect
github.com/GoAdminGroup/html v0.0.1 // indirect
github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
@@ -82,12 +94,19 @@ require (
github.com/florianl/go-nfqueue/v2 v2.0.2 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gaissmai/bart v0.18.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.3.0 // indirect
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
@@ -95,14 +114,27 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/huandu/go-clone v1.7.3 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0
github.com/leodido/go-urn v1.4.0 // indirect
github.com/libdns/libdns v1.1.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/mdlayher/netlink v1.9.0 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/nxadm/tail v1.4.11 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
@@ -141,6 +173,7 @@ require (
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect
@@ -153,6 +186,7 @@ require (
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
@@ -162,17 +196,27 @@ require (
golang.org/x/sync v0.19.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/time v0.12.0
golang.org/x/tools v0.42.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
lukechampine.com/blake3 v1.4.1
xorm.io/builder v0.3.7 // indirect
xorm.io/xorm v1.0.2 // indirect
)
replace github.com/sagernet/wireguard-go => github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.3.2
replace github.com/sagernet/wireguard-go => github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.3.4
replace github.com/sagernet/tailscale => github.com/shtorm-7/tailscale v1.92.4-sing-box-1.13-mod.6-extended-1.0.1
replace github.com/sagernet/tailscale => github.com/shtorm-7/tailscale v1.92.4-sing-box-1.13-mod.6-extended-1.0.2
replace github.com/sagernet/sing-mux => github.com/shtorm-7/sing-mux v0.3.4-extended-1.0.0
replace github.com/ameshkov/dnscrypt/v2 => github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0
replace github.com/sagernet/sing-vmess => github.com/starifly/sing-vmess v0.2.7-mod.9

294
go.sum
View File

@@ -1,13 +1,37 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
github.com/360EntSecGroup-Skylar/excelize v1.4.1 h1:l55mJb6rkkaUzOpSsgEeKYtS6/0gHwBYyfo5Jcjv/Ks=
github.com/360EntSecGroup-Skylar/excelize v1.4.1/go.mod h1:vnax29X2usfl7HHkBrX5EvSCJcmH3dT9luvxzu8iGAE=
github.com/AdguardTeam/golibs v0.32.7 h1:3dmGlAVgmvquCCwHsvEl58KKcRAK3z1UnjMnwSIeDH4=
github.com/AdguardTeam/golibs v0.32.7/go.mod h1:bE8KV1zqTzgZjmjFyBJ9f9O5DEKO717r7e57j1HclJA=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/GoAdminGroup/go-admin v1.2.26 h1:kk18rVrteLcrzH7iMM5p/13jghDC5n3DJG/7zAnbnEU=
github.com/GoAdminGroup/go-admin v1.2.26/go.mod h1:QXj94ZrDclKzqwZnAGUWaK3qY1Wfr6/Qy5GnRGeXR+k=
github.com/GoAdminGroup/html v0.0.1 h1:SdWNWl4OKPsvDk2GDp5ZKD6ceWoN8n4Pj6cUYxavUd0=
github.com/GoAdminGroup/html v0.0.1/go.mod h1:A1laTJaOx8sQ64p2dE8IqtstDeCNBHEazrEp7hR5VvM=
github.com/GoAdminGroup/themes v0.0.48 h1:OveEEoFBCBTU5kNicqnvs0e/pL6uZKNQU1RAP9kmNFA=
github.com/GoAdminGroup/themes v0.0.48/go.mod h1:w/5P0WCmM8iv7DYE5scIT8AODYMoo6zj/bVlzAbgOaU=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e h1:n+DcnTNkQnHlwpsrHoQtkrJIO7CBx029fw6oR4vIob4=
github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
@@ -18,6 +42,8 @@ github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAf
github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q=
github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc=
github.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc=
github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg=
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
@@ -28,8 +54,13 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -40,28 +71,57 @@ github.com/database64128/netx-go v0.1.1/go.mod h1:LNlYVipaYkQArRFDNNJ02VkNV+My9A
github.com/database64128/tfo-go/v2 v2.3.2 h1:UhZMKiMq3swZGUiETkLBDzQnZBPSAeBMClpJGlnJ5Fw=
github.com/database64128/tfo-go/v2 v2.3.2/go.mod h1:GC3uB5oa4beGpCUbRb2ZOWP73bJJFmMyAVgQSO7r724=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
github.com/denisenkom/go-mssqldb v0.0.0-20190707035753-2be1aa521ff4/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
github.com/denisenkom/go-mssqldb v0.0.0-20200206145737-bbfc9a55622e h1:LzwWXEScfcTu7vUZNlDDWDARoSGEtvlDKK2BYHowNeE=
github.com/denisenkom/go-mssqldb v0.0.0-20200206145737-bbfc9a55622e/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU=
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.17.1 h1:pIKbspsKRYNyUrORVI33t1/yz2syaaUkIanskAbGBHY=
github.com/enfein/mieru/v3 v3.17.1/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE=
github.com/florianl/go-nfqueue/v2 v2.0.2/go.mod h1:VA09+iPOT43OMoCKNfXHyzujQUty2xmzyCRkBOlmabc=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gaissmai/bart v0.18.0 h1:jQLBT/RduJu0pv/tLwXE+xKPgtWJejbxuXAR+wLJafo=
github.com/gaissmai/bart v0.18.0/go.mod h1:JJzMAhNF5Rjo4SF4jWBrANuJfqY+FvsFhW7t1UZJ+XY=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs=
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
@@ -70,12 +130,26 @@ github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZR
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I=
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
@@ -84,42 +158,103 @@ github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U=
github.com/huandu/go-assert v1.1.6 h1:oaAfYxq9KNDi9qswn/6aE0EydfxSa+tWZC1KabNitYs=
github.com/huandu/go-assert v1.1.6/go.mod h1:JuIfbmYG9ykwvuxoJ3V8TB5QP+3+ajIA54Y44TmkMxs=
github.com/huandu/go-clone v1.7.3 h1:rtQODA+ABThEn6J5LBTppJfKmZy/FwfpMUWa8d01TTQ=
github.com/huandu/go-clone v1.7.3/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE=
github.com/huandu/go-sqlbuilder v1.39.1 h1:uUaj41yLNTQBe7ojNF6Im1RPbHCN4zCjMRySTEC2ooI=
github.com/huandu/go-sqlbuilder v1.39.1/go.mod h1:zdONH67liL+/TvoUMwnZP/sUYGSSvHh9psLe/HpXn8E=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91 h1:u9i04mGE3iliBh0EFuWaKsmcwrLacqGmq1G3XoaM7gY=
github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU=
github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk=
github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U=
github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/libdns/acmedns v0.5.0 h1:5pRtmUj4Lb/QkNJSl1xgOGBUJTWW7RjpNaIhjpDXjPE=
github.com/libdns/acmedns v0.5.0/go.mod h1:X7UAFP1Ep9NpTwWpVlrZzJLR7epynAy0wrIxSPFgKjQ=
github.com/libdns/alidns v1.0.6 h1:/Ii428ty6WHFJmE24rZxq2taq++gh7rf9jhgLfp8PmM=
@@ -130,6 +265,14 @@ github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco=
github.com/mdlayher/netlink v1.9.0/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
@@ -142,23 +285,67 @@ github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/openai/openai-go/v3 v3.24.0 h1:08x6GnYiB+AAejTo6yzPY8RkZMJQ8NpreiOyM5QfyYU=
github.com/openai/openai-go/v3 v3.24.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4=
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
@@ -244,8 +431,6 @@ github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgj
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
github.com/sagernet/sing v0.8.2 h1:kX1IH9SWJv4S0T9M8O+HNahWgbOuY1VauxbF7NU5lOg=
github.com/sagernet/sing v0.8.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
github.com/sagernet/sing-quic v0.6.0 h1:dhrFnP45wgVKEOT1EvtsToxdzRnHIDIAgj6WHV9pLyM=
github.com/sagernet/sing-quic v0.6.0/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
@@ -256,26 +441,36 @@ github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-tun v0.8.2 h1:rQr/x3eQCHh3oleIaoJdPdJwqzZp4+QWcJLT0Wz2xKY=
github.com/sagernet/sing-tun v0.8.2/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY=
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0 h1:e5s7RKBd2rIPR0StbvZ2vTVtJ5jDTsTk5wtIIapZTRg=
github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI=
github.com/shtorm-7/tailscale v1.92.4-sing-box-1.13-mod.6-extended-1.0.1 h1:7xEmwWocT6yKIObtKMdaYD6kG6vVvl02Mm7Jo5PGr6Y=
github.com/shtorm-7/tailscale v1.92.4-sing-box-1.13-mod.6-extended-1.0.1/go.mod h1:E80TFYhiqOWekKiqj0p0Sedd+yJJ2hzPYVSXWVOVFHo=
github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.3.2 h1:xDtfY7iJx12b48NdNyY5hXF8aCLwjfXPQbz6YkAfuZc=
github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.3.2/go.mod h1:Me2JlCDYHxnd0mnuX7L5LXAeDHCltI7vSKq3eTE6SVE=
github.com/shtorm-7/sing-mux v0.3.4-extended-1.0.0 h1:a5OoXr3e2ACbM6vDIaaGL44IdHQ6wPjcSoU13vfC0Sw=
github.com/shtorm-7/sing-mux v0.3.4-extended-1.0.0/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
github.com/shtorm-7/tailscale v1.92.4-sing-box-1.13-mod.6-extended-1.0.2 h1:gCTT0YleFvcaqKwLVoLLXEUqtN8at45XGuoP77EA/CQ=
github.com/shtorm-7/tailscale v1.92.4-sing-box-1.13-mod.6-extended-1.0.2/go.mod h1:TYIIqO5sZpWq873rLIeO2usszSMUpR3h6WdqVVs65ug=
github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.3.4 h1:t/2ZxRo8cwvydImFaKuUSDrcZYhX753JiXGe7411krI=
github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.3.4/go.mod h1:Me2JlCDYHxnd0mnuX7L5LXAeDHCltI7vSKq3eTE6SVE=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/starifly/sing-vmess v0.2.7-mod.9 h1:xobAmejSbBQ0A3f/EtJ9cJd3m6gK7dDPccPdeGz7tXY=
github.com/starifly/sing-vmess v0.2.7-mod.9/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ=
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
@@ -304,6 +499,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
@@ -315,8 +512,12 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
@@ -340,28 +541,56 @@ go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
@@ -369,18 +598,25 @@ golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
@@ -389,25 +625,57 @@ golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20260309223511-c9735035b4c6 h1:q2oSL6CrUQDUOT8D70ImK10gGRTIZjhR7fgSU//5kc0=
gvisor.dev/gvisor v0.0.0-20260309223511-c9735035b4c6/go.mod h1:xQ2PWgHmWJA/Ph4i1q1jBm39BKhc3W0DXqWoDSyuBOY=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
xorm.io/builder v0.3.7 h1:2pETdKRK+2QG4mLX4oODHEhn5Z8j1m8sXa7jfu+/SZI=
xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/xorm v1.0.2 h1:kZlCh9rqd1AzGwWitcrEEqHE1h1eaZE/ujU5/2tWEtg=
xorm.io/xorm v1.0.2/go.mod h1:o4vnEsQ5V2F1/WK6w4XTwmiWJeGj82tqjAnHe44wVHY=

View File

@@ -19,9 +19,12 @@ import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/anytls"
"github.com/sagernet/sing-box/protocol/block"
"github.com/sagernet/sing-box/protocol/bond"
"github.com/sagernet/sing-box/protocol/direct"
"github.com/sagernet/sing-box/protocol/group"
"github.com/sagernet/sing-box/protocol/http"
"github.com/sagernet/sing-box/protocol/limiter/bandwidth"
"github.com/sagernet/sing-box/protocol/limiter/connection"
"github.com/sagernet/sing-box/protocol/mieru"
"github.com/sagernet/sing-box/protocol/mixed"
"github.com/sagernet/sing-box/protocol/naive"
@@ -36,6 +39,11 @@ import (
"github.com/sagernet/sing-box/protocol/tunnel"
"github.com/sagernet/sing-box/protocol/vless"
"github.com/sagernet/sing-box/protocol/vmess"
"github.com/sagernet/sing-box/service/admin_panel"
"github.com/sagernet/sing-box/service/manager"
"github.com/sagernet/sing-box/service/node"
nodeManagerClient "github.com/sagernet/sing-box/service/node_manager/client"
nodeManagerServer "github.com/sagernet/sing-box/service/node_manager/server"
"github.com/sagernet/sing-box/service/resolved"
"github.com/sagernet/sing-box/service/ssmapi"
E "github.com/sagernet/sing/common/exceptions"
@@ -65,6 +73,8 @@ func InboundRegistry() *inbound.Registry {
vless.RegisterInbound(registry)
anytls.RegisterInbound(registry)
bond.RegisterInbound(registry)
registerQUICInbounds(registry)
registerStubForRemovedInbounds(registry)
@@ -78,6 +88,7 @@ func OutboundRegistry() *outbound.Registry {
block.RegisterOutbound(registry)
group.RegisterFailover(registry)
group.RegisterSelector(registry)
group.RegisterURLTest(registry)
@@ -94,6 +105,11 @@ func OutboundRegistry() *outbound.Registry {
mieru.RegisterOutbound(registry)
anytls.RegisterOutbound(registry)
bond.RegisterOutbound(registry)
bandwidth.RegisterOutbound(registry)
connection.RegisterOutbound(registry)
registerQUICOutbounds(registry)
registerStubForRemovedOutbounds(registry)
@@ -135,6 +151,11 @@ func DNSTransportRegistry() *dns.TransportRegistry {
func ServiceRegistry() *service.Registry {
registry := service.NewRegistry()
admin_panel.RegisterService(registry)
manager.RegisterService(registry)
node.RegisterService(registry)
nodeManagerClient.RegisterService(registry)
nodeManagerServer.RegisterService(registry)
resolved.RegisterService(registry)
ssmapi.RegisterService(registry)

View File

@@ -13,6 +13,8 @@ func init() {
}
type idKey struct{}
type muxIdKey struct{}
type hwidKey struct{}
type ID struct {
ID uint32
@@ -34,3 +36,28 @@ func IDFromContext(ctx context.Context) (ID, bool) {
id, loaded := ctx.Value((*idKey)(nil)).(ID)
return id, loaded
}
func ContextWithNewMuxID(ctx context.Context) context.Context {
return ContextWithMuxID(ctx, ID{
ID: rand.Uint32(),
CreatedAt: time.Now(),
})
}
func ContextWithMuxID(ctx context.Context, id ID) context.Context {
return context.WithValue(ctx, (*muxIdKey)(nil), id)
}
func MuxIDFromContext(ctx context.Context) (ID, bool) {
id, loaded := ctx.Value((*muxIdKey)(nil)).(ID)
return id, loaded
}
func ContextWithHWID(ctx context.Context, id ID) context.Context {
return context.WithValue(ctx, (*hwidKey)(nil), id)
}
func HWIDFromContext(ctx context.Context) (ID, bool) {
id, loaded := ctx.Value((*hwidKey)(nil)).(ID)
return id, loaded
}

13
option/admin_panel.go Normal file
View File

@@ -0,0 +1,13 @@
package option
type AdminPanelServiceOptions struct {
ListenOptions
Manager string `json:"manager"`
Database AdminPanelServiceDatabase `json:"database"`
InboundTLSOptionsContainer
}
type AdminPanelServiceDatabase struct {
Driver string `json:"driver"`
DSN string `json:"dsn"`
}

16
option/bond.go Normal file
View File

@@ -0,0 +1,16 @@
package option
type BondInboundOptions struct {
Inbounds []Inbound `json:"inbounds"`
}
type BondOutboundOptions struct {
Outbounds []BondOutbound `json:"outbounds"`
}
type BondOutbound struct {
Outbound Outbound `json:"outbound"`
DownloadRatio uint8 `json:"download_ratio"`
UploadRatio uint8 `json:"upload_ratio"`
Count uint8 `json:"count"`
}

View File

@@ -16,3 +16,7 @@ type URLTestOutboundOptions struct {
IdleTimeout badoption.Duration `json:"idle_timeout,omitempty"`
InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"`
}
type FailoverOutboundOptions struct {
Outbounds []string `json:"outbounds"`
}

37
option/limiter.go Normal file
View File

@@ -0,0 +1,37 @@
package option
import (
"github.com/sagernet/sing/common/byteformats"
)
type BandwidthLimiterOutboundOptions struct {
Strategy string `json:"strategy"`
Mode string `json:"mode"`
ConnectionType string `json:"connection_type,omitempty"`
Speed *byteformats.NetworkBytesCompat `json:"speed"`
Users []BandwidthLimiterUser `json:"users,omitempty"`
Route RouteOptions `json:"route"`
}
type BandwidthLimiterUser struct {
Name string `json:"name"`
Strategy string `json:"strategy"`
Mode string `json:"mode"`
ConnectionType string `json:"connection_type,omitempty"`
Speed *byteformats.NetworkBytesCompat `json:"speed"`
}
type ConnectionLimiterOutboundOptions struct {
Strategy string `json:"strategy"`
ConnectionType string `json:"connection_type,omitempty"`
Count uint32 `json:"count"`
Users []ConnectionLimiterUser `json:"users,omitempty"`
Route RouteOptions `json:"route"`
}
type ConnectionLimiterUser struct {
Name string `json:"name"`
Strategy string `json:"strategy"`
ConnectionType string `json:"connection_type,omitempty"`
Count uint32 `json:"count"`
}

11
option/manager.go Normal file
View File

@@ -0,0 +1,11 @@
package option
type ManagerServiceDatabase struct {
Driver string `json:"driver"`
DSN string `json:"dsn"`
}
type ManagerServiceOptions struct {
Inbounds []string `json:"inbounds"`
Database ManagerServiceDatabase `json:"database"`
}

9
option/node.go Normal file
View File

@@ -0,0 +1,9 @@
package option
type NodeServiceOptions struct {
UUID string
Inbounds []string `json:"inbounds"`
ConnectionLimiters []string `json:"connection_limiters"`
BandwidthLimiters []string `json:"bandwidth_limiters"`
Manager string `json:"manager"`
}

13
option/node_manager.go Normal file
View File

@@ -0,0 +1,13 @@
package option
type NodeManagerServerServiceOptions struct {
ListenOptions
InboundTLSOptionsContainer
Manager string `json:"manager"`
}
type NodeManagerClientServiceOptions struct {
DialerOptions
ServerOptions
OutboundTLSOptionsContainer
}

View File

@@ -21,6 +21,7 @@ type _V2RayTransportOptions struct {
GRPCOptions V2RayGRPCOptions `json:"-"`
HTTPUpgradeOptions V2RayHTTPUpgradeOptions `json:"-"`
XHTTPOptions V2RayXHTTPOptions `json:"-"`
KCPOptions V2RayKCPOptions `json:"-"`
}
type V2RayTransportOptions _V2RayTransportOptions
@@ -40,6 +41,8 @@ func (o V2RayTransportOptions) MarshalJSON() ([]byte, error) {
v = o.HTTPUpgradeOptions
case C.V2RayTransportTypeXHTTP:
v = o.XHTTPOptions
case C.V2RayTransportTypeKCP:
v = o.KCPOptions
case "":
return nil, E.New("missing transport type")
default:
@@ -67,6 +70,8 @@ func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error {
v = &o.HTTPUpgradeOptions
case C.V2RayTransportTypeXHTTP:
v = &o.XHTTPOptions
case C.V2RayTransportTypeKCP:
v = &o.KCPOptions
default:
return E.New("unknown transport type: " + o.Type)
}
@@ -468,3 +473,64 @@ func (m *V2RayXHTTPXmuxOptions) GetNormalizedHMaxRequestTimes() Xbadoption.Range
func (m *V2RayXHTTPXmuxOptions) GetNormalizedHMaxReusableSecs() Xbadoption.Range {
return m.HMaxReusableSecs
}
type V2RayKCPOptions struct {
MTU uint32 `json:"mtu,omitempty"`
TTI uint32 `json:"tti,omitempty"`
UplinkCapacity uint32 `json:"uplink_capacity,omitempty"`
DownlinkCapacity uint32 `json:"downlink_capacity,omitempty"`
Congestion bool `json:"congestion,omitempty"`
ReadBufferSize uint32 `json:"read_buffer_size,omitempty"`
WriteBufferSize uint32 `json:"write_buffer_size,omitempty"`
HeaderType string `json:"header_type,omitempty"`
Seed string `json:"seed,omitempty"`
}
func (k *V2RayKCPOptions) GetMTU() uint32 {
if k.MTU == 0 {
return 1350
}
return k.MTU
}
func (k *V2RayKCPOptions) GetTTI() uint32 {
if k.TTI == 0 {
return 50
}
return k.TTI
}
func (k *V2RayKCPOptions) GetUplinkCapacity() uint32 {
if k.UplinkCapacity == 0 {
return 12
}
return k.UplinkCapacity
}
func (k *V2RayKCPOptions) GetDownlinkCapacity() uint32 {
if k.DownlinkCapacity == 0 {
return 100
}
return k.DownlinkCapacity
}
func (k *V2RayKCPOptions) GetReadBufferSize() uint32 {
if k.ReadBufferSize == 0 {
return 1
}
return k.ReadBufferSize
}
func (k *V2RayKCPOptions) GetWriteBufferSize() uint32 {
if k.WriteBufferSize == 0 {
return 1
}
return k.WriteBufferSize
}
func (k *V2RayKCPOptions) GetHeaderType() string {
if k.HeaderType == "" {
return "none"
}
return k.HeaderType
}

View File

@@ -2,7 +2,8 @@ package option
type VLESSInboundOptions struct {
ListenOptions
Users []VLESSUser `json:"users,omitempty"`
Users []VLESSUser `json:"users,omitempty"`
Decryption string `json:"decryption,omitempty"`
InboundTLSOptionsContainer
Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"`
Transport *V2RayTransportOptions `json:"transport,omitempty"`
@@ -17,9 +18,10 @@ type VLESSUser struct {
type VLESSOutboundOptions struct {
DialerOptions
ServerOptions
UUID string `json:"uuid"`
Flow string `json:"flow,omitempty"`
Network NetworkList `json:"network,omitempty"`
UUID string `json:"uuid"`
Flow string `json:"flow,omitempty"`
Encryption string `json:"encryption,omitempty"`
Network NetworkList `json:"network,omitempty"`
OutboundTLSOptionsContainer
Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
Transport *V2RayTransportOptions `json:"transport,omitempty"`

View File

@@ -3,6 +3,7 @@ package option
import (
"net/netip"
Xbadoption "github.com/sagernet/sing-box/common/xray/json/badoption"
"github.com/sagernet/sing/common/json/badoption"
)
@@ -33,15 +34,17 @@ type WireGuardPeer struct {
}
type WireGuardWARPEndpointOptions struct {
System bool `json:"system,omitempty"`
Name string `json:"name,omitempty"`
ListenPort uint16 `json:"listen_port,omitempty"`
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
Workers int `json:"workers,omitempty"`
PreallocatedBuffersPerPool uint32 `json:"preallocated_buffers_per_pool,omitempty"`
DisablePauses bool `json:"disable_pauses,omitempty"`
Amnezia *WireGuardAmnezia `json:"amnezia,omitempty"`
Profile WARPProfile `json:"profile,omitempty"`
System bool `json:"system,omitempty"`
Name string `json:"name,omitempty"`
ListenPort uint16 `json:"listen_port,omitempty"`
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
PersistentKeepaliveInterval uint16 `json:"persistent_keepalive_interval,omitempty"`
Reserved []uint8 `json:"reserved,omitempty"`
Workers int `json:"workers,omitempty"`
PreallocatedBuffersPerPool uint32 `json:"preallocated_buffers_per_pool,omitempty"`
DisablePauses bool `json:"disable_pauses,omitempty"`
Amnezia *WireGuardAmnezia `json:"amnezia,omitempty"`
Profile WARPProfile `json:"profile,omitempty"`
DialerOptions
}
@@ -54,24 +57,24 @@ type WARPProfile struct {
}
type WireGuardAmnezia struct {
JC int `json:"jc,omitempty"`
JMin int `json:"jmin,omitempty"`
JMax int `json:"jmax,omitempty"`
S1 int `json:"s1,omitempty"`
S2 int `json:"s2,omitempty"`
S3 int `json:"s3,omitempty"`
S4 int `json:"s4,omitempty"`
H1 uint32 `json:"h1,omitempty"`
H2 uint32 `json:"h2,omitempty"`
H3 uint32 `json:"h3,omitempty"`
H4 uint32 `json:"h4,omitempty"`
I1 string `json:"i1,omitempty"`
I2 string `json:"i2,omitempty"`
I3 string `json:"i3,omitempty"`
I4 string `json:"i4,omitempty"`
I5 string `json:"i5,omitempty"`
J1 string `json:"j1,omitempty"`
J2 string `json:"j2,omitempty"`
J3 string `json:"j3,omitempty"`
ITime int64 `json:"itime,omitempty"`
JC int `json:"jc,omitempty"`
JMin int `json:"jmin,omitempty"`
JMax int `json:"jmax,omitempty"`
S1 int `json:"s1,omitempty"`
S2 int `json:"s2,omitempty"`
S3 int `json:"s3,omitempty"`
S4 int `json:"s4,omitempty"`
H1 *Xbadoption.Range `json:"h1,omitempty"`
H2 *Xbadoption.Range `json:"h2,omitempty"`
H3 *Xbadoption.Range `json:"h3,omitempty"`
H4 *Xbadoption.Range `json:"h4,omitempty"`
I1 string `json:"i1,omitempty"`
I2 string `json:"i2,omitempty"`
I3 string `json:"i3,omitempty"`
I4 string `json:"i4,omitempty"`
I5 string `json:"i5,omitempty"`
J1 string `json:"j1,omitempty"`
J2 string `json:"j2,omitempty"`
J3 string `json:"j3,omitempty"`
ITime int64 `json:"itime,omitempty"`
}

164
protocol/bond/conn.go Normal file
View File

@@ -0,0 +1,164 @@
package bond
import (
"encoding/binary"
"errors"
"io"
"net"
"time"
)
type bondedConn struct {
conns []net.Conn
downloadRatios []uint8
uploadRatios []uint8
readBuffer []byte
readOffset int
readSize int
}
func NewBondedConn(conns []net.Conn, downloadRatios, uploadRatios []uint8) *bondedConn {
return &bondedConn{
conns: conns,
downloadRatios: downloadRatios,
uploadRatios: uploadRatios,
readBuffer: make([]byte, 65535),
}
}
func (c *bondedConn) Read(b []byte) (n int, err error) {
if c.readOffset == c.readSize {
var header [2]byte
_, err := io.ReadFull(c.conns[0], header[:])
if err != nil {
return 0, err
}
size := int(binary.BigEndian.Uint16(header[:]))
chunkLens := splitByRatios(size, c.downloadRatios)
total := 0
for i, chunkLen := range chunkLens {
if chunkLen == 0 {
continue
}
chunk := c.readBuffer[total : total+chunkLen]
n, err := io.ReadFull(c.conns[i], chunk)
total += n
if err != nil {
return total, err
}
}
c.readOffset = 0
c.readSize = size
}
n = copy(b, c.readBuffer[c.readOffset:c.readSize])
c.readOffset += n
return n, nil
}
func (c *bondedConn) Write(b []byte) (n int, err error) {
chunkLens := splitByRatios(len(b), c.uploadRatios)
var header [2]byte
binary.BigEndian.PutUint16(header[:], uint16(len(b)))
_, err = c.conns[0].Write(header[:])
if err != nil {
return 0, err
}
total := 0
for i, chunkLen := range chunkLens {
if chunkLen == 0 {
continue
}
chunk := b[total : total+chunkLen]
conn := c.conns[i]
subTotal := 0
for subTotal < len(chunk) {
n, err := conn.Write(chunk[subTotal:])
subTotal += n
total += n
if err != nil {
return total, err
}
if n == 0 {
return total, io.ErrUnexpectedEOF
}
}
}
return total, err
}
func (c *bondedConn) Close() error {
errs := make([]error, 0)
for _, conn := range c.conns {
err := conn.Close()
if err != nil {
errs = append(errs, err)
}
}
if len(errs) != 0 {
return errors.Join(errs...)
}
return nil
}
func (c *bondedConn) LocalAddr() net.Addr {
return nil
}
func (c *bondedConn) RemoteAddr() net.Addr {
return nil
}
func (c *bondedConn) SetDeadline(t time.Time) error {
errs := make([]error, 0)
for _, conn := range c.conns {
err := conn.SetDeadline(t)
if err != nil {
errs = append(errs, err)
}
}
if len(errs) != 0 {
return errors.Join(errs...)
}
return nil
}
func (c *bondedConn) SetReadDeadline(t time.Time) error {
errs := make([]error, 0)
for _, conn := range c.conns {
err := conn.SetReadDeadline(t)
if err != nil {
errs = append(errs, err)
}
}
if len(errs) != 0 {
return errors.Join(errs...)
}
return nil
}
func (c *bondedConn) SetWriteDeadline(t time.Time) error {
errs := make([]error, 0)
for _, conn := range c.conns {
err := conn.SetWriteDeadline(t)
if err != nil {
errs = append(errs, err)
}
}
if len(errs) != 0 {
return errors.Join(errs...)
}
return nil
}
func splitByRatios(number int, ratios []uint8) []int {
result := make([]int, len(ratios))
remaining := number
for i := 0; i < len(ratios)-1; i++ {
part := number * int(ratios[i]) / 100
result[i] = part
remaining -= part
}
result[len(ratios)-1] = remaining
return result
}

155
protocol/bond/inbound.go Normal file
View File

@@ -0,0 +1,155 @@
package bond
import (
"context"
"errors"
"net"
"time"
"github.com/patrickmn/go-cache"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/common/kmutex"
"github.com/sagernet/sing-box/common/uot"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
func RegisterInbound(registry *inbound.Registry) {
inbound.Register[option.BondInboundOptions](registry, C.TypeBond, NewInbound)
}
type Inbound struct {
inbound.Adapter
logger logger.ContextLogger
router adapter.ConnectionRouterEx
inbounds []adapter.Inbound
conns *cache.Cache
mtx *kmutex.Kmutex[string]
}
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.BondInboundOptions) (adapter.Inbound, error) {
if len(options.Inbounds) == 0 {
return nil, E.New("missing tags")
}
inbound := &Inbound{
Adapter: inbound.NewAdapter(C.TypeBond, tag),
logger: logger,
router: uot.NewRouter(router, logger),
conns: cache.New(C.TCPConnectTimeout, time.Second),
mtx: kmutex.New[string](),
}
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
inbounds := make([]adapter.Inbound, len(options.Inbounds))
for i, inboundOptions := range options.Inbounds {
inbound, err := inboundRegistry.UnsafeCreate(ctx, NewRouter(router, logger, inbound.connHandler), logger, inboundOptions.Tag, inboundOptions.Type, inboundOptions.Options)
if err != nil {
return nil, err
}
inbounds[i] = inbound
}
inbound.inbounds = inbounds
inbound.conns.OnEvicted(func(s string, i interface{}) {
inbound.mtx.Lock(s)
defer inbound.mtx.Unlock(s)
ratioConns := i.(map[uint8]*ratioConn)
for _, ratioConn := range ratioConns {
if ratioConn != nil {
ratioConn.conn.Close()
}
}
})
return inbound, nil
}
func (h *Inbound) Start(stage adapter.StartStage) error {
for _, inbound := range h.inbounds {
err := inbound.Start(stage)
if err != nil {
return err
}
}
return nil
}
func (h *Inbound) Close() error {
errs := make([]error, 0)
for _, inbound := range h.inbounds {
err := inbound.Close()
if err != nil {
errs = append(errs, err)
}
}
if len(errs) != 0 {
return errors.Join(errs...)
}
return nil
}
func (h *Inbound) connHandler(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
if metadata.Destination != Destination {
h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
return nil
}
request, err := ReadRequest(conn)
if err != nil {
return err
}
requestUUID := request.UUID.String()
h.mtx.Lock(requestUUID)
var ratioConns map[uint8]*ratioConn
rawRatioConns, ok := h.conns.Get(requestUUID)
if ok {
ratioConns = rawRatioConns.(map[uint8]*ratioConn)
} else {
ratioConns = make(map[uint8]*ratioConn, request.Count)
h.conns.SetDefault(requestUUID, ratioConns)
}
ratioConns[request.Index] = &ratioConn{
conn: conn,
downloadRatio: request.DownloadRatio,
uploadRatio: request.UploadRatio,
}
if len(ratioConns) == int(request.Count) {
conns := make([]net.Conn, len(ratioConns))
downloadRatios := make([]uint8, len(ratioConns))
uploadRatios := make([]uint8, len(ratioConns))
var totalDownloadRatio, totalUploadRatio uint8
for index, ratioConn := range ratioConns {
conns[index] = ratioConn.conn
downloadRatios[index] = ratioConn.downloadRatio
uploadRatios[index] = ratioConn.uploadRatio
totalDownloadRatio += ratioConn.downloadRatio
totalUploadRatio += ratioConn.uploadRatio
delete(ratioConns, index)
}
if totalDownloadRatio != 100 || totalUploadRatio != 100 {
for _, conn := range conns {
conn.Close()
}
h.mtx.Unlock(requestUUID)
return E.New("invalid ratios")
}
conn = NewBondedConn(conns, downloadRatios, uploadRatios)
metadata.Inbound = h.Tag()
metadata.InboundType = C.TypeBond
metadata.Destination = request.Destination
h.mtx.Unlock(requestUUID)
h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
return nil
}
h.mtx.Unlock(requestUUID)
return nil
}
type ratioConn struct {
conn net.Conn
downloadRatio uint8
uploadRatio uint8
}

152
protocol/bond/outbound.go Normal file
View File

@@ -0,0 +1,152 @@
package bond
import (
"context"
"errors"
"net"
"sync"
"github.com/gofrs/uuid/v5"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/outbound"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/uot"
"github.com/sagernet/sing/service"
)
func RegisterOutbound(registry *outbound.Registry) {
outbound.Register[option.BondOutboundOptions](registry, C.TypeBond, NewOutbound)
}
type Outbound struct {
outbound.Adapter
ctx context.Context
logger logger.ContextLogger
outbounds []adapter.Outbound
downloadRatios []uint8
uploadRatios []uint8
uotClient *uot.Client
}
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.BondOutboundOptions) (adapter.Outbound, error) {
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
outbounds := make([]adapter.Outbound, 0, len(options.Outbounds))
downloadRatios := make([]uint8, 0, len(options.Outbounds))
uploadRatios := make([]uint8, 0, len(options.Outbounds))
var totalDownloadRatio, totalUploadRatio uint8
for _, outboundOptions := range options.Outbounds {
count := outboundOptions.Count
if count == 0 {
count = 1
}
for range count {
outbound, err := outboundRegistry.UnsafeCreateOutbound(ctx, router, logger, outboundOptions.Outbound.Tag, outboundOptions.Outbound.Type, outboundOptions.Outbound.Options)
if err != nil {
return nil, err
}
outbounds = append(outbounds, outbound)
downloadRatios = append(downloadRatios, outboundOptions.DownloadRatio)
uploadRatios = append(uploadRatios, outboundOptions.UploadRatio)
totalDownloadRatio += outboundOptions.DownloadRatio
totalUploadRatio += outboundOptions.UploadRatio
}
}
if totalDownloadRatio != 100 || totalUploadRatio != 100 {
return nil, E.New("invalid ratios")
}
outbound := &Outbound{
Adapter: outbound.NewAdapter(C.TypeBond, tag, []string{N.NetworkTCP, N.NetworkUDP}, []string{}),
ctx: ctx,
outbounds: outbounds,
downloadRatios: downloadRatios,
uploadRatios: uploadRatios,
logger: logger,
}
outbound.uotClient = &uot.Client{
Dialer: outbound,
Version: uot.Version,
}
return outbound, nil
}
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if N.NetworkName(network) == N.NetworkUDP {
return h.uotClient.DialContext(ctx, network, destination)
}
conns := make([]net.Conn, len(h.outbounds))
connUUID, err := uuid.NewV4()
if err != nil {
return nil, err
}
errs := make([]error, 0, len(conns))
var mtx sync.Mutex
var wg sync.WaitGroup
for i, outbound := range h.outbounds {
wg.Go(
func() {
conn, err := outbound.DialContext(ctx, network, Destination)
if err != nil {
mtx.Lock()
errs = append(errs, err)
mtx.Unlock()
return
}
err = WriteRequest(
conn,
&Request{
UUID: connUUID,
Index: byte(i),
Count: byte(len(h.outbounds)),
DownloadRatio: h.uploadRatios[i],
UploadRatio: h.downloadRatios[i],
Destination: destination,
},
)
if err != nil {
conn.Close()
mtx.Lock()
errs = append(errs, err)
mtx.Unlock()
return
}
conns[i] = conn
},
)
}
wg.Wait()
if len(errs) != 0 {
for _, conn := range conns {
if conn != nil {
conn.Close()
}
}
return nil, errors.Join(errs...)
}
conn := NewBondedConn(conns, h.downloadRatios, h.uploadRatios)
return conn, nil
}
func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return h.uotClient.ListenPacket(ctx, destination)
}
func (h *Outbound) Close() error {
errs := make([]error, 0)
for _, outbound := range h.outbounds {
err := common.Close(outbound)
if err != nil {
errs = append(errs, err)
}
}
if len(errs) != 0 {
return errors.Join(errs...)
}
return nil
}

100
protocol/bond/protocol.go Normal file
View File

@@ -0,0 +1,100 @@
package bond
import (
"encoding/binary"
"io"
"github.com/gofrs/uuid/v5"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
)
const (
Version = 0
)
var Destination = M.Socksaddr{
Fqdn: "sp.bond.sing-box.arpa",
Port: 444,
}
var AddressSerializer = M.NewSerializer(
M.AddressFamilyByte(0x01, M.AddressFamilyIPv4),
M.AddressFamilyByte(0x03, M.AddressFamilyIPv6),
M.AddressFamilyByte(0x02, M.AddressFamilyFqdn),
M.PortThenAddress(),
)
type Request struct {
UUID uuid.UUID
Index byte
Count byte
DownloadRatio byte
UploadRatio byte
Destination M.Socksaddr
}
func ReadRequest(reader io.Reader) (*Request, error) {
var request Request
var version uint8
err := binary.Read(reader, binary.BigEndian, &version)
if err != nil {
return nil, err
}
if version != Version {
return nil, E.New("unknown version: ", version)
}
_, err = io.ReadFull(reader, request.UUID[:])
if err != nil {
return nil, err
}
err = binary.Read(reader, binary.BigEndian, &request.Index)
if err != nil {
return nil, err
}
err = binary.Read(reader, binary.BigEndian, &request.Count)
if err != nil {
return nil, err
}
err = binary.Read(reader, binary.BigEndian, &request.DownloadRatio)
if err != nil {
return nil, err
}
err = binary.Read(reader, binary.BigEndian, &request.UploadRatio)
if err != nil {
return nil, err
}
request.Destination, err = AddressSerializer.ReadAddrPort(reader)
if err != nil {
return nil, err
}
return &request, nil
}
func WriteRequest(writer io.Writer, request *Request) error {
var requestLen int
requestLen += 1 // version
requestLen += 16 // UUID
requestLen += 1 // index
requestLen += 1 // count
requestLen += 1 // download ratio
requestLen += 1 // upload ratio
requestLen += AddressSerializer.AddrPortLen(request.Destination)
buffer := buf.NewSize(requestLen)
defer buffer.Release()
common.Must(
buffer.WriteByte(Version),
common.Error(buffer.Write(request.UUID[:])),
buffer.WriteByte(request.Index),
buffer.WriteByte(request.Count),
buffer.WriteByte(request.DownloadRatio),
buffer.WriteByte(request.UploadRatio),
)
err := AddressSerializer.WriteAddrPort(buffer, request.Destination)
if err != nil {
return err
}
return common.Error(writer.Write(buffer.Bytes()))
}

41
protocol/bond/router.go Normal file
View File

@@ -0,0 +1,41 @@
package bond
import (
"context"
"net"
"os"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/logger"
N "github.com/sagernet/sing/common/network"
)
type Router struct {
adapter.Router
logger logger.ContextLogger
handler func(context.Context, net.Conn, adapter.InboundContext, N.CloseHandlerFunc) error
}
func NewRouter(router adapter.Router, logger logger.ContextLogger, handler func(context.Context, net.Conn, adapter.InboundContext, N.CloseHandlerFunc) error) *Router {
return &Router{Router: router, logger: logger, handler: handler}
}
func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
return r.handler(ctx, conn, metadata, func(error) {})
}
func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
return os.ErrInvalid
}
func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
if err := r.handler(ctx, conn, metadata, onClose); err != nil {
r.logger.ErrorContext(ctx, err)
N.CloseOnHandshakeFailure(conn, onClose, err)
}
}
func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
r.logger.ErrorContext(ctx, os.ErrInvalid)
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
}

109
protocol/group/failover.go Normal file
View File

@@ -0,0 +1,109 @@
package group
import (
"context"
"net"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/outbound"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
func RegisterFailover(registry *outbound.Registry) {
outbound.Register[option.FailoverOutboundOptions](registry, C.TypeFailover, NewFailover)
}
var (
_ adapter.OutboundGroup = (*Failover)(nil)
)
type Failover struct {
outbound.Adapter
ctx context.Context
outbound adapter.OutboundManager
logger logger.ContextLogger
tags []string
outbounds map[string]adapter.Outbound
lastUsedOutbound string
mtx sync.Mutex
}
func NewFailover(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.FailoverOutboundOptions) (adapter.Outbound, error) {
if len(options.Outbounds) == 0 {
return nil, E.New("missing tags")
}
outbound := &Failover{
Adapter: outbound.NewAdapter(C.TypeFailover, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.Outbounds),
ctx: ctx,
outbound: service.FromContext[adapter.OutboundManager](ctx),
logger: logger,
tags: options.Outbounds,
outbounds: make(map[string]adapter.Outbound, len(options.Outbounds)),
lastUsedOutbound: options.Outbounds[0],
}
return outbound, nil
}
func (s *Failover) Start() error {
for i, tag := range s.tags {
outbound, loaded := s.outbound.Outbound(tag)
if !loaded {
return E.New("outbound ", i, " not found: ", tag)
}
s.outbounds[tag] = outbound
}
return nil
}
func (s *Failover) Now() string {
s.mtx.Lock()
defer s.mtx.Unlock()
return s.lastUsedOutbound
}
func (s *Failover) All() []string {
return s.tags
}
func (s *Failover) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
var conn net.Conn
var err error
for _, outbound := range s.outbounds {
conn, err = outbound.DialContext(ctx, network, destination)
if err != nil {
s.logger.ErrorContext(ctx, err)
continue
}
s.mtx.Lock()
defer s.mtx.Unlock()
s.lastUsedOutbound = outbound.Tag()
return conn, nil
}
return nil, err
}
func (s *Failover) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
var conn net.PacketConn
var err error
for _, outbound := range s.outbounds {
conn, err = outbound.ListenPacket(ctx, destination)
if err != nil {
s.logger.ErrorContext(ctx, err)
continue
}
s.mtx.Lock()
defer s.mtx.Unlock()
s.lastUsedOutbound = outbound.Tag()
return conn, nil
}
return nil, err
}

View File

@@ -178,3 +178,11 @@ func (h *Inbound) Close() error {
common.PtrOrNil(h.service),
)
}
func (h *Inbound) UpdateUsers(users []option.HysteriaUser) {
h.service.UpdateUsers(common.MapIndexed(users, func(index int, _ option.HysteriaUser) int {
return index
}), common.Map(users, func(it option.HysteriaUser) string {
return it.AuthString
}))
}

View File

@@ -211,3 +211,11 @@ func (h *Inbound) Close() error {
common.PtrOrNil(h.service),
)
}
func (h *Inbound) UpdateUsers(users []option.Hysteria2User) {
h.service.UpdateUsers(common.MapIndexed(users, func(index int, _ option.Hysteria2User) int {
return index
}), common.Map(users, func(it option.Hysteria2User) string {
return it.Password
}))
}

View File

@@ -0,0 +1,158 @@
package bandwidth
import (
"context"
"net"
"golang.org/x/time/rate"
)
type connWithDownloadBandwidthLimiter struct {
net.Conn
ctx context.Context
limiter *rate.Limiter
burst int
}
func NewConnWithDownloadBandwidthLimiter(ctx context.Context, conn net.Conn, limiter *rate.Limiter) *connWithDownloadBandwidthLimiter {
return &connWithDownloadBandwidthLimiter{conn, ctx, limiter, limiter.Burst()}
}
func (conn *connWithDownloadBandwidthLimiter) Write(p []byte) (n int, err error) {
var nn int
for {
end := len(p)
if end == 0 {
break
}
if conn.burst < len(p) {
end = conn.burst
}
err = conn.limiter.WaitN(conn.ctx, end)
if err != nil {
return
}
nn, err = conn.Conn.Write(p[:end])
n += nn
if err != nil {
return
}
p = p[end:]
}
return
}
type connWithUploadBandwidthLimiter struct {
net.Conn
ctx context.Context
limiter *rate.Limiter
burst int
}
func NewConnWithUploadBandwidthLimiter(ctx context.Context, conn net.Conn, limiter *rate.Limiter) *connWithUploadBandwidthLimiter {
return &connWithUploadBandwidthLimiter{conn, ctx, limiter, limiter.Burst()}
}
func (conn *connWithUploadBandwidthLimiter) Read(p []byte) (n int, err error) {
if conn.burst < len(p) {
p = p[:conn.burst]
}
n, err = conn.Conn.Read(p)
if err != nil {
return
}
err = conn.limiter.WaitN(conn.ctx, n)
if err != nil {
return
}
return
}
type connWithCloseHandler struct {
net.Conn
onClose CloseHandlerFunc
}
func NewConnWithCloseHandler(conn net.Conn, onClose CloseHandlerFunc) *connWithCloseHandler {
return &connWithCloseHandler{conn, onClose}
}
func (conn *connWithCloseHandler) Close() error {
conn.onClose()
return conn.Conn.Close()
}
type packetConnWithDownloadBandwidthLimiter struct {
net.PacketConn
ctx context.Context
limiter *rate.Limiter
burst int
}
func NewPacketConnWithDownloadBandwidthLimiter(ctx context.Context, conn net.PacketConn, limiter *rate.Limiter) *packetConnWithDownloadBandwidthLimiter {
return &packetConnWithDownloadBandwidthLimiter{conn, ctx, limiter, limiter.Burst()}
}
func (conn *packetConnWithDownloadBandwidthLimiter) WriteTo(p []byte, addr net.Addr) (n int, err error) {
var nn int
for {
end := len(p)
if end == 0 {
break
}
if conn.burst < len(p) {
end = conn.burst
}
err = conn.limiter.WaitN(conn.ctx, end)
if err != nil {
return
}
nn, err = conn.PacketConn.WriteTo(p[:end], addr)
n += nn
if err != nil {
return
}
p = p[end:]
}
return
}
type packetConnWithUploadBandwidthLimiter struct {
net.PacketConn
ctx context.Context
limiter *rate.Limiter
burst int
}
func NewPacketConnWithUploadBandwidthLimiter(ctx context.Context, conn net.PacketConn, limiter *rate.Limiter) *packetConnWithUploadBandwidthLimiter {
return &packetConnWithUploadBandwidthLimiter{conn, ctx, limiter, limiter.Burst()}
}
func (conn *packetConnWithUploadBandwidthLimiter) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
if conn.burst < len(p) {
p = p[:conn.burst]
}
n, addr, err = conn.PacketConn.ReadFrom(p)
if err != nil {
return
}
err = conn.limiter.WaitN(conn.ctx, n)
if err != nil {
return
}
return
}
type packetConnWithCloseHandler struct {
net.PacketConn
onClose CloseHandlerFunc
}
func NewPacketConnWithCloseHandler(conn net.PacketConn, onClose CloseHandlerFunc) *packetConnWithCloseHandler {
return &packetConnWithCloseHandler{conn, onClose}
}
func (conn *packetConnWithCloseHandler) Close() error {
conn.onClose()
return conn.PacketConn.Close()
}

View File

@@ -0,0 +1,146 @@
package bandwidth
import (
"context"
"net"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/outbound"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/route"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
func RegisterOutbound(registry *outbound.Registry) {
outbound.Register[option.BandwidthLimiterOutboundOptions](registry, C.TypeBandwidthLimiter, NewOutbound)
}
type Outbound struct {
outbound.Adapter
ctx context.Context
outbound adapter.OutboundManager
connection adapter.ConnectionManager
logger logger.ContextLogger
strategy BandwidthStrategy
outboundTag string
detour adapter.Outbound
router *route.Router
}
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.BandwidthLimiterOutboundOptions) (adapter.Outbound, error) {
if options.Strategy == "" {
return nil, E.New("missing strategy")
}
if options.Route.Final == "" {
return nil, E.New("missing final outbound")
}
var strategy BandwidthStrategy
var err error
switch options.Strategy {
case "users":
usersStrategies := make(map[string]BandwidthStrategy, len(options.Users))
for _, user := range options.Users {
userStrategy, err := CreateStrategy(user.Strategy, user.Mode, user.ConnectionType, options.Speed.Value())
if err != nil {
return nil, err
}
usersStrategies[user.Name] = userStrategy
}
strategy = NewUsersBandwidthStrategy(usersStrategies)
case "manager":
strategy = NewManagerBandwidthStrategy()
default:
strategy, err = CreateStrategy(options.Strategy, options.Mode, options.ConnectionType, options.Speed.Value())
if err != nil {
return nil, err
}
}
logFactory := service.FromContext[log.Factory](ctx)
r := route.NewRouter(ctx, logFactory, options.Route, option.DNSOptions{})
err = r.Initialize(options.Route.Rules, options.Route.RuleSet)
if err != nil {
return nil, err
}
outbound := &Outbound{
Adapter: outbound.NewAdapter(C.TypeBandwidthLimiter, tag, nil, []string{}),
ctx: ctx,
outbound: service.FromContext[adapter.OutboundManager](ctx),
connection: service.FromContext[adapter.ConnectionManager](ctx),
logger: logger,
strategy: strategy,
outboundTag: options.Route.Final,
router: r,
}
return outbound, nil
}
func (h *Outbound) Network() []string {
return []string{N.NetworkTCP, N.NetworkUDP}
}
func (h *Outbound) Start() error {
detour, loaded := h.outbound.Outbound(h.outboundTag)
if !loaded {
return E.New("outbound not found: ", h.outboundTag)
}
h.detour = detour
for _, stage := range []adapter.StartStage{adapter.StartStateStart, adapter.StartStatePostStart, adapter.StartStateStarted} {
err := h.router.Start(stage)
if err != nil {
return err
}
}
return nil
}
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
conn, err := h.detour.DialContext(ctx, network, destination)
if err != nil {
return nil, err
}
return h.strategy.wrapConn(ctx, conn, adapter.ContextFrom(ctx), true)
}
func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
conn, err := h.detour.ListenPacket(ctx, destination)
if err != nil {
return nil, err
}
return h.strategy.wrapPacketConn(ctx, conn, adapter.ContextFrom(ctx), true)
}
func (h *Outbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
conn, err := h.strategy.wrapConn(ctx, conn, &metadata, false)
if err != nil {
h.logger.ErrorContext(ctx, err)
return
}
metadata.Inbound = h.Tag()
metadata.InboundType = h.Type()
h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
return
}
func (h *Outbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
packetConn, err := h.strategy.wrapPacketConn(ctx, bufio.NewNetPacketConn(conn), &metadata, false)
if err != nil {
h.logger.ErrorContext(ctx, err)
return
}
metadata.Inbound = h.Tag()
metadata.InboundType = h.Type()
h.router.RoutePacketConnectionEx(ctx, bufio.NewPacketConn(packetConn), metadata, onClose)
return
}
func (h *Outbound) GetStrategy() BandwidthStrategy {
return h.strategy
}

View File

@@ -0,0 +1,266 @@
package bandwidth
import (
"context"
"net"
"strconv"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/time/rate"
)
type (
CloseHandlerFunc = func()
ConnIDGetter = func(context.Context, *adapter.InboundContext) (string, bool)
ConnWrapper = func(ctx context.Context, conn net.Conn, limiter *rate.Limiter, reverse bool) net.Conn
PacketConnWrapper = func(ctx context.Context, conn net.PacketConn, limiter *rate.Limiter, reverse bool) net.PacketConn
)
type BandwidthStrategy interface {
wrapConn(ctx context.Context, conn net.Conn, metadata *adapter.InboundContext, reverse bool) (net.Conn, error)
wrapPacketConn(ctx context.Context, conn net.PacketConn, metadata *adapter.InboundContext, reverse bool) (net.PacketConn, error)
}
type BandwidthLimiterStrategy interface {
getLimiter(ctx context.Context, metadata *adapter.InboundContext) (*rate.Limiter, CloseHandlerFunc, error)
}
type DefaultWrapStrategy struct {
limiterStrategy BandwidthLimiterStrategy
connWrapper ConnWrapper
packetConnWrapper PacketConnWrapper
}
func NewDefaultWrapStrategy(limiterStrategy BandwidthLimiterStrategy, connWrapper ConnWrapper, packetConnWrapper PacketConnWrapper) *DefaultWrapStrategy {
return &DefaultWrapStrategy{limiterStrategy, connWrapper, packetConnWrapper}
}
func (s *DefaultWrapStrategy) wrapConn(ctx context.Context, conn net.Conn, metadata *adapter.InboundContext, reverse bool) (net.Conn, error) {
limiter, onClose, err := s.limiterStrategy.getLimiter(ctx, metadata)
if err != nil {
return nil, err
}
return NewConnWithCloseHandler(s.connWrapper(ctx, conn, limiter, reverse), onClose), nil
}
func (s *DefaultWrapStrategy) wrapPacketConn(ctx context.Context, conn net.PacketConn, metadata *adapter.InboundContext, reverse bool) (net.PacketConn, error) {
limiter, onClose, err := s.limiterStrategy.getLimiter(ctx, metadata)
if err != nil {
return nil, err
}
return NewPacketConnWithCloseHandler(s.packetConnWrapper(ctx, conn, limiter, reverse), onClose), nil
}
type GlobalBandwidthStrategy struct {
limiter *rate.Limiter
}
func NewGlobalBandwidthStrategy(speed uint64) *GlobalBandwidthStrategy {
return &GlobalBandwidthStrategy{
limiter: createSpeedLimiter(speed),
}
}
func (s *GlobalBandwidthStrategy) getLimiter(ctx context.Context, metadata *adapter.InboundContext) (*rate.Limiter, CloseHandlerFunc, error) {
return s.limiter, func() {}, nil
}
type idBandwidthLimiter struct {
limiter *rate.Limiter
handles uint32
}
type ConnectionBandwidthStrategy struct {
limiters map[string]*idBandwidthLimiter
connIDGetter ConnIDGetter
speed uint64
mtx sync.Mutex
}
func NewConnectionBandwidthStrategy(connIDGetter ConnIDGetter, speed uint64) *ConnectionBandwidthStrategy {
return &ConnectionBandwidthStrategy{
limiters: make(map[string]*idBandwidthLimiter),
connIDGetter: connIDGetter,
speed: speed,
}
}
func (s *ConnectionBandwidthStrategy) getLimiter(ctx context.Context, metadata *adapter.InboundContext) (*rate.Limiter, CloseHandlerFunc, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
id, ok := s.connIDGetter(ctx, metadata)
if !ok {
return nil, nil, E.New("id not found")
}
limiter, ok := s.limiters[id]
if !ok {
limiter = &idBandwidthLimiter{
limiter: createSpeedLimiter(s.speed),
}
s.limiters[id] = limiter
}
limiter.handles++
var once sync.Once
return limiter.limiter, func() {
once.Do(func() {
s.mtx.Lock()
defer s.mtx.Unlock()
limiter.handles--
if limiter.handles == 0 {
delete(s.limiters, id)
}
})
}, nil
}
type UsersBandwidthStrategy struct {
strategies map[string]BandwidthStrategy
mtx sync.Mutex
}
func NewUsersBandwidthStrategy(strategies map[string]BandwidthStrategy) *UsersBandwidthStrategy {
return &UsersBandwidthStrategy{
strategies: strategies,
}
}
func (s *UsersBandwidthStrategy) wrapConn(ctx context.Context, conn net.Conn, metadata *adapter.InboundContext, reverse bool) (net.Conn, error) {
strategy, err := s.getStrategy(ctx, metadata)
if err != nil {
return nil, err
}
return strategy.wrapConn(ctx, conn, metadata, reverse)
}
func (s *UsersBandwidthStrategy) wrapPacketConn(ctx context.Context, conn net.PacketConn, metadata *adapter.InboundContext, reverse bool) (net.PacketConn, error) {
strategy, err := s.getStrategy(ctx, metadata)
if err != nil {
return nil, err
}
return strategy.wrapPacketConn(ctx, conn, metadata, reverse)
}
func (s *UsersBandwidthStrategy) getStrategy(ctx context.Context, metadata *adapter.InboundContext) (BandwidthStrategy, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
var user string
if metadata != nil {
user = metadata.User
}
strategy, ok := s.strategies[user]
if ok {
return strategy, nil
}
return nil, E.New("user strategy not found: ", user)
}
type ManagerBandwidthStrategy struct {
*UsersBandwidthStrategy
}
func NewManagerBandwidthStrategy() *ManagerBandwidthStrategy {
return &ManagerBandwidthStrategy{
UsersBandwidthStrategy: NewUsersBandwidthStrategy(map[string]BandwidthStrategy{}),
}
}
func (s *ManagerBandwidthStrategy) UpdateStrategies(strategies map[string]BandwidthStrategy) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.strategies = strategies
}
func CreateStrategy(strategy string, mode string, connectionType string, speed uint64) (BandwidthStrategy, error) {
var limiterStrategy BandwidthLimiterStrategy
switch strategy {
case "global":
limiterStrategy = NewGlobalBandwidthStrategy(speed)
case "connection":
var connIDGetter ConnIDGetter
switch connectionType {
case "mux":
connIDGetter = func(ctx context.Context, metadata *adapter.InboundContext) (string, bool) {
id, ok := log.MuxIDFromContext(ctx)
if !ok {
return "", ok
}
return strconv.FormatUint(uint64(id.ID), 10), ok
}
case "hwid":
connIDGetter = func(ctx context.Context, metadata *adapter.InboundContext) (string, bool) {
id, ok := ctx.Value("hwid").(string)
return id, ok
}
case "ip":
connIDGetter = func(ctx context.Context, metadata *adapter.InboundContext) (string, bool) {
return metadata.Source.IPAddr().String(), true
}
default:
return nil, E.New("connection type not found: ", connectionType)
}
limiterStrategy = NewConnectionBandwidthStrategy(connIDGetter, speed)
default:
return nil, E.New("strategy not found: ", strategy)
}
var (
connWrapper ConnWrapper
packetConnWrapper PacketConnWrapper
)
switch mode {
case "download":
connWrapper = connWithDownloadBandwidthWrapper
packetConnWrapper = packetConnWithDownloadBandwidthWrapper
case "upload":
connWrapper = connWithUploadBandwidthWrapper
packetConnWrapper = packetConnWithUploadBandwidthWrapper
case "duplex":
connWrapper = connWithDuplexBandwidthWrapper
packetConnWrapper = packetConnWithDuplexBandwidthWrapper
default:
return nil, E.New("mode not found: ", mode)
}
return NewDefaultWrapStrategy(limiterStrategy, connWrapper, packetConnWrapper), nil
}
func createSpeedLimiter(speed uint64) *rate.Limiter {
return rate.NewLimiter(rate.Limit(float64(speed)), 10000)
}
func connWithDownloadBandwidthWrapper(ctx context.Context, conn net.Conn, limiter *rate.Limiter, reverse bool) net.Conn {
if reverse {
return NewConnWithUploadBandwidthLimiter(ctx, conn, limiter)
}
return NewConnWithDownloadBandwidthLimiter(ctx, conn, limiter)
}
func connWithUploadBandwidthWrapper(ctx context.Context, conn net.Conn, limiter *rate.Limiter, reverse bool) net.Conn {
if reverse {
return NewConnWithDownloadBandwidthLimiter(ctx, conn, limiter)
}
return NewConnWithUploadBandwidthLimiter(ctx, conn, limiter)
}
func connWithDuplexBandwidthWrapper(ctx context.Context, conn net.Conn, limiter *rate.Limiter, reverse bool) net.Conn {
return NewConnWithUploadBandwidthLimiter(ctx, NewConnWithDownloadBandwidthLimiter(ctx, conn, limiter), limiter)
}
func packetConnWithDownloadBandwidthWrapper(ctx context.Context, conn net.PacketConn, limiter *rate.Limiter, reverse bool) net.PacketConn {
if reverse {
return NewPacketConnWithUploadBandwidthLimiter(ctx, conn, limiter)
}
return NewPacketConnWithDownloadBandwidthLimiter(ctx, conn, limiter)
}
func packetConnWithUploadBandwidthWrapper(ctx context.Context, conn net.PacketConn, limiter *rate.Limiter, reverse bool) net.PacketConn {
if reverse {
return NewPacketConnWithDownloadBandwidthLimiter(ctx, conn, limiter)
}
return NewPacketConnWithUploadBandwidthLimiter(ctx, conn, limiter)
}
func packetConnWithDuplexBandwidthWrapper(ctx context.Context, conn net.PacketConn, limiter *rate.Limiter, reverse bool) net.PacketConn {
return NewPacketConnWithUploadBandwidthLimiter(ctx, NewPacketConnWithDownloadBandwidthLimiter(ctx, conn, limiter), limiter)
}

View File

@@ -0,0 +1,37 @@
package connection
import (
"context"
"sync"
E "github.com/sagernet/sing/common/exceptions"
)
func NewDefaultLock(max uint32) LockIDGetter {
locks := make(map[string]*uint32)
mtx := sync.Mutex{}
return func(id string) (CloseHandlerFunc, context.Context, error) {
mtx.Lock()
defer mtx.Unlock()
handles, ok := locks[id]
if !ok {
if len(locks) == int(max) {
return nil, nil, E.New("not enough free locks")
}
handles = new(uint32)
locks[id] = handles
}
*handles++
var once sync.Once
return func() {
once.Do(func() {
mtx.Lock()
defer mtx.Unlock()
*handles--
if *handles == 0 {
delete(locks, id)
}
})
}, nil, nil
}
}

View File

@@ -0,0 +1,204 @@
package connection
import (
"context"
"net"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/outbound"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/route"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
func RegisterOutbound(registry *outbound.Registry) {
outbound.Register[option.ConnectionLimiterOutboundOptions](registry, C.TypeConnectionLimiter, NewOutbound)
}
type Outbound struct {
outbound.Adapter
ctx context.Context
outbound adapter.OutboundManager
connection adapter.ConnectionManager
logger logger.ContextLogger
strategy ConnectionStrategy
outboundTag string
detour adapter.Outbound
router *route.Router
}
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ConnectionLimiterOutboundOptions) (adapter.Outbound, error) {
if options.Strategy == "" {
return nil, E.New("missing strategy")
}
if options.Route.Final == "" {
return nil, E.New("missing final outbound")
}
var strategy ConnectionStrategy
var err error
switch options.Strategy {
case "users":
usersStrategies := make(map[string]ConnectionStrategy, len(options.Users))
for _, user := range options.Users {
userStrategy, err := CreateStrategy(user.Strategy, user.ConnectionType, NewDefaultLock(user.Count))
if err != nil {
return nil, err
}
usersStrategies[user.Name] = userStrategy
}
strategy = NewUsersConnectionStrategy(usersStrategies)
case "manager":
strategy = NewManagerConnectionStrategy()
default:
strategy, err = CreateStrategy(options.Strategy, options.ConnectionType, NewDefaultLock(options.Count))
if err != nil {
return nil, err
}
}
logFactory := service.FromContext[log.Factory](ctx)
r := route.NewRouter(ctx, logFactory, options.Route, option.DNSOptions{})
err = r.Initialize(options.Route.Rules, options.Route.RuleSet)
if err != nil {
return nil, err
}
outbound := &Outbound{
Adapter: outbound.NewAdapter(C.TypeConnectionLimiter, tag, nil, []string{}),
ctx: ctx,
outbound: service.FromContext[adapter.OutboundManager](ctx),
connection: service.FromContext[adapter.ConnectionManager](ctx),
logger: logger,
outboundTag: options.Route.Final,
strategy: strategy,
router: r,
}
return outbound, nil
}
func (h *Outbound) Network() []string {
return []string{N.NetworkTCP, N.NetworkUDP}
}
func (h *Outbound) Start() error {
detour, loaded := h.outbound.Outbound(h.outboundTag)
if !loaded {
return E.New("outbound not found: ", h.outboundTag)
}
h.detour = detour
for _, stage := range []adapter.StartStage{adapter.StartStateStart, adapter.StartStatePostStart, adapter.StartStateStarted} {
err := h.router.Start(stage)
if err != nil {
return err
}
}
return nil
}
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
onClose, lockCtx, err := h.strategy.request(ctx, adapter.ContextFrom(ctx))
if err != nil {
return nil, err
}
conn, err := h.detour.DialContext(ctx, network, destination)
if err != nil {
onClose()
return nil, err
}
conn = newConnWithCloseHandlerFunc(conn, onClose)
if lockCtx != nil {
go connChecker(lockCtx, conn.Close)
}
return conn, nil
}
func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
onClose, lockCtx, err := h.strategy.request(ctx, adapter.ContextFrom(ctx))
if err != nil {
return nil, err
}
conn, err := h.detour.ListenPacket(ctx, destination)
if err != nil {
onClose()
return nil, err
}
conn = newPacketConnWithCloseHandlerFunc(conn, onClose)
if lockCtx != nil {
go connChecker(lockCtx, conn.Close)
}
return conn, nil
}
func (h *Outbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
limiterOnClose, lockCtx, err := h.strategy.request(ctx, &metadata)
if err != nil {
h.logger.ErrorContext(ctx, err)
return
}
conn = newConnWithCloseHandlerFunc(conn, limiterOnClose)
if lockCtx != nil {
go connChecker(lockCtx, conn.Close)
}
metadata.Inbound = h.Tag()
metadata.InboundType = h.Type()
h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
return
}
func (h *Outbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
limiterOnClose, lockCtx, err := h.strategy.request(ctx, &metadata)
if err != nil {
h.logger.ErrorContext(ctx, err)
return
}
conn = bufio.NewPacketConn(newPacketConnWithCloseHandlerFunc(bufio.NewNetPacketConn(conn), limiterOnClose))
if lockCtx != nil {
go connChecker(lockCtx, conn.Close)
}
metadata.Inbound = h.Tag()
metadata.InboundType = h.Type()
h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
return
}
func (h *Outbound) GetStrategy() ConnectionStrategy {
return h.strategy
}
type connWithCloseHandlerFunc struct {
net.Conn
onClose CloseHandlerFunc
}
func newConnWithCloseHandlerFunc(conn net.Conn, onClose CloseHandlerFunc) *connWithCloseHandlerFunc {
return &connWithCloseHandlerFunc{conn, onClose}
}
func (conn *connWithCloseHandlerFunc) Close() error {
conn.onClose()
return conn.Conn.Close()
}
type packetConnWithCloseHandlerFunc struct {
net.PacketConn
onClose CloseHandlerFunc
}
func newPacketConnWithCloseHandlerFunc(conn net.PacketConn, onClose CloseHandlerFunc) *packetConnWithCloseHandlerFunc {
return &packetConnWithCloseHandlerFunc{conn, onClose}
}
func (conn *packetConnWithCloseHandlerFunc) Close() error {
conn.onClose()
return conn.PacketConn.Close()
}
func connChecker(ctx context.Context, closeFunc func() error) {
<-ctx.Done()
closeFunc()
}

View File

@@ -0,0 +1,119 @@
package connection
import (
"context"
"strconv"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
)
type (
CloseHandlerFunc = func()
ConnIDGetter = func(context.Context, *adapter.InboundContext) (string, bool)
LockIDGetter = func(string) (CloseHandlerFunc, context.Context, error)
ConnectionStrategy interface {
request(ctx context.Context, metadata *adapter.InboundContext) (onClose CloseHandlerFunc, lockCtx context.Context, err error)
}
)
type DefaultConnectionStrategy struct {
connIDGetter ConnIDGetter
lockIDGetter LockIDGetter
mtx sync.Mutex
}
func NewDefaultConnectionStrategy(connIDGetter ConnIDGetter, lockIDGetter LockIDGetter) *DefaultConnectionStrategy {
outbound := &DefaultConnectionStrategy{
connIDGetter: connIDGetter,
lockIDGetter: lockIDGetter,
}
return outbound
}
func (s *DefaultConnectionStrategy) request(ctx context.Context, metadata *adapter.InboundContext) (CloseHandlerFunc, context.Context, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
id, ok := s.connIDGetter(ctx, metadata)
if !ok {
return nil, nil, E.New("id not found")
}
return s.lockIDGetter(id)
}
type UsersConnectionStrategy struct {
strategies map[string]ConnectionStrategy
mtx sync.Mutex
}
func NewUsersConnectionStrategy(strategies map[string]ConnectionStrategy) *UsersConnectionStrategy {
return &UsersConnectionStrategy{
strategies: strategies,
}
}
func (s *UsersConnectionStrategy) request(ctx context.Context, metadata *adapter.InboundContext) (CloseHandlerFunc, context.Context, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
var user string
if metadata != nil {
user = metadata.User
}
strategy, ok := s.strategies[user]
if ok {
return strategy.request(ctx, metadata)
}
return nil, nil, E.New("user strategy not found: ", user)
}
type ManagerConnectionStrategy struct {
*UsersConnectionStrategy
}
func NewManagerConnectionStrategy() *ManagerConnectionStrategy {
return &ManagerConnectionStrategy{
UsersConnectionStrategy: NewUsersConnectionStrategy(map[string]ConnectionStrategy{}),
}
}
func (s *ManagerConnectionStrategy) UpdateStrategies(strategies map[string]ConnectionStrategy) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.strategies = strategies
}
func CreateStrategy(strategy string, connectionType string, lockIDGetter LockIDGetter) (ConnectionStrategy, error) {
switch strategy {
case "connection":
var connIDGetter ConnIDGetter
switch connectionType {
case "mux":
connIDGetter = func(ctx context.Context, metadata *adapter.InboundContext) (string, bool) {
id, ok := log.MuxIDFromContext(ctx)
if !ok {
return "", ok
}
return strconv.FormatUint(uint64(id.ID), 10), ok
}
case "hwid":
connIDGetter = func(ctx context.Context, metadata *adapter.InboundContext) (string, bool) {
id, ok := ctx.Value("hwid").(string)
return id, ok
}
case "ip":
connIDGetter = func(ctx context.Context, metadata *adapter.InboundContext) (string, bool) {
return metadata.Source.IPAddr().String(), true
}
default:
return nil, E.New("connection type not found: ", connectionType)
}
return NewDefaultConnectionStrategy(connIDGetter, lockIDGetter), nil
default:
return nil, E.New("strategy not found: ", strategy)
}
}

View File

@@ -164,6 +164,14 @@ func (h *Inbound) Close() error {
)
}
func (h *Inbound) UpdateUsers(users []option.TrojanUser) {
h.service.UpdateUsers(common.MapIndexed(users, func(index int, _ option.TrojanUser) int {
return index
}), common.Map(users, func(it option.TrojanUser) string {
return it.Password
}))
}
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
if h.tlsConfig != nil && h.transport == nil {
tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig)

View File

@@ -168,3 +168,13 @@ func (h *Inbound) Close() error {
common.PtrOrNil(h.server),
)
}
func (h *Inbound) UpdateUsers(users []option.TUICUser) {
h.server.UpdateUsers(common.MapIndexed(users, func(index int, _ option.TUICUser) int {
return index
}), common.Map(users, func(it option.TUICUser) [16]byte {
return [16]byte(uuid.Must(uuid.FromString(it.UUID)).Bytes())
}), common.Map(users, func(it option.TUICUser) string {
return it.Password
}))
}

View File

@@ -3,13 +3,13 @@ package tunnel
import (
"context"
"net"
"os"
"time"
"github.com/gofrs/uuid/v5"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/outbound"
sbUot "github.com/sagernet/sing-box/common/uot"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
@@ -18,6 +18,7 @@ import (
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/uot"
"github.com/sagernet/sing/service"
)
@@ -27,12 +28,13 @@ func RegisterClientEndpoint(registry *endpoint.Registry) {
type ClientEndpoint struct {
outbound.Adapter
ctx context.Context
outbound adapter.Outbound
router adapter.ConnectionRouterEx
logger logger.ContextLogger
uuid uuid.UUID
key uuid.UUID
ctx context.Context
outbound adapter.Outbound
router adapter.ConnectionRouterEx
logger logger.ContextLogger
uuid uuid.UUID
key uuid.UUID
uotClient *uot.Client
}
func NewClientEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunnelClientEndpointOptions) (adapter.Endpoint, error) {
@@ -45,9 +47,9 @@ func NewClientEndpoint(ctx context.Context, router adapter.Router, logger log.Co
return nil, err
}
client := &ClientEndpoint{
Adapter: outbound.NewAdapter(C.TypeTunnelClient, tag, []string{N.NetworkTCP}, []string{}),
Adapter: outbound.NewAdapter(C.TypeTunnelClient, tag, []string{N.NetworkTCP, N.NetworkUDP}, []string{}),
ctx: ctx,
router: router,
router: sbUot.NewRouter(router, logger),
logger: logger,
uuid: clientUUID,
key: clientKey,
@@ -58,6 +60,10 @@ func NewClientEndpoint(ctx context.Context, router adapter.Router, logger log.Co
return nil, err
}
client.outbound = outbound
client.uotClient = &uot.Client{
Dialer: outbound,
Version: uot.Version,
}
return client, nil
}
@@ -85,8 +91,8 @@ func (c *ClientEndpoint) Start(stage adapter.StartStage) error {
}
func (c *ClientEndpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if network != N.NetworkTCP {
return nil, os.ErrInvalid
if N.NetworkName(network) == N.NetworkUDP {
return c.uotClient.DialContext(ctx, network, destination)
}
var destinationUUID *uuid.UUID
if metadata := adapter.ContextFrom(ctx); metadata != nil {
@@ -109,11 +115,14 @@ func (c *ClientEndpoint) DialContext(ctx context.Context, network string, destin
return nil, err
}
err = WriteRequest(conn, &Request{UUID: c.key, Command: CommandTCP, DestinationUUID: *destinationUUID, Destination: destination})
return conn, err
if err != nil {
return nil, err
}
return conn, nil
}
func (c *ClientEndpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return nil, os.ErrInvalid
return c.uotClient.ListenPacket(ctx, destination)
}
func (c *ClientEndpoint) Close() error {
@@ -139,6 +148,7 @@ func (c *ClientEndpoint) startInboundConn() error {
func (c *ClientEndpoint) connHandler(conn net.Conn, request *Request) {
metadata := adapter.InboundContext{
Inbound: c.Tag(),
Source: M.ParseSocksaddr(conn.RemoteAddr().String()),
Destination: request.Destination,
}

View File

@@ -3,14 +3,13 @@ package tunnel
import (
"context"
"net"
"os"
"sync"
"time"
"github.com/gofrs/uuid/v5"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/outbound"
sbUot "github.com/sagernet/sing-box/common/uot"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
@@ -19,6 +18,7 @@ import (
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/uot"
"github.com/sagernet/sing/service"
)
@@ -28,16 +28,15 @@ func RegisterServerEndpoint(registry *endpoint.Registry) {
type ServerEndpoint struct {
outbound.Adapter
logger logger.ContextLogger
inbound adapter.Inbound
router adapter.Router
uuid uuid.UUID
users map[uuid.UUID]uuid.UUID
keys map[uuid.UUID]uuid.UUID
conns map[uuid.UUID]chan net.Conn
timeout time.Duration
mtx sync.Mutex
logger logger.ContextLogger
inbound adapter.Inbound
router adapter.ConnectionRouterEx
uuid uuid.UUID
users map[uuid.UUID]uuid.UUID
keys map[uuid.UUID]uuid.UUID
conns map[uuid.UUID]chan net.Conn
timeout time.Duration
uotClient *uot.Client
}
func NewServerEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunnelServerEndpointOptions) (adapter.Endpoint, error) {
@@ -46,9 +45,9 @@ func NewServerEndpoint(ctx context.Context, router adapter.Router, logger log.Co
return nil, err
}
server := &ServerEndpoint{
Adapter: outbound.NewAdapter(C.TypeTunnelServer, tag, []string{N.NetworkTCP}, []string{}),
Adapter: outbound.NewAdapter(C.TypeTunnelServer, tag, []string{N.NetworkTCP, N.NetworkUDP}, []string{}),
logger: logger,
router: router,
router: sbUot.NewRouter(router, logger),
uuid: serverUUID,
}
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
@@ -78,6 +77,10 @@ func NewServerEndpoint(ctx context.Context, router adapter.Router, logger log.Co
} else {
server.timeout = C.TCPConnectTimeout
}
server.uotClient = &uot.Client{
Dialer: server,
Version: uot.Version,
}
return server, nil
}
@@ -86,8 +89,8 @@ func (s *ServerEndpoint) Start(stage adapter.StartStage) error {
}
func (s *ServerEndpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if network != N.NetworkTCP {
return nil, os.ErrInvalid
if N.NetworkName(network) == N.NetworkUDP {
return s.uotClient.DialContext(ctx, network, destination)
}
var sourceUUID *uuid.UUID
var ch chan net.Conn
@@ -97,13 +100,11 @@ func (s *ServerEndpoint) DialContext(ctx context.Context, network string, destin
if err != nil {
return nil, err
}
s.mtx.Lock()
var ok bool
ch, ok = s.conns[tunnelDestination]
if !ok {
return nil, E.New("user ", metadata.TunnelDestination, " not found")
}
s.mtx.Unlock()
}
if metadata.TunnelSource != "" {
tunnelSource, err := uuid.FromString(metadata.TunnelSource)
@@ -131,6 +132,7 @@ func (s *ServerEndpoint) DialContext(ctx context.Context, network string, destin
case conn := <-ch:
err := WriteRequest(conn, &Request{UUID: *sourceUUID, Command: CommandTCP, Destination: destination})
if err != nil {
conn.Close()
s.logger.ErrorContext(ctx, err)
continue
}
@@ -142,7 +144,7 @@ func (s *ServerEndpoint) DialContext(ctx context.Context, network string, destin
}
func (s *ServerEndpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return nil, os.ErrInvalid
return s.uotClient.ListenPacket(ctx, destination)
}
func (s *ServerEndpoint) Close() error {
@@ -159,8 +161,6 @@ func (s *ServerEndpoint) connHandler(ctx context.Context, conn net.Conn, metadat
return err
}
if request.Command == CommandInbound {
s.mtx.Lock()
defer s.mtx.Unlock()
uuid, ok := s.users[request.UUID]
if !ok {
return E.New("key ", request.UUID.String(), " not found")
@@ -183,14 +183,12 @@ func (s *ServerEndpoint) connHandler(ctx context.Context, conn net.Conn, metadat
if sourceUUID == request.DestinationUUID {
return E.New("routing loop on ", sourceUUID)
}
s.mtx.Lock()
if request.DestinationUUID != s.uuid {
_, ok = s.keys[request.DestinationUUID]
if !ok {
return E.New("user ", sourceUUID, " not found")
return E.New("user ", request.DestinationUUID, " not found")
}
}
s.mtx.Unlock()
metadata.Inbound = s.Tag()
metadata.InboundType = C.TypeTunnelServer
metadata.Destination = request.Destination

View File

@@ -0,0 +1,214 @@
package encryption
import (
"crypto/cipher"
"crypto/ecdh"
"crypto/mlkem"
"crypto/rand"
"io"
"net"
"sync"
"time"
"github.com/sagernet/sing-box/common/xray/cpuid"
E "github.com/sagernet/sing/common/exceptions"
"lukechampine.com/blake3"
)
type ClientInstance struct {
NfsPKeys []any
NfsPKeysBytes [][]byte
Hash32s [][32]byte
RelaysLength int
XorMode uint32
Seconds uint32
PaddingLens [][3]int
PaddingGaps [][3]int
RWLock sync.RWMutex
Expire time.Time
PfsKey []byte
Ticket []byte
}
func (i *ClientInstance) Init(nfsPKeysBytes [][]byte, xorMode, seconds uint32, padding string) (err error) {
if i.NfsPKeys != nil {
return E.New("already initialized")
}
l := len(nfsPKeysBytes)
if l == 0 {
return E.New("empty nfsPKeysBytes")
}
i.NfsPKeys = make([]any, l)
i.NfsPKeysBytes = nfsPKeysBytes
i.Hash32s = make([][32]byte, l)
for j, k := range nfsPKeysBytes {
if len(k) == 32 {
if i.NfsPKeys[j], err = ecdh.X25519().NewPublicKey(k); err != nil {
return
}
i.RelaysLength += 32 + 32
} else {
if i.NfsPKeys[j], err = mlkem.NewEncapsulationKey768(k); err != nil {
return
}
i.RelaysLength += 1088 + 32
}
i.Hash32s[j] = blake3.Sum256(k)
}
i.RelaysLength -= 32
i.XorMode = xorMode
i.Seconds = seconds
return ParsePadding(padding, &i.PaddingLens, &i.PaddingGaps)
}
func (i *ClientInstance) IsFullRandomXorMode() bool {
return i.XorMode == 2
}
func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) {
if i.NfsPKeys == nil {
return nil, E.New("uninitialized")
}
c := NewCommonConn(conn, cpuid.HasAESGCM)
ivAndRealysLength := 16 + i.RelaysLength
pfsKeyExchangeLength := 18 + 1184 + 32 + 16
paddingLength, paddingLens, paddingGaps := CreatePadding(i.PaddingLens, i.PaddingGaps)
clientHello := make([]byte, ivAndRealysLength+pfsKeyExchangeLength+paddingLength)
iv := clientHello[:16]
rand.Read(iv)
relays := clientHello[16:ivAndRealysLength]
var nfsKey []byte
var lastCTR cipher.Stream
for j, k := range i.NfsPKeys {
var index = 32
if k, ok := k.(*ecdh.PublicKey); ok {
privateKey, _ := ecdh.X25519().GenerateKey(rand.Reader)
copy(relays, privateKey.PublicKey().Bytes())
var err error
nfsKey, err = privateKey.ECDH(k)
if err != nil {
return nil, err
}
}
if k, ok := k.(*mlkem.EncapsulationKey768); ok {
var ciphertext []byte
nfsKey, ciphertext = k.Encapsulate()
copy(relays, ciphertext)
index = 1088
}
if i.XorMode > 0 { // this xor can (others can't) be recovered by client's config, revealing an X25519 public key / ML-KEM-768 ciphertext, that's why "native" values
NewCTR(i.NfsPKeysBytes[j], iv).XORKeyStream(relays, relays[:index]) // make X25519 public key / ML-KEM-768 ciphertext distinguishable from random bytes
}
if lastCTR != nil {
lastCTR.XORKeyStream(relays, relays[:32]) // make this relay irreplaceable
}
if j == len(i.NfsPKeys)-1 {
break
}
lastCTR = NewCTR(nfsKey, iv)
lastCTR.XORKeyStream(relays[index:], i.Hash32s[j+1][:])
relays = relays[index+32:]
}
nfsAEAD := NewAEAD(iv, nfsKey, c.UseAES)
if i.Seconds > 0 {
i.RWLock.RLock()
if time.Now().Before(i.Expire) {
c.Client = i
c.UnitedKey = append(i.PfsKey, nfsKey...) // different unitedKey for each connection
nfsAEAD.Seal(clientHello[:ivAndRealysLength], nil, EncodeLength(32), nil)
nfsAEAD.Seal(clientHello[:ivAndRealysLength+18], nil, i.Ticket, nil)
i.RWLock.RUnlock()
c.PreWrite = clientHello[:ivAndRealysLength+18+32]
c.AEAD = NewAEAD(clientHello[ivAndRealysLength+18:ivAndRealysLength+18+32], c.UnitedKey, c.UseAES)
if i.XorMode == 2 {
c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, iv), nil, len(c.PreWrite), 16)
}
return c, nil
}
i.RWLock.RUnlock()
}
pfsKeyExchange := clientHello[ivAndRealysLength : ivAndRealysLength+pfsKeyExchangeLength]
nfsAEAD.Seal(pfsKeyExchange[:0], nil, EncodeLength(pfsKeyExchangeLength-18), nil)
mlkem768DKey, _ := mlkem.GenerateKey768()
x25519SKey, _ := ecdh.X25519().GenerateKey(rand.Reader)
pfsPublicKey := append(mlkem768DKey.EncapsulationKey().Bytes(), x25519SKey.PublicKey().Bytes()...)
nfsAEAD.Seal(pfsKeyExchange[:18], nil, pfsPublicKey, nil)
padding := clientHello[ivAndRealysLength+pfsKeyExchangeLength:]
nfsAEAD.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil)
nfsAEAD.Seal(padding[:18], nil, padding[18:paddingLength-16], nil)
paddingLens[0] = ivAndRealysLength + pfsKeyExchangeLength + paddingLens[0]
for i, l := range paddingLens { // sends padding in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control
if l > 0 {
if _, err := conn.Write(clientHello[:l]); err != nil {
return nil, err
}
clientHello = clientHello[l:]
}
if len(paddingGaps) > i {
time.Sleep(paddingGaps[i])
}
}
encryptedPfsPublicKey := make([]byte, 1088+32+16)
if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil {
return nil, err
}
nfsAEAD.Open(encryptedPfsPublicKey[:0], MaxNonce, encryptedPfsPublicKey, nil)
mlkem768Key, err := mlkem768DKey.Decapsulate(encryptedPfsPublicKey[:1088])
if err != nil {
return nil, err
}
peerX25519PKey, err := ecdh.X25519().NewPublicKey(encryptedPfsPublicKey[1088 : 1088+32])
if err != nil {
return nil, err
}
x25519Key, err := x25519SKey.ECDH(peerX25519PKey)
if err != nil {
return nil, err
}
pfsKey := make([]byte, 32+32) // no more capacity
copy(pfsKey, mlkem768Key)
copy(pfsKey[32:], x25519Key)
c.UnitedKey = append(pfsKey, nfsKey...)
c.AEAD = NewAEAD(pfsPublicKey, c.UnitedKey, c.UseAES)
c.PeerAEAD = NewAEAD(encryptedPfsPublicKey[:1088+32], c.UnitedKey, c.UseAES)
encryptedTicket := make([]byte, 32)
if _, err := io.ReadFull(conn, encryptedTicket); err != nil {
return nil, err
}
if _, err := c.PeerAEAD.Open(encryptedTicket[:0], nil, encryptedTicket, nil); err != nil {
return nil, err
}
seconds := DecodeLength(encryptedTicket)
if i.Seconds > 0 && seconds > 0 {
i.RWLock.Lock()
i.Expire = time.Now().Add(time.Duration(seconds) * time.Second)
i.PfsKey = pfsKey
i.Ticket = encryptedTicket[:16]
i.RWLock.Unlock()
}
encryptedLength := make([]byte, 18)
if _, err := io.ReadFull(conn, encryptedLength); err != nil {
return nil, err
}
if _, err := c.PeerAEAD.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil {
return nil, err
}
length := DecodeLength(encryptedLength[:2])
c.PeerPadding = make([]byte, length) // important: allows server sends padding slowly, eliminating 1-RTT's traffic pattern
if i.XorMode == 2 {
c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, iv), NewCTR(c.UnitedKey, encryptedTicket[:16]), 0, length)
}
return c, nil
}

View File

@@ -0,0 +1,297 @@
package encryption
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"errors"
"fmt"
"io"
"net"
"strconv"
"strings"
"sync"
"time"
"github.com/sagernet/sing-box/common/xray/crypto"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/crypto/chacha20poly1305"
"lukechampine.com/blake3"
)
var OutBytesPool = sync.Pool{
New: func() any {
return make([]byte, 5+8192+16)
},
}
type EncryptionConn interface {
net.Conn
IsEncryptionLayer() bool
}
type CommonConn struct {
net.Conn
UseAES bool
Client *ClientInstance
UnitedKey []byte
PreWrite []byte
AEAD *AEAD
PeerAEAD *AEAD
PeerPadding []byte
rawInput bytes.Buffer
input bytes.Reader
}
func NewCommonConn(conn net.Conn, useAES bool) *CommonConn {
return &CommonConn{
Conn: conn,
UseAES: useAES,
}
}
func (c *CommonConn) Write(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
outBytes := OutBytesPool.Get().([]byte)
defer OutBytesPool.Put(outBytes)
for n := 0; n < len(b); {
b := b[n:]
if len(b) > 8192 {
b = b[:8192] // for avoiding another copy() in peer's Read()
}
n += len(b)
headerAndData := outBytes[:5+len(b)+16]
EncodeHeader(headerAndData, len(b)+16)
max := false
if bytes.Equal(c.AEAD.Nonce[:], MaxNonce) {
max = true
}
c.AEAD.Seal(headerAndData[:5], nil, b, headerAndData[:5])
if max {
c.AEAD = NewAEAD(headerAndData, c.UnitedKey, c.UseAES)
}
if c.PreWrite != nil {
headerAndData = append(c.PreWrite, headerAndData...)
c.PreWrite = nil
}
if _, err := c.Conn.Write(headerAndData); err != nil {
return 0, err
}
}
return len(b), nil
}
func (c *CommonConn) Read(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
if c.PeerAEAD == nil { // client's 0-RTT
serverRandom := make([]byte, 16)
if _, err := io.ReadFull(c.Conn, serverRandom); err != nil {
return 0, err
}
c.PeerAEAD = NewAEAD(serverRandom, c.UnitedKey, c.UseAES)
if xorConn, ok := c.Conn.(*XorConn); ok {
xorConn.PeerCTR = NewCTR(c.UnitedKey, serverRandom)
}
}
if c.PeerPadding != nil { // client's 1-RTT
if _, err := io.ReadFull(c.Conn, c.PeerPadding); err != nil {
return 0, err
}
if _, err := c.PeerAEAD.Open(c.PeerPadding[:0], nil, c.PeerPadding, nil); err != nil {
return 0, err
}
c.PeerPadding = nil
}
if c.input.Len() > 0 {
return c.input.Read(b)
}
peerHeader := [5]byte{}
if _, err := io.ReadFull(c.Conn, peerHeader[:]); err != nil {
return 0, err
}
l, err := DecodeHeader(peerHeader[:]) // l: 17~17000
if err != nil {
if c.Client != nil && errors.Is(err, ErrInvalidHeader) { // client's 0-RTT
c.Client.RWLock.Lock()
if bytes.HasPrefix(c.UnitedKey, c.Client.PfsKey) {
c.Client.Expire = time.Now() // expired
}
c.Client.RWLock.Unlock()
return 0, E.New("new handshake needed")
}
return 0, err
}
c.Client = nil
if c.rawInput.Cap() < l {
c.rawInput.Grow(l) // no need to use sync.Pool, because we are always reading
}
peerData := c.rawInput.Bytes()[:l]
if _, err := io.ReadFull(c.Conn, peerData); err != nil {
return 0, err
}
dst := peerData[:l-16]
if len(dst) <= len(b) {
dst = b[:len(dst)] // avoids another copy()
}
var newAEAD *AEAD
if bytes.Equal(c.PeerAEAD.Nonce[:], MaxNonce) {
newAEAD = NewAEAD(append(peerHeader[:], peerData...), c.UnitedKey, c.UseAES)
}
_, err = c.PeerAEAD.Open(dst[:0], nil, peerData, peerHeader[:])
if newAEAD != nil {
c.PeerAEAD = newAEAD
}
if err != nil {
return 0, err
}
if len(dst) > len(b) {
c.input.Reset(dst[copy(b, dst):])
dst = b // for len(dst)
}
return len(dst), nil
}
// Upstream returns the underlying connection, allowing Vision to unwrap and access the TLS connection
func (c *CommonConn) Upstream() any {
return c.Conn
}
func (c *CommonConn) IsEncryptionLayer() bool {
return true
}
type AEAD struct {
cipher.AEAD
Nonce [12]byte
}
func NewAEAD(ctx, key []byte, useAES bool) *AEAD {
k := make([]byte, 32)
blake3.DeriveKey(k, string(ctx), key)
var aead cipher.AEAD
if useAES {
block, _ := aes.NewCipher(k)
aead, _ = cipher.NewGCM(block)
} else {
aead, _ = chacha20poly1305.New(k)
}
return &AEAD{AEAD: aead}
}
func (a *AEAD) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
if nonce == nil {
nonce = IncreaseNonce(a.Nonce[:])
}
return a.AEAD.Seal(dst, nonce, plaintext, additionalData)
}
func (a *AEAD) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
if nonce == nil {
nonce = IncreaseNonce(a.Nonce[:])
}
return a.AEAD.Open(dst, nonce, ciphertext, additionalData)
}
func IncreaseNonce(nonce []byte) []byte {
for i := range 12 {
nonce[11-i]++
if nonce[11-i] != 0 {
break
}
}
return nonce
}
var MaxNonce = bytes.Repeat([]byte{255}, 12)
func EncodeLength(l int) []byte {
return []byte{byte(l >> 8), byte(l)}
}
func DecodeLength(b []byte) int {
return int(b[0])<<8 | int(b[1])
}
func EncodeHeader(h []byte, l int) {
h[0] = 23
h[1] = 3
h[2] = 3
h[3] = byte(l >> 8)
h[4] = byte(l)
}
var ErrInvalidHeader = errors.New("invalid header")
func DecodeHeader(h []byte) (l int, err error) {
l = int(h[3])<<8 | int(h[4])
if h[0] != 23 || h[1] != 3 || h[2] != 3 {
l = 0
}
if l < 17 || l > 17000 { // TODO: TLSv1.3 max length
err = fmt.Errorf("%w: %v", ErrInvalidHeader, h[:5]) // DO NOT CHANGE: relied by client's Read()
}
return
}
func ParsePadding(padding string, paddingLens, paddingGaps *[][3]int) (err error) {
if padding == "" {
return
}
maxLen := 0
for i, s := range strings.Split(padding, ".") {
x := strings.Split(s, "-")
if len(x) < 3 || x[0] == "" || x[1] == "" || x[2] == "" {
return E.New("invalid padding lenth/gap parameter: " + s)
}
y := [3]int{}
if y[0], err = strconv.Atoi(x[0]); err != nil {
return
}
if y[1], err = strconv.Atoi(x[1]); err != nil {
return
}
if y[2], err = strconv.Atoi(x[2]); err != nil {
return
}
if i == 0 && (y[0] < 100 || y[1] < 18+17 || y[2] < 18+17) {
return E.New("first padding length must not be smaller than 35")
}
if i%2 == 0 {
*paddingLens = append(*paddingLens, y)
maxLen += max(y[1], y[2])
} else {
*paddingGaps = append(*paddingGaps, y)
}
}
if maxLen > 18+65535 {
return E.New("total padding length must not be larger than 65553")
}
return
}
func CreatePadding(paddingLens, paddingGaps [][3]int) (length int, lens []int, gaps []time.Duration) {
if len(paddingLens) == 0 {
paddingLens = [][3]int{{100, 111, 1111}, {50, 0, 3333}}
paddingGaps = [][3]int{{75, 0, 111}}
}
for _, y := range paddingLens {
l := 0
if y[0] >= int(crypto.RandBetween(0, 100)) {
l = int(crypto.RandBetween(int64(y[1]), int64(y[2])))
}
lens = append(lens, l)
length += l
}
for _, y := range paddingGaps {
g := 0
if y[0] >= int(crypto.RandBetween(0, 100)) {
g = int(crypto.RandBetween(int64(y[1]), int64(y[2])))
}
gaps = append(gaps, time.Duration(g)*time.Millisecond)
}
return
}

View File

@@ -0,0 +1,336 @@
package encryption
import (
"bytes"
"crypto/cipher"
"crypto/ecdh"
"crypto/mlkem"
"crypto/rand"
"fmt"
"io"
"net"
"sync"
"time"
"github.com/sagernet/sing-box/common/xray/crypto"
E "github.com/sagernet/sing/common/exceptions"
"lukechampine.com/blake3"
)
type ServerSession struct {
PfsKey []byte
NfsKeys sync.Map
}
type ServerInstance struct {
NfsSKeys []any
NfsPKeysBytes [][]byte
Hash32s [][32]byte
RelaysLength int
XorMode uint32
SecondsFrom int64
SecondsTo int64
PaddingLens [][3]int
PaddingGaps [][3]int
RWLock sync.RWMutex
Closed bool
Lasts map[int64][16]byte
Tickets [][16]byte
Sessions map[[16]byte]*ServerSession
}
func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode uint32, secondsFrom, secondsTo int64, padding string) (err error) {
if i.NfsSKeys != nil {
return E.New("already initialized")
}
l := len(nfsSKeysBytes)
if l == 0 {
return E.New("empty nfsSKeysBytes")
}
i.NfsSKeys = make([]any, l)
i.NfsPKeysBytes = make([][]byte, l)
i.Hash32s = make([][32]byte, l)
for j, k := range nfsSKeysBytes {
if len(k) == 32 {
if i.NfsSKeys[j], err = ecdh.X25519().NewPrivateKey(k); err != nil {
return
}
i.NfsPKeysBytes[j] = i.NfsSKeys[j].(*ecdh.PrivateKey).PublicKey().Bytes()
i.RelaysLength += 32 + 32
} else {
if i.NfsSKeys[j], err = mlkem.NewDecapsulationKey768(k); err != nil {
return
}
i.NfsPKeysBytes[j] = i.NfsSKeys[j].(*mlkem.DecapsulationKey768).EncapsulationKey().Bytes()
i.RelaysLength += 1088 + 32
}
i.Hash32s[j] = blake3.Sum256(i.NfsPKeysBytes[j])
}
i.RelaysLength -= 32
i.XorMode = xorMode
i.SecondsFrom = secondsFrom
i.SecondsTo = secondsTo
err = ParsePadding(padding, &i.PaddingLens, &i.PaddingGaps)
if err != nil {
return
}
if i.SecondsFrom > 0 || i.SecondsTo > 0 {
i.Lasts = make(map[int64][16]byte)
i.Tickets = make([][16]byte, 0, 1024)
i.Sessions = make(map[[16]byte]*ServerSession)
go func() {
for {
time.Sleep(time.Minute)
i.RWLock.Lock()
if i.Closed {
i.RWLock.Unlock()
return
}
minute := time.Now().Unix() / 60
last := i.Lasts[minute]
delete(i.Lasts, minute)
delete(i.Lasts, minute-1) // for insurance
if last != [16]byte{} {
for j, ticket := range i.Tickets {
delete(i.Sessions, ticket)
if ticket == last {
i.Tickets = i.Tickets[j+1:]
break
}
}
}
i.RWLock.Unlock()
}
}()
}
return
}
func (i *ServerInstance) Close() (err error) {
i.RWLock.Lock()
i.Closed = true
i.RWLock.Unlock()
return
}
func (i *ServerInstance) IsXorMode() bool {
return i.XorMode > 0
}
func (i *ServerInstance) IsFullRandomXorMode() bool {
return i.XorMode == 2
}
func (i *ServerInstance) Handshake(conn net.Conn, fallback *[]byte) (*CommonConn, error) {
if i.NfsSKeys == nil {
return nil, E.New("uninitialized")
}
c := NewCommonConn(conn, true)
ivAndRelays := make([]byte, 16+i.RelaysLength)
if _, err := io.ReadFull(conn, ivAndRelays); err != nil {
return nil, err
}
if fallback != nil {
*fallback = append(*fallback, ivAndRelays...)
}
iv := ivAndRelays[:16]
relays := ivAndRelays[16:]
var nfsKey []byte
var lastCTR cipher.Stream
for j, k := range i.NfsSKeys {
if lastCTR != nil {
lastCTR.XORKeyStream(relays, relays[:32]) // recover this relay
}
var index = 32
if _, ok := k.(*mlkem.DecapsulationKey768); ok {
index = 1088
}
if i.XorMode > 0 {
NewCTR(i.NfsPKeysBytes[j], iv).XORKeyStream(relays, relays[:index]) // we don't use buggy elligator2, because we have PSK :)
}
if k, ok := k.(*ecdh.PrivateKey); ok {
publicKey, err := ecdh.X25519().NewPublicKey(relays[:index])
if err != nil {
return nil, err
}
if publicKey.Bytes()[31] > 127 { // we just don't want the observer can change even one bit without breaking the connection, though it has nothing to do with security
return nil, E.New("the highest bit of the last byte of the peer-sent X25519 public key is not 0")
}
nfsKey, err = k.ECDH(publicKey)
if err != nil {
return nil, err
}
}
if k, ok := k.(*mlkem.DecapsulationKey768); ok {
var err error
nfsKey, err = k.Decapsulate(relays[:index])
if err != nil {
return nil, err
}
}
if j == len(i.NfsSKeys)-1 {
break
}
relays = relays[index:]
lastCTR = NewCTR(nfsKey, iv)
lastCTR.XORKeyStream(relays, relays[:32])
if !bytes.Equal(relays[:32], i.Hash32s[j+1][:]) {
return nil, E.New("unexpected hash32: " + fmt.Sprintf("%v", relays[:32]))
}
relays = relays[32:]
}
nfsAEAD := NewAEAD(iv, nfsKey, c.UseAES)
encryptedLength := make([]byte, 18)
if _, err := io.ReadFull(conn, encryptedLength); err != nil {
return nil, err
}
if fallback != nil {
*fallback = append(*fallback, encryptedLength...)
}
decryptedLength := make([]byte, 2)
if _, err := nfsAEAD.Open(decryptedLength[:0], nil, encryptedLength, nil); err != nil {
c.UseAES = !c.UseAES
nfsAEAD = NewAEAD(iv, nfsKey, c.UseAES)
if _, err := nfsAEAD.Open(decryptedLength[:0], nil, encryptedLength, nil); err != nil {
return nil, err
}
}
if fallback != nil {
*fallback = nil
}
length := DecodeLength(decryptedLength)
if length == 32 {
if i.SecondsFrom == 0 && i.SecondsTo == 0 {
return nil, E.New("0-RTT is not allowed")
}
encryptedTicket := make([]byte, 32)
if _, err := io.ReadFull(conn, encryptedTicket); err != nil {
return nil, err
}
ticket, err := nfsAEAD.Open(nil, nil, encryptedTicket, nil)
if err != nil {
return nil, err
}
i.RWLock.RLock()
s := i.Sessions[[16]byte(ticket)]
i.RWLock.RUnlock()
if s == nil {
noises := make([]byte, crypto.RandBetween(1279, 2279)) // matches 1-RTT's server hello length for "random", though it is not important, just for example
var err error
for err == nil {
rand.Read(noises)
_, err = DecodeHeader(noises)
}
conn.Write(noises) // make client do new handshake
return nil, E.New("expired ticket")
}
if _, loaded := s.NfsKeys.LoadOrStore([32]byte(nfsKey), true); loaded { // prevents bad client also
return nil, E.New("replay detected")
}
c.UnitedKey = append(s.PfsKey, nfsKey...) // the same nfsKey links the upload & download (prevents server -> client's another request)
c.PreWrite = make([]byte, 16)
rand.Read(c.PreWrite) // always trust yourself, not the client (also prevents being parsed as TLS thus causing false interruption for "native" and "xorpub")
c.AEAD = NewAEAD(c.PreWrite, c.UnitedKey, c.UseAES)
c.PeerAEAD = NewAEAD(encryptedTicket, c.UnitedKey, c.UseAES) // unchangeable ctx (prevents server -> server), and different ctx length for upload / download (prevents client -> client)
if i.XorMode == 2 {
c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, c.PreWrite), NewCTR(c.UnitedKey, iv), 16, 0) // it doesn't matter if the attacker sends client's iv back to the client
}
return c, nil
}
if length < 1184+32+16 { // client may send more public keys in the future's version
return nil, E.New("too short length")
}
encryptedPfsPublicKey := make([]byte, length)
if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil {
return nil, err
}
if _, err := nfsAEAD.Open(encryptedPfsPublicKey[:0], nil, encryptedPfsPublicKey, nil); err != nil {
return nil, err
}
mlkem768EKey, err := mlkem.NewEncapsulationKey768(encryptedPfsPublicKey[:1184])
if err != nil {
return nil, err
}
mlkem768Key, encapsulatedPfsKey := mlkem768EKey.Encapsulate()
peerX25519PKey, err := ecdh.X25519().NewPublicKey(encryptedPfsPublicKey[1184 : 1184+32])
if err != nil {
return nil, err
}
x25519SKey, _ := ecdh.X25519().GenerateKey(rand.Reader)
x25519Key, err := x25519SKey.ECDH(peerX25519PKey)
if err != nil {
return nil, err
}
pfsKey := make([]byte, 32+32) // no more capacity
copy(pfsKey, mlkem768Key)
copy(pfsKey[32:], x25519Key)
pfsPublicKey := append(encapsulatedPfsKey, x25519SKey.PublicKey().Bytes()...)
c.UnitedKey = append(pfsKey, nfsKey...)
c.AEAD = NewAEAD(pfsPublicKey, c.UnitedKey, c.UseAES)
c.PeerAEAD = NewAEAD(encryptedPfsPublicKey[:1184+32], c.UnitedKey, c.UseAES)
ticket := [16]byte{}
rand.Read(ticket[:])
var seconds int64
if i.SecondsTo == 0 {
seconds = i.SecondsFrom * crypto.RandBetween(50, 100) / 100
} else {
seconds = crypto.RandBetween(i.SecondsFrom, i.SecondsTo)
}
copy(ticket[:], EncodeLength(int(seconds)))
if seconds > 0 {
i.RWLock.Lock()
i.Lasts[(time.Now().Unix()+max(i.SecondsFrom, i.SecondsTo))/60+2] = ticket
i.Tickets = append(i.Tickets, ticket)
i.Sessions[ticket] = &ServerSession{PfsKey: pfsKey}
i.RWLock.Unlock()
}
pfsKeyExchangeLength := 1088 + 32 + 16
encryptedTicketLength := 32
paddingLength, paddingLens, paddingGaps := CreatePadding(i.PaddingLens, i.PaddingGaps)
serverHello := make([]byte, pfsKeyExchangeLength+encryptedTicketLength+paddingLength)
nfsAEAD.Seal(serverHello[:0], MaxNonce, pfsPublicKey, nil)
c.AEAD.Seal(serverHello[:pfsKeyExchangeLength], nil, ticket[:], nil)
padding := serverHello[pfsKeyExchangeLength+encryptedTicketLength:]
c.AEAD.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil)
c.AEAD.Seal(padding[:18], nil, padding[18:paddingLength-16], nil)
paddingLens[0] = pfsKeyExchangeLength + encryptedTicketLength + paddingLens[0]
for i, l := range paddingLens { // sends padding in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control
if l > 0 {
if _, err := conn.Write(serverHello[:l]); err != nil {
return nil, err
}
serverHello = serverHello[l:]
}
if len(paddingGaps) > i {
time.Sleep(paddingGaps[i])
}
}
// important: allows client sends padding slowly, eliminating 1-RTT's traffic pattern
if _, err := io.ReadFull(conn, encryptedLength); err != nil {
return nil, err
}
if _, err := nfsAEAD.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil {
return nil, err
}
encryptedPadding := make([]byte, DecodeLength(encryptedLength[:2]))
if _, err := io.ReadFull(conn, encryptedPadding); err != nil {
return nil, err
}
if _, err := nfsAEAD.Open(encryptedPadding[:0], nil, encryptedPadding, nil); err != nil {
return nil, err
}
if i.XorMode == 2 {
c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, ticket[:]), NewCTR(c.UnitedKey, iv), 0, 0)
}
return c, nil
}

View File

@@ -0,0 +1,101 @@
package encryption
import (
"crypto/aes"
"crypto/cipher"
"net"
"lukechampine.com/blake3"
)
func NewCTR(key, iv []byte) cipher.Stream {
k := make([]byte, 32)
blake3.DeriveKey(k, "VLESS", key) // avoids using key directly
block, _ := aes.NewCipher(k)
return cipher.NewCTR(block, iv)
}
type XorConn struct {
net.Conn
CTR cipher.Stream
PeerCTR cipher.Stream
OutSkip int
OutHeader []byte
InSkip int
InHeader []byte
}
func NewXorConn(conn net.Conn, ctr, peerCTR cipher.Stream, outSkip, inSkip int) *XorConn {
return &XorConn{
Conn: conn,
CTR: ctr,
PeerCTR: peerCTR,
OutSkip: outSkip,
OutHeader: make([]byte, 0, 5), // important
InSkip: inSkip,
InHeader: make([]byte, 0, 5), // important
}
}
func (c *XorConn) Write(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
for p := b; ; {
if len(p) <= c.OutSkip {
c.OutSkip -= len(p)
break
}
p = p[c.OutSkip:]
c.OutSkip = 0
need := 5 - len(c.OutHeader)
if len(p) < need {
c.OutHeader = append(c.OutHeader, p...)
c.CTR.XORKeyStream(p, p)
break
}
c.OutSkip, _ = DecodeHeader(append(c.OutHeader, p[:need]...))
c.OutHeader = c.OutHeader[:0]
c.CTR.XORKeyStream(p[:need], p[:need])
p = p[need:]
}
if _, err := c.Conn.Write(b); err != nil {
return 0, err
}
return len(b), nil
}
func (c *XorConn) Read(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
n, err := c.Conn.Read(b)
for p := b[:n]; ; {
if len(p) <= c.InSkip {
c.InSkip -= len(p)
break
}
p = p[c.InSkip:]
c.InSkip = 0
need := 5 - len(c.InHeader)
if len(p) < need {
c.PeerCTR.XORKeyStream(p, p)
c.InHeader = append(c.InHeader, p...)
break
}
c.PeerCTR.XORKeyStream(p[:need], p[:need])
c.InSkip, _ = DecodeHeader(append(c.InHeader, p[:need]...))
c.InHeader = c.InHeader[:0]
p = p[need:]
}
return n, err
}
// Upstream returns the underlying connection, allowing Vision to unwrap and access the TLS connection
func (c *XorConn) Upstream() any {
return c.Conn
}
func (c *XorConn) IsEncryptionLayer() bool {
return true
}

View File

@@ -2,8 +2,11 @@ package vless
import (
"context"
"encoding/base64"
"net"
"os"
"strconv"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/inbound"
@@ -14,6 +17,7 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/vless/encryption"
"github.com/sagernet/sing-box/transport/v2ray"
"github.com/sagernet/sing-vmess/packetaddr"
"github.com/sagernet/sing-vmess/vless"
@@ -35,14 +39,15 @@ var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
type Inbound struct {
inbound.Adapter
ctx context.Context
router adapter.ConnectionRouterEx
logger logger.ContextLogger
listener *listener.Listener
users []option.VLESSUser
service *vless.Service[int]
tlsConfig tls.ServerConfig
transport adapter.V2RayServerTransport
ctx context.Context
router adapter.ConnectionRouterEx
logger logger.ContextLogger
listener *listener.Listener
users []option.VLESSUser
service *vless.Service[int]
tlsConfig tls.ServerConfig
transport adapter.V2RayServerTransport
decryption *encryption.ServerInstance
}
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSInboundOptions) (adapter.Inbound, error) {
@@ -88,6 +93,18 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
return nil, E.Cause(err, "create server transport: ", options.Transport.Type)
}
}
// Parse decryption configuration
if options.Decryption != "" && options.Decryption != "none" {
decryptionConfig, err := parseServerDecryption(options.Decryption)
if err != nil {
return nil, E.Cause(err, "parse decryption")
}
inbound.decryption = &encryption.ServerInstance{}
if err := inbound.decryption.Init(decryptionConfig.keys, decryptionConfig.xorMode, decryptionConfig.secondsFrom, decryptionConfig.secondsTo, decryptionConfig.padding); err != nil {
return nil, E.Cause(err, "initialize decryption")
}
logger.Debug("decryption initialized with ", len(decryptionConfig.keys), " keys xorMode=", decryptionConfig.xorMode, " secondsFrom=", decryptionConfig.secondsFrom, " secondsTo=", decryptionConfig.secondsTo, " padding=", decryptionConfig.padding)
}
inbound.listener = listener.New(listener.Options{
Context: ctx,
Logger: logger,
@@ -139,6 +156,9 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
}
func (h *Inbound) Close() error {
if h.decryption != nil {
h.decryption.Close()
}
return common.Close(
h.service,
h.listener,
@@ -147,7 +167,26 @@ func (h *Inbound) Close() error {
)
}
func (h *Inbound) UpdateUsers(users []option.VLESSUser) {
h.users = users
h.service.UpdateUsers(common.MapIndexed(users, func(index int, _ option.VLESSUser) int {
return index
}), common.Map(users, func(it option.VLESSUser) string {
return it.UUID
}), common.Map(users, func(it option.VLESSUser) string {
return it.Flow
}))
}
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
canSplice := h.transport == nil
if canSplice && h.decryption != nil && h.decryption.IsFullRandomXorMode() {
canSplice = false
}
h.newConnectionExInternal(ctx, conn, metadata, onClose, canSplice)
}
func (h *Inbound) newConnectionExInternal(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc, canSplice bool) {
if h.tlsConfig != nil && h.transport == nil {
tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig)
if err != nil {
@@ -157,7 +196,17 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a
}
conn = tlsConn
}
err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose)
// Apply decryption if configured
if h.decryption != nil {
encConn, err := h.decryption.Handshake(conn, nil)
if err != nil {
N.CloseOnHandshakeFailure(conn, onClose, err)
h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": encryption handshake"))
return
}
conn = encConn
}
err := h.service.NewConnectionWithOptions(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose, canSplice)
if err != nil {
N.CloseOnHandshakeFailure(conn, onClose, err)
h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
@@ -206,6 +255,90 @@ func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn,
h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
}
type serverDecryptionConfig struct {
keys [][]byte
xorMode uint32
secondsFrom int64
secondsTo int64
padding string
}
func parseServerDecryption(raw string) (serverDecryptionConfig, error) {
var cfg serverDecryptionConfig
raw = strings.TrimSpace(raw)
if raw == "" {
return cfg, E.New("empty decryption string")
}
parts := strings.Split(raw, ".")
if len(parts) < 4 {
return cfg, E.New("invalid decryption string: missing components")
}
if parts[0] != "mlkem768x25519plus" {
return cfg, E.New("unsupported decryption prefix: ", parts[0])
}
switch parts[1] {
case "native":
cfg.xorMode = 0
case "xorpub":
cfg.xorMode = 1
case "random":
cfg.xorMode = 2
default:
return cfg, E.New("unknown decryption mode: ", parts[1])
}
secondsToken := strings.TrimSpace(parts[2])
if secondsToken == "" {
return cfg, E.New("invalid decryption seconds segment")
}
trimmed := strings.TrimSuffix(secondsToken, "s")
if trimmed == "" {
return cfg, E.New("invalid decryption seconds segment")
}
values := strings.SplitN(trimmed, "-", 2)
secondsFrom, err := strconv.ParseInt(values[0], 10, 64)
if err != nil {
return cfg, E.Cause(err, "parse decryption seconds_from")
}
cfg.secondsFrom = secondsFrom
if len(values) == 2 && values[1] != "" {
secondsTo, err := strconv.ParseInt(values[1], 10, 64)
if err != nil {
return cfg, E.Cause(err, "parse decryption seconds_to")
}
cfg.secondsTo = secondsTo
}
paddingPhase := true
var paddingParts []string
for _, segment := range parts[3:] {
segment = strings.TrimSpace(segment)
if segment == "" {
return cfg, E.New("invalid empty segment in decryption string")
}
if data, err := base64.RawURLEncoding.DecodeString(segment); err == nil {
if len(data) == 32 || len(data) == 64 {
cfg.keys = append(cfg.keys, data)
paddingPhase = false
continue
}
return cfg, E.New("invalid decryption key length: ", len(data))
}
if paddingPhase {
paddingParts = append(paddingParts, segment)
continue
}
return cfg, E.New("invalid decryption key: ", segment)
}
if len(cfg.keys) == 0 {
return cfg, E.New("no valid decryption keys found in decryption string")
}
if len(paddingParts) > 0 {
cfg.padding = strings.Join(paddingParts, ".")
}
return cfg, nil
}
var _ adapter.V2RayServerTransportHandler = (*inboundTransportHandler)(nil)
type inboundTransportHandler Inbound
@@ -218,5 +351,5 @@ func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.
metadata.InboundDetour = h.listener.ListenOptions().Detour
//nolint:staticcheck
h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
(*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose)
(*Inbound)(h).newConnectionExInternal(ctx, conn, metadata, onClose, false)
}

View File

@@ -2,16 +2,23 @@ package vless
import (
"context"
stdtls "crypto/tls"
"encoding/base64"
"net"
"reflect"
"strings"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/mux"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/common/vision"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/vless/encryption"
"github.com/sagernet/sing-box/transport/v2ray"
"github.com/sagernet/sing-vmess/packetaddr"
"github.com/sagernet/sing-vmess/vless"
@@ -39,6 +46,8 @@ type Outbound struct {
transport adapter.V2RayClientTransport
packetAddr bool
xudp bool
encryption *encryption.ClientInstance
vision bool
}
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSOutboundOptions) (adapter.Outbound, error) {
@@ -51,6 +60,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
logger: logger,
dialer: outboundDialer,
serverAddr: options.ServerOptions.Build(),
vision: strings.HasPrefix(options.Flow, "xtls-rprx-vision"),
}
if options.TLS != nil {
outbound.tlsConfig, err = tls.NewClientWithOptions(tls.ClientOptions{
@@ -86,11 +96,28 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
return nil, E.New("unknown packet encoding: ", options.PacketEncoding)
}
}
// Parse encryption configuration
if options.Encryption != "" && options.Encryption != "none" {
encryptionConfig, err := parseClientEncryption(options.Encryption)
if err != nil {
return nil, E.Cause(err, "parse encryption")
}
outbound.encryption = &encryption.ClientInstance{}
if err := outbound.encryption.Init(encryptionConfig.keys, encryptionConfig.xorMode, encryptionConfig.seconds, encryptionConfig.padding); err != nil {
return nil, E.Cause(err, "initialize encryption")
}
logger.Debug("encryption initialized: keys=", len(encryptionConfig.keys), " xorMode=", encryptionConfig.xorMode, " seconds=", encryptionConfig.seconds, " padding=", encryptionConfig.padding)
}
muxOpts := common.PtrValueOrDefault(options.Multiplex)
if muxOpts.Enabled {
options.Flow = ""
}
outbound.client, err = vless.NewClient(options.UUID, options.Flow, logger)
if err != nil {
return nil, err
}
outbound.multiplexDialer, err = mux.NewClientWithOptions((*vlessDialer)(outbound), logger, common.PtrValueOrDefault(options.Multiplex))
outbound.multiplexDialer, err = mux.NewClientWithOptions((*vlessDialer)(outbound), logger, muxOpts)
if err != nil {
return nil, err
}
@@ -147,20 +174,91 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
metadata.Outbound = h.Tag()
metadata.Destination = destination
var conn net.Conn
var baseConn net.Conn
var hookOnce sync.Once
if h.vision {
ctx = vision.WithHook(ctx, func(tlsConn net.Conn) {
if tlsConn == nil || !isVisionTLSConn(tlsConn) {
return
}
hookOnce.Do(func() {
baseConn = tlsConn
})
})
}
var err error
if h.transport != nil {
conn, err = h.transport.DialContext(ctx)
if err == nil && h.vision {
if baseConn == nil {
// Only set baseConn if the transport delivered a TLS-capable connection
if isVisionTLSConn(conn) {
h.logger.Warn("Vision enabled but hook was not called by transport, using fallback")
baseConn = conn
}
}
}
} else if h.tlsDialer != nil {
conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr)
if err == nil && h.vision && baseConn == nil {
baseConn = conn
}
} else {
conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
}
if err != nil {
return nil, err
}
// Apply encryption if configured
if h.encryption != nil {
conn, err = h.encryption.Handshake(conn)
if err != nil {
return nil, E.Cause(err, "encryption handshake")
}
}
// For Vision: wrap the connection to expose the TLS/encryption connection for vless client
var visionBaseConn net.Conn // The connection to pass to Vision (TLS or encryption layer)
var visionCanSplice bool
if h.vision {
isRAWTransport := h.transport == nil
if baseConn != nil && !isVisionTLSConn(baseConn) {
baseConn = nil
}
if baseConn != nil {
// Has TLS/Reality: use baseConn (TLS connection)
visionBaseConn = baseConn
visionCanSplice = isRAWTransport
conn = newVisionConnWrapper(conn, baseConn)
} else if h.encryption != nil {
// Only has encryption (no TLS/Reality): use encryption layer itself
encConn := findEncryptionLayer(conn)
if encConn != nil {
visionBaseConn = encConn
if h.encryption.IsFullRandomXorMode() {
visionCanSplice = false
} else {
visionCanSplice = isRAWTransport
}
conn = newVisionConnWrapper(conn, encConn)
} else {
return nil, E.New("Vision: failed to find encryption layer")
}
} else {
return nil, E.New("Vision requires either TLS/Reality or Encryption")
}
}
switch N.NetworkName(network) {
case N.NetworkTCP:
h.logger.InfoContext(ctx, "outbound connection to ", destination)
if h.vision && visionBaseConn != nil {
// For Vision, we need to pass the base connection (TLS or encryption layer)
// to prepareConn so it can properly initialize VisionConn
return h.client.DialEarlyConnWithOptions(conn, visionBaseConn, destination, visionCanSplice)
}
return h.client.DialEarlyConn(conn, destination)
case N.NetworkUDP:
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
@@ -201,6 +299,14 @@ func (h *vlessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr)
common.Close(conn)
return nil, err
}
// Apply encryption if configured
if h.encryption != nil {
conn, err = h.encryption.Handshake(conn)
if err != nil {
common.Close(conn)
return nil, E.Cause(err, "encryption handshake")
}
}
if h.xudp {
return h.client.DialEarlyXUDPPacketConn(conn, destination)
} else if h.packetAddr {
@@ -216,3 +322,152 @@ func (h *vlessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr)
return h.client.DialEarlyPacketConn(conn, destination)
}
}
type visionConnWrapper struct {
net.Conn
upstream net.Conn
}
var (
_ N.ReaderWithUpstream = (*visionConnWrapper)(nil)
_ N.WriterWithUpstream = (*visionConnWrapper)(nil)
_ common.WithUpstream = (*visionConnWrapper)(nil)
)
func newVisionConnWrapper(conn net.Conn, upstream net.Conn) net.Conn {
if upstream == nil || conn == nil || conn == upstream {
return conn
}
return &visionConnWrapper{
Conn: conn,
upstream: upstream,
}
}
func (c *visionConnWrapper) Upstream() any {
return c.upstream
}
func (c *visionConnWrapper) ReaderReplaceable() bool {
if replacer, ok := c.Conn.(N.ReaderWithUpstream); ok {
return replacer.ReaderReplaceable()
}
return true
}
func (c *visionConnWrapper) WriterReplaceable() bool {
if replacer, ok := c.Conn.(N.WriterWithUpstream); ok {
return replacer.WriterReplaceable()
}
return true
}
// isVisionTLSConn returns true when the provided connection exposes TLS semantics Vision expects.
func isVisionTLSConn(conn net.Conn) bool {
if conn == nil {
return false
}
if _, ok := conn.(interface{ ConnectionState() stdtls.ConnectionState }); ok {
return true
}
if _, ok := conn.(interface{ Handshake() error }); ok {
return true
}
connType := reflect.TypeOf(conn)
if connType == nil {
return false
}
if connType.Kind() == reflect.Ptr {
pkgPath := connType.Elem().PkgPath()
if pkgPath == "crypto/tls" || strings.Contains(pkgPath, "utls") || strings.Contains(pkgPath, "shadowtls") {
return true
}
}
return false
}
func findEncryptionLayer(conn net.Conn) net.Conn {
for conn != nil {
if enc, ok := conn.(encryption.EncryptionConn); ok && enc.IsEncryptionLayer() {
return conn
}
if upstream, ok := conn.(common.WithUpstream); ok {
if next := upstream.Upstream(); next != nil {
if nextConn, ok := next.(net.Conn); ok {
conn = nextConn
continue
}
}
}
break
}
return nil
}
type clientEncryptionConfig struct {
keys [][]byte
xorMode uint32
seconds uint32
padding string
}
func parseClientEncryption(raw string) (clientEncryptionConfig, error) {
var cfg clientEncryptionConfig
raw = strings.TrimSpace(raw)
if raw == "" {
return cfg, E.New("empty encryption string")
}
parts := strings.Split(raw, ".")
if len(parts) < 4 {
return cfg, E.New("invalid encryption string: missing components")
}
if parts[0] != "mlkem768x25519plus" {
return cfg, E.New("unsupported encryption prefix: ", parts[0])
}
switch parts[1] {
case "native":
cfg.xorMode = 0
case "xorpub":
cfg.xorMode = 1
case "random":
cfg.xorMode = 2
default:
return cfg, E.New("unknown encryption mode: ", parts[1])
}
switch parts[2] {
case "0rtt":
cfg.seconds = 1
case "1rtt":
cfg.seconds = 0
default:
return cfg, E.New("unsupported encryption RTT value: ", parts[2])
}
paddingPhase := true
var paddingParts []string
for _, segment := range parts[3:] {
segment = strings.TrimSpace(segment)
if segment == "" {
return cfg, E.New("invalid empty segment in encryption string")
}
if data, err := base64.RawURLEncoding.DecodeString(segment); err == nil {
if len(data) == 32 || len(data) == 1184 {
cfg.keys = append(cfg.keys, data)
paddingPhase = false
continue
}
return cfg, E.New("invalid encryption key length: ", len(data))
}
if paddingPhase {
paddingParts = append(paddingParts, segment)
continue
}
return cfg, E.New("invalid encryption key: ", segment)
}
if len(cfg.keys) == 0 {
return cfg, E.New("no valid encryption keys found in encryption string")
}
if len(paddingParts) > 0 {
cfg.padding = strings.Join(paddingParts, ".")
}
return cfg, nil
}

View File

@@ -153,6 +153,16 @@ func (h *Inbound) Close() error {
)
}
func (h *Inbound) UpdateUsers(users []option.VMessUser) {
h.service.UpdateUsers(common.MapIndexed(users, func(index int, _ option.VMessUser) int {
return index
}), common.Map(users, func(it option.VMessUser) string {
return it.UUID
}), common.Map(users, func(it option.VMessUser) int {
return it.AlterId
}))
}
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
if h.tlsConfig != nil && h.transport == nil {
tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig)

View File

@@ -154,6 +154,8 @@ func NewWARPEndpoint(ctx context.Context, router adapter.Router, logger log.Cont
netip.MustParsePrefix("0.0.0.0/0"),
netip.MustParsePrefix("::/0"),
},
PersistentKeepaliveInterval: options.PersistentKeepaliveInterval,
Reserved: options.Reserved,
},
},
MTU: 1280,

View File

@@ -1 +1 @@
with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0
with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_manager,with_admin_panel,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0

View File

@@ -13,7 +13,7 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/tlsfragment"
tf "github.com/sagernet/sing-box/common/tlsfragment"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"

View File

@@ -13,10 +13,10 @@ import (
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
R "github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing-mux"
mux "github.com/sagernet/sing-mux"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/ping"
"github.com/sagernet/sing-vmess"
vmess "github.com/sagernet/sing-vmess"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
@@ -139,12 +139,11 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
}
}
if selectedRule == nil {
defaultOutbound := r.outbound.Default()
if !common.Contains(defaultOutbound.Network(), N.NetworkTCP) {
if !common.Contains(r.defaultOutbound.Network(), N.NetworkTCP) {
buf.ReleaseMulti(buffers)
return E.New("TCP is not supported by default outbound: ", defaultOutbound.Tag())
return E.New("TCP is not supported by default outbound: ", r.defaultOutbound.Tag())
}
selectedOutbound = defaultOutbound
selectedOutbound = r.defaultOutbound
}
for _, buffer := range buffers {
@@ -265,12 +264,11 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
}
}
if selectedRule == nil || selectReturn {
defaultOutbound := r.outbound.Default()
if !common.Contains(defaultOutbound.Network(), N.NetworkUDP) {
if !common.Contains(r.defaultOutbound.Network(), N.NetworkUDP) {
N.ReleaseMultiPacketBuffer(packetBuffers)
return E.New("UDP is not supported by outbound: ", defaultOutbound.Tag())
return E.New("UDP is not supported by outbound: ", r.defaultOutbound.Tag())
}
selectedOutbound = defaultOutbound
selectedOutbound = r.defaultOutbound
}
for _, buffer := range packetBuffers {
conn = bufio.NewCachedPacketConn(conn, buffer.Buffer, buffer.Destination)

View File

@@ -29,7 +29,9 @@ type Router struct {
dnsTransport adapter.DNSTransportManager
connection adapter.ConnectionManager
network adapter.NetworkManager
defaultOutbound adapter.Outbound
rules []adapter.Rule
final string
needFindProcess bool
ruleSets []adapter.RuleSet
ruleSetMap map[string]adapter.RuleSet
@@ -51,6 +53,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route
connection: service.FromContext[adapter.ConnectionManager](ctx),
network: service.FromContext[adapter.NetworkManager](ctx),
rules: make([]adapter.Rule, 0, len(options.Rules)),
final: options.Final,
ruleSetMap: make(map[string]adapter.RuleSet),
needFindProcess: hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess,
pauseManager: service.FromContext[pause.Manager](ctx),
@@ -158,6 +161,15 @@ func (r *Router) Start(stage adapter.StartStage) error {
return E.Cause(err, "post start rule_set[", ruleSet.Name(), "]")
}
}
if r.final != "" {
defaultOutbound, loaded := r.outbound.Outbound(r.final)
if !loaded {
return E.New("outbound not found: ", r.final)
}
r.defaultOutbound = defaultOutbound
} else {
r.defaultOutbound = r.outbound.Default()
}
r.started = true
return nil
case adapter.StartStateStarted:

View File

@@ -0,0 +1,400 @@
package migration
import (
"database/sql"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/sagernet/sing-box/common/migrate/source"
)
var migrations = map[string]string{
"1_initialize_schema.up.sql": `
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
CREATE SEQUENCE public.goadmin_menu_myid_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
MAXVALUE 99999999
CACHE 1;
SET default_tablespace = '';
SET default_table_access_method = heap;
CREATE TABLE public.goadmin_menu (
id integer DEFAULT nextval('public.goadmin_menu_myid_seq'::regclass) NOT NULL,
parent_id integer DEFAULT 0 NOT NULL,
type integer DEFAULT 0,
"order" integer DEFAULT 0 NOT NULL,
title character varying(50) NOT NULL,
header character varying(100),
plugin_name character varying(100) NOT NULL,
icon character varying(50) NOT NULL,
uri character varying(3000) NOT NULL,
uuid character varying(100),
created_at timestamp without time zone DEFAULT now(),
updated_at timestamp without time zone DEFAULT now()
);
CREATE SEQUENCE public.goadmin_operation_log_myid_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
MAXVALUE 99999999
CACHE 1;
CREATE TABLE public.goadmin_operation_log (
id integer DEFAULT nextval('public.goadmin_operation_log_myid_seq'::regclass) NOT NULL,
user_id integer NOT NULL,
path character varying(255) NOT NULL,
method character varying(10) NOT NULL,
ip character varying(15) NOT NULL,
input text NOT NULL,
created_at timestamp without time zone DEFAULT now(),
updated_at timestamp without time zone DEFAULT now()
);
CREATE SEQUENCE public.goadmin_permissions_myid_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
MAXVALUE 99999999
CACHE 1;
CREATE TABLE public.goadmin_permissions (
id integer DEFAULT nextval('public.goadmin_permissions_myid_seq'::regclass) NOT NULL,
name character varying(50) NOT NULL,
slug character varying(50) NOT NULL,
http_method character varying(255),
http_path text NOT NULL,
created_at timestamp without time zone DEFAULT now(),
updated_at timestamp without time zone DEFAULT now()
);
CREATE TABLE public.goadmin_role_menu (
role_id integer NOT NULL,
menu_id integer NOT NULL,
created_at timestamp without time zone DEFAULT now(),
updated_at timestamp without time zone DEFAULT now()
);
CREATE TABLE public.goadmin_role_permissions (
role_id integer NOT NULL,
permission_id integer NOT NULL,
created_at timestamp without time zone DEFAULT now(),
updated_at timestamp without time zone DEFAULT now()
);
CREATE TABLE public.goadmin_role_users (
role_id integer NOT NULL,
user_id integer NOT NULL,
created_at timestamp without time zone DEFAULT now(),
updated_at timestamp without time zone DEFAULT now()
);
CREATE SEQUENCE public.goadmin_roles_myid_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
MAXVALUE 99999999
CACHE 1;
CREATE TABLE public.goadmin_roles (
id integer DEFAULT nextval('public.goadmin_roles_myid_seq'::regclass) NOT NULL,
name character varying NOT NULL,
slug character varying NOT NULL,
created_at timestamp without time zone DEFAULT now(),
updated_at timestamp without time zone DEFAULT now()
);
CREATE SEQUENCE public.goadmin_session_myid_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
MAXVALUE 99999999
CACHE 1;
CREATE TABLE public.goadmin_session (
id integer DEFAULT nextval('public.goadmin_session_myid_seq'::regclass) NOT NULL,
sid character varying(50) NOT NULL,
"values" character varying(3000) NOT NULL,
created_at timestamp without time zone DEFAULT now(),
updated_at timestamp without time zone DEFAULT now()
);
CREATE SEQUENCE public.goadmin_site_myid_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
MAXVALUE 99999999
CACHE 1;
CREATE TABLE public.goadmin_site (
id integer DEFAULT nextval('public.goadmin_site_myid_seq'::regclass) NOT NULL,
key character varying(100) NOT NULL,
value text NOT NULL,
type integer DEFAULT 0,
description character varying(3000),
state integer DEFAULT 0,
created_at timestamp without time zone DEFAULT now(),
updated_at timestamp without time zone DEFAULT now()
);
CREATE TABLE public.goadmin_user_permissions (
user_id integer NOT NULL,
permission_id integer NOT NULL,
created_at timestamp without time zone DEFAULT now(),
updated_at timestamp without time zone DEFAULT now()
);
CREATE SEQUENCE public.goadmin_users_myid_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
MAXVALUE 99999999
CACHE 1;
CREATE TABLE public.goadmin_users (
id integer DEFAULT nextval('public.goadmin_users_myid_seq'::regclass) NOT NULL,
username character varying(100) NOT NULL,
password character varying(100) NOT NULL,
name character varying(100) NOT NULL,
avatar character varying(255),
remember_token character varying(100),
created_at timestamp without time zone DEFAULT now(),
updated_at timestamp without time zone DEFAULT now()
);
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (1, 0, 1, 1, 'Dashboard', NULL, '', 'fa-bar-chart', '/', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (2, 0, 1, 2, 'Admin', NULL, '', 'fa-tasks', '', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (3, 2, 1, 2, 'Users', NULL, '', 'fa-users', '/info/manager', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (4, 2, 1, 3, 'Roles', NULL, '', 'fa-user', '/info/roles', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (5, 2, 1, 4, 'Permission', NULL, '', 'fa-ban', '/info/permission', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (7, 2, 1, 6, 'Operation log', NULL, '', 'fa-history', '/info/op', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (9, 0, 0, 9, 'Users', '', '', 'fa-users', '/info/users', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (14, 0, 0, 12, 'Github', 'Miscellaneous', '', 'fa-github', 'https://github.com/shtorm-7/sing-box-extended', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (15, 0, 0, 13, 'Donate', '', '', 'fa-heart', 'https://github.com/shtorm-7/sing-box-extended#support-the-project', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (13, 0, 0, 7, 'Squads', 'General', '', 'fa-gg', '/info/squads', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (11, 0, 0, 8, 'Nodes', '', '', 'fa-sitemap', '/info/nodes', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (10, 0, 0, 10, 'Connection limiters', 'Limiters', '', 'fa-plug', '/info/connection_limiters', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_menu (id, parent_id, type, "order", title, header, plugin_name, icon, uri, uuid, created_at, updated_at) VALUES (8, 0, 0, 11, 'Bandwidth limiters', '', '', 'fa-dashboard', '/info/bandwidth_limiters', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_permissions (id, name, slug, http_method, http_path, created_at, updated_at) VALUES (1, 'All permission', '*', '', '*', '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_permissions (id, name, slug, http_method, http_path, created_at, updated_at) VALUES (2, 'Dashboard', 'dashboard', 'GET,PUT,POST,DELETE', '/', '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_role_menu (role_id, menu_id, created_at, updated_at) VALUES (1, 1, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_role_menu (role_id, menu_id, created_at, updated_at) VALUES (1, 7, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_role_menu (role_id, menu_id, created_at, updated_at) VALUES (2, 7, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (1, 1, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (1, 2, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (2, 2, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
INSERT INTO public.goadmin_role_permissions (role_id, permission_id, created_at, updated_at) VALUES (0, 3, NULL, NULL);
INSERT INTO public.goadmin_role_users (role_id, user_id, created_at, updated_at) VALUES (1, 1, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_role_users (role_id, user_id, created_at, updated_at) VALUES (2, 2, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_roles (id, name, slug, created_at, updated_at) VALUES (1, 'Administrator', 'administrator', '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_roles (id, name, slug, created_at, updated_at) VALUES (2, 'Operator', 'operator', '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (6, 'site_off', 'false', 0, NULL, 1, '2026-02-15 09:57:02.436501', '2026-02-15 09:57:02.436501');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (7, 'prohibit_config_modification', 'false', 0, NULL, 1, '2026-02-15 09:57:02.441183', '2026-02-15 09:57:02.441183');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (11, 'login_url', '/login', 0, NULL, 1, '2026-02-15 09:57:02.459525', '2026-02-15 09:57:02.459525');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (16, 'open_admin_api', 'false', 0, NULL, 1, '2026-02-15 09:57:02.483908', '2026-02-15 09:57:02.483908');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (18, 'domain', '', 0, NULL, 1, '2026-02-15 09:57:02.493151', '2026-02-15 09:57:02.493151');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (23, 'asset_root_path', './public/', 0, NULL, 1, '2026-02-15 09:57:02.517213', '2026-02-15 09:57:02.517213');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (24, 'url_prefix', 'admin', 0, NULL, 1, '2026-02-15 09:57:02.521815', '2026-02-15 09:57:02.521815');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (33, 'exclude_theme_components', 'null', 0, NULL, 1, '2026-02-15 09:57:02.565725', '2026-02-15 09:57:02.565725');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (39, 'app_id', 'Qn0eh7HQsrt9', 0, NULL, 1, '2026-02-15 09:57:02.592551', '2026-02-15 09:57:02.592551');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (41, 'auth_user_table', 'goadmin_users', 0, NULL, 1, '2026-02-15 09:57:02.601496', '2026-02-15 09:57:02.601496');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (53, 'bootstrap_file_path', '', 0, NULL, 1, '2026-02-15 09:57:02.658984', '2026-02-15 09:57:02.658984');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (55, 'index_url', '/', 0, NULL, 1, '2026-02-15 09:57:02.668457', '2026-02-15 09:57:02.668457');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (66, 'login_logo', '', 0, NULL, 1, '2026-02-15 09:57:02.719608', '2026-02-15 09:57:02.719608');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (67, 'hide_visitor_user_center_entrance', 'false', 0, NULL, 1, '2026-02-15 09:57:02.724307', '2026-02-15 09:57:02.724307');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (68, 'go_mod_file_path', '', 0, NULL, 1, '2026-02-15 09:57:02.728694', '2026-02-15 09:57:02.728694');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (3, 'logger_encoder_caller', 'full', 0, NULL, 1, '2026-02-15 09:57:02.420312', '2026-02-15 09:57:02.420312');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (60, 'logger_encoder_caller_key', 'caller', 0, NULL, 1, '2026-02-15 09:57:02.692189', '2026-02-15 09:57:02.692189');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (34, 'logo', 'Sing-box Extended', 0, NULL, 1, '2026-02-15 09:57:02.570594', '2026-02-15 09:57:02.570594');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (69, 'env', 'prod', 0, NULL, 1, '2026-02-15 09:57:02.733059', '2026-02-15 09:57:02.733059');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (29, 'color_scheme', 'skin-black', 0, NULL, 1, '2026-02-15 09:57:02.545599', '2026-02-15 09:57:02.545599');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (17, 'allow_del_operation_log', 'false', 0, NULL, 1, '2026-02-15 09:57:02.488458', '2026-02-15 09:57:02.488458');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (35, 'info_log_path', '', 0, NULL, 1, '2026-02-15 09:57:02.574649', '2026-02-15 09:57:02.574649');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (22, 'operation_log_off', 'false', 0, NULL, 1, '2026-02-15 09:57:02.512394', '2026-02-15 09:57:02.512394');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (42, 'hide_app_info_entrance', 'false', 0, NULL, 1, '2026-02-15 09:57:02.606071', '2026-02-15 09:57:02.606071');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (12, 'access_log_path', '', 0, NULL, 1, '2026-02-15 09:57:02.464612', '2026-02-15 09:57:02.464612');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (32, 'logger_rotate_max_age', '30', 0, NULL, 1, '2026-02-15 09:57:02.560801', '2026-02-15 09:57:02.560801');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (40, 'custom_foot_html', '', 0, NULL, 1, '2026-02-15 09:57:02.597285', '2026-02-15 09:57:02.597285');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (62, 'logger_encoder_duration', 'string', 0, NULL, 1, '2026-02-15 09:57:02.701522', '2026-02-15 09:57:02.701522');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (65, 'logger_encoder_level_key', 'level', 0, NULL, 1, '2026-02-15 09:57:02.715108', '2026-02-15 09:57:02.715108');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (64, 'debug', 'false', 0, NULL, 1, '2026-02-15 09:57:02.710705', '2026-02-15 09:57:02.710705');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (43, 'hide_plugin_entrance', 'false', 0, NULL, 1, '2026-02-15 09:57:02.610825', '2026-02-15 09:57:02.610825');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (54, 'animation_type', '', 0, NULL, 1, '2026-02-15 09:57:02.663713', '2026-02-15 09:57:02.663713');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (48, 'theme', 'sword', 0, NULL, 1, '2026-02-15 09:57:02.634039', '2026-02-15 09:57:02.634039');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (45, 'info_log_off', 'false', 0, NULL, 1, '2026-02-15 09:57:02.620165', '2026-02-15 09:57:02.620165');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (31, 'error_log_path', '', 0, NULL, 1, '2026-02-15 09:57:02.555798', '2026-02-15 09:57:02.555798');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (5, 'asset_url', '', 0, NULL, 1, '2026-02-15 09:57:02.431855', '2026-02-15 09:57:02.431855');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (36, 'logger_encoder_encoding', 'console', 0, NULL, 1, '2026-02-15 09:57:02.579052', '2026-02-15 09:57:02.579052');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (27, 'login_title', 'Sing-box Extended', 0, NULL, 1, '2026-02-15 09:57:02.536102', '2026-02-15 09:57:02.536102');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (51, 'animation_duration', '0.00', 0, NULL, 1, '2026-02-15 09:57:02.64867', '2026-02-15 09:57:02.64867');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (19, 'file_upload_engine', '{"name":"local"}', 0, NULL, 1, '2026-02-15 09:57:02.49794', '2026-02-15 09:57:02.49794');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (26, 'logger_encoder_time', 'iso8601', 0, NULL, 1, '2026-02-15 09:57:02.531365', '2026-02-15 09:57:02.531365');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (10, 'custom_404_html', '', 0, NULL, 1, '2026-02-15 09:57:02.454777', '2026-02-15 09:57:02.454777');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (58, 'sql_log', 'false', 0, NULL, 1, '2026-02-15 09:57:02.682567', '2026-02-15 09:57:02.682567');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (2, 'logger_encoder_message_key', 'msg', 0, NULL, 1, '2026-02-15 09:57:02.415189', '2026-02-15 09:57:02.415189');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (46, 'logger_encoder_stacktrace_key', 'stacktrace', 0, NULL, 1, '2026-02-15 09:57:02.624977', '2026-02-15 09:57:02.624977');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (63, 'mini_logo', 'SBE', 0, NULL, 1, '2026-02-15 09:57:02.706145', '2026-02-15 09:57:02.706145');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (38, 'custom_403_html', '', 0, NULL, 1, '2026-02-15 09:57:02.588062', '2026-02-15 09:57:02.588062');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (30, 'language', 'en', 0, NULL, 1, '2026-02-15 09:57:02.550466', '2026-02-15 09:57:02.550466');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (15, 'hide_config_center_entrance', 'false', 0, NULL, 1, '2026-02-15 09:57:02.479097', '2026-02-15 09:57:02.479097');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (59, 'logger_rotate_max_backups', '5', 0, NULL, 1, '2026-02-15 09:57:02.687429', '2026-02-15 09:57:02.687429');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (57, 'custom_head_html', '', 0, NULL, 1, '2026-02-15 09:57:02.677723', '2026-02-15 09:57:02.677723');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (52, 'custom_500_html', '', 0, NULL, 1, '2026-02-15 09:57:02.654236', '2026-02-15 09:57:02.654236');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (44, 'title', 'Sing-box Extended', 0, NULL, 1, '2026-02-15 09:57:02.615471', '2026-02-15 09:57:02.615471');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (47, 'session_life_time', '7200', 0, NULL, 1, '2026-02-15 09:57:02.629619', '2026-02-15 09:57:02.629619');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (8, 'access_log_off', 'false', 0, NULL, 1, '2026-02-15 09:57:02.445593', '2026-02-15 09:57:02.445593');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (49, 'error_log_off', 'false', 0, NULL, 1, '2026-02-15 09:57:02.6385', '2026-02-15 09:57:02.6385');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (50, 'logger_rotate_max_size', '10', 0, NULL, 1, '2026-02-15 09:57:02.643733', '2026-02-15 09:57:02.643733');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (14, 'logger_rotate_compress', 'false', 0, NULL, 1, '2026-02-15 09:57:02.474296', '2026-02-15 09:57:02.474296');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (13, 'logger_encoder_time_key', 'ts', 0, NULL, 1, '2026-02-15 09:57:02.469396', '2026-02-15 09:57:02.469396');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (37, 'animation_delay', '0.00', 0, NULL, 1, '2026-02-15 09:57:02.583815', '2026-02-15 09:57:02.583815');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (20, 'extra', '', 0, NULL, 1, '2026-02-15 09:57:02.50276', '2026-02-15 09:57:02.50276');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (25, 'access_assets_log_off', 'false', 0, NULL, 1, '2026-02-15 09:57:02.526618', '2026-02-15 09:57:02.526618');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (4, 'logger_level', '0', 0, NULL, 1, '2026-02-15 09:57:02.426736', '2026-02-15 09:57:02.426736');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (9, 'footer_info', '', 0, NULL, 1, '2026-02-15 09:57:02.450409', '2026-02-15 09:57:02.450409');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (21, 'no_limit_login_ip', 'false', 0, NULL, 1, '2026-02-15 09:57:02.507609', '2026-02-15 09:57:02.507609');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (28, 'hide_tool_entrance', 'false', 0, NULL, 1, '2026-02-15 09:57:02.540813', '2026-02-15 09:57:02.540813');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (61, 'logger_encoder_level', 'capitalColor', 0, NULL, 1, '2026-02-15 09:57:02.696859', '2026-02-15 09:57:02.696859');
INSERT INTO public.goadmin_site (id, key, value, type, description, state, created_at, updated_at) VALUES (56, 'logger_encoder_name_key', 'logger', 0, NULL, 1, '2026-02-15 09:57:02.672962', '2026-02-15 09:57:02.672962');
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (1, 1, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (2, 2, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
INSERT INTO public.goadmin_user_permissions (user_id, permission_id, created_at, updated_at) VALUES (0, 1, NULL, NULL);
INSERT INTO public.goadmin_users (id, username, password, name, avatar, remember_token, created_at, updated_at) VALUES (2, 'operator', '$2a$10$rVqkOzHjN2MdlEprRflb1eGP0oZXuSrbJLOmJagFsCd81YZm0bsh.', 'Operator', '', NULL, '2019-09-10 00:00:00', '2019-09-10 00:00:00');
INSERT INTO public.goadmin_users (id, username, password, name, avatar, remember_token, created_at, updated_at) VALUES (1, 'admin', '$2a$10$ilNHHnX5S6EMw.Ffc1Y1JezYCyquFIO.7Z0vLr1eHJUXnGy4cdrtq', 'admin', '', 'tlNcBVK9AvfYH7WEnwB1RKvocJu8FfRy4um3DJtwdHuJy0dwFsLOgAc0xUfh', '2019-09-10 00:00:00', '2019-09-10 00:00:00');
SELECT pg_catalog.setval('public.goadmin_menu_myid_seq', 12, true);
SELECT pg_catalog.setval('public.goadmin_operation_log_myid_seq', 11, true);
SELECT pg_catalog.setval('public.goadmin_permissions_myid_seq', 2, true);
SELECT pg_catalog.setval('public.goadmin_roles_myid_seq', 2, true);
SELECT pg_catalog.setval('public.goadmin_session_myid_seq', 7, true);
SELECT pg_catalog.setval('public.goadmin_site_myid_seq', 69, true);
SELECT pg_catalog.setval('public.goadmin_users_myid_seq', 2, true);
ALTER TABLE ONLY public.goadmin_menu
ADD CONSTRAINT goadmin_menu_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.goadmin_operation_log
ADD CONSTRAINT goadmin_operation_log_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.goadmin_permissions
ADD CONSTRAINT goadmin_permissions_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.goadmin_roles
ADD CONSTRAINT goadmin_roles_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.goadmin_session
ADD CONSTRAINT goadmin_session_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.goadmin_site
ADD CONSTRAINT goadmin_site_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.goadmin_users
ADD CONSTRAINT goadmin_users_pkey PRIMARY KEY (id);
`,
"1_initialize_schema.down.sql": ``,
}
func MigratePostgreSQL(db *sql.DB) error {
driver, err := postgres.WithInstance(db, &postgres.Config{})
if err != nil {
return err
}
sourceDriver := source.NewRawDriver(migrations)
if err := sourceDriver.Init(); err != nil {
return err
}
m, err := migrate.NewWithInstance(
"raw",
sourceDriver,
"postgres",
driver,
)
if err != nil {
return err
}
return m.Up()
}

View File

@@ -0,0 +1,13 @@
package pages
import (
"github.com/GoAdminGroup/go-admin/context"
"github.com/GoAdminGroup/go-admin/template/types"
)
func DashboardPage(ctx *context.Context) (types.Panel, error) {
return types.Panel{
Title: "Dashboard",
}, nil
}

View File

@@ -0,0 +1,188 @@
//go:build with_admin_panel
package admin_panel
import (
"context"
"database/sql"
"errors"
"net/http"
"github.com/go-chi/chi"
"github.com/golang-migrate/migrate/v4"
_ "github.com/lib/pq"
"golang.org/x/net/http2"
_ "github.com/GoAdminGroup/go-admin/adapter/chi"
"github.com/GoAdminGroup/go-admin/engine"
"github.com/GoAdminGroup/go-admin/modules/config"
_ "github.com/GoAdminGroup/go-admin/modules/db/drivers/sqlite"
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
"github.com/GoAdminGroup/go-admin/template"
"github.com/GoAdminGroup/go-admin/template/chartjs"
_ "github.com/GoAdminGroup/themes/adminlte"
_ "github.com/GoAdminGroup/themes/sword"
"github.com/sagernet/sing-box/adapter"
boxService "github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/common/listener"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/service/admin_panel/migration"
"github.com/sagernet/sing-box/service/admin_panel/pages"
"github.com/sagernet/sing-box/service/admin_panel/tables"
CM "github.com/sagernet/sing-box/service/manager/constant"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
aTLS "github.com/sagernet/sing/common/tls"
"github.com/sagernet/sing/service"
)
func RegisterService(registry *boxService.Registry) {
boxService.Register[option.AdminPanelServiceOptions](registry, C.TypeAdminPanel, NewService)
}
type Service struct {
boxService.Adapter
ctx context.Context
logger log.ContextLogger
listener *listener.Listener
tlsConfig tls.ServerConfig
httpServer *http.Server
options option.AdminPanelServiceOptions
}
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.AdminPanelServiceOptions) (adapter.Service, error) {
s := &Service{
Adapter: boxService.NewAdapter(C.TypeAdminPanel, tag),
ctx: ctx,
logger: logger,
listener: listener.New(listener.Options{
Context: ctx,
Logger: logger,
Network: []string{N.NetworkTCP},
Listen: options.ListenOptions,
}),
options: options,
}
return s, nil
}
func (s *Service) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
boxManager := service.FromContext[adapter.ServiceManager](s.ctx)
service, ok := boxManager.Get(s.options.Manager)
if !ok {
return E.New("manager ", s.options.Manager, " not found")
}
manager, ok := service.(CM.Manager)
if !ok {
return E.New("invalid ", s.options.Manager, " manager")
}
switch s.options.Database.Driver {
case "postgresql":
db, err := sql.Open("postgres", s.options.Database.DSN)
if err != nil {
return err
}
defer db.Close()
if err := migration.MigratePostgreSQL(db); err != nil && err != migrate.ErrNoChange {
return err
}
default:
return E.New("unknown driver \"", s.options.Database.Driver, "\"")
}
var generators = map[string]table.Generator{
"squads": tables.SquadTableFactory(
manager,
s.logger,
),
"nodes": tables.NodeTableFactory(
manager,
s.logger,
),
"users": tables.UserTableFactory(
manager,
s.logger,
),
"connection_limiters": tables.ConnectionLimiterTableFactory(
manager,
s.logger,
),
"bandwidth_limiters": tables.BandwidthLimiterTableFactory(
manager,
s.logger,
),
}
eng := engine.Default()
chiRouter := chi.NewRouter()
template.AddComp(chartjs.NewChart())
if err := eng.AddConfig(&config.Config{
UrlPrefix: "admin",
IndexUrl: "/",
LoginUrl: "/login",
Databases: config.DatabaseList{
"default": config.Database{
Driver: s.options.Database.Driver,
Dsn: s.options.Database.DSN,
},
},
}).
AddGenerators(generators).
Use(chiRouter); err != nil {
return err
}
eng.HTML("GET", "/admin", pages.DashboardPage)
chiRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/admin", http.StatusMovedPermanently)
})
chiRouter.Get("/admin/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/admin", http.StatusMovedPermanently)
})
if s.options.TLS != nil {
tlsConfig, err := tls.NewServer(s.ctx, s.logger, common.PtrValueOrDefault(s.options.TLS))
if err != nil {
return err
}
s.tlsConfig = tlsConfig
}
if s.tlsConfig != nil {
err := s.tlsConfig.Start()
if err != nil {
return E.Cause(err, "create TLS config")
}
}
tcpListener, err := s.listener.ListenTCP()
if err != nil {
return err
}
if s.tlsConfig != nil {
if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) {
s.tlsConfig.SetNextProtos(append([]string{"h2"}, s.tlsConfig.NextProtos()...))
}
tcpListener = aTLS.NewListener(tcpListener, s.tlsConfig)
}
s.httpServer = &http.Server{
Handler: chiRouter,
}
go func() {
err = s.httpServer.Serve(tcpListener)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
s.logger.Error("serve error: ", err)
}
}()
return nil
}
func (s *Service) Close() error {
return common.Close(
common.PtrOrNil(s.httpServer),
common.PtrOrNil(s.listener),
s.tlsConfig,
)
}

View File

@@ -0,0 +1,20 @@
//go:build !with_admin_panel
package admin_panel
import (
"context"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/service"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func RegisterService(registry *service.Registry) {
service.Register[option.AdminPanelServiceOptions](registry, C.TypeAdminPanel, func(ctx context.Context, logger log.ContextLogger, tag string, options option.AdminPanelServiceOptions) (adapter.Service, error) {
return nil, E.New(`Admin panel is not included in this build, rebuild with -tags with_admin_panel`)
})
}

View File

@@ -0,0 +1,259 @@
package tables
import (
"encoding/json"
"strconv"
"strings"
"time"
"github.com/GoAdminGroup/go-admin/context"
"github.com/GoAdminGroup/go-admin/modules/db"
mForm "github.com/GoAdminGroup/go-admin/plugins/admin/modules/form"
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/parameter"
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
"github.com/GoAdminGroup/go-admin/template"
"github.com/GoAdminGroup/go-admin/template/types"
"github.com/GoAdminGroup/go-admin/template/types/form"
"github.com/sagernet/sing-box/log"
CM "github.com/sagernet/sing-box/service/manager/constant"
)
func BandwidthLimiterTableFactory(manager CM.Manager, logger log.Logger) func(ctx *context.Context) table.Table {
return func(ctx *context.Context) table.Table {
t := table.NewDefaultTable(ctx, table.Config{
CanAdd: true,
Editable: true,
Deletable: true,
Exportable: true,
PrimaryKey: table.PrimaryKey{
Type: db.Int,
Name: table.DefaultPrimaryKeyName,
},
})
squads, err := manager.GetSquads(map[string][]string{})
if err != nil {
return nil
}
squadsByID := make(map[int]string, len(squads))
squadOptions := make(types.FieldOptions, len(squads))
for i, squad := range squads {
squadsByID[squad.ID] = squad.Name
squadOptions[i] = types.FieldOption{
Text: squad.Name,
Value: strconv.Itoa(squad.ID),
}
}
info := t.GetInfo().SetFilterFormLayout(form.LayoutFilter)
info.AddField("ID", "id", db.Int).
FieldSortable()
info.AddField("Squads", "squad_ids", db.Varchar).
FieldDisplay(func(model types.FieldModel) interface{} {
values := model.Row["squad_ids"].([]interface{})
labels := template.HTML("")
labelTpl := label(ctx).SetType("success")
labelValues := make([]string, len(values))
for i, squadID := range values {
labelValues[i] = squadsByID[int(squadID.(float64))]
}
for key, label := range labelValues {
if key == len(labelValues)-1 {
labels += labelTpl.SetContent(template.HTML(label)).GetContent()
} else {
labels += labelTpl.SetContent(template.HTML(label)).GetContent()
}
}
return labels
})
info.AddField("Username", "username", db.Varchar).
FieldFilterable().
FieldSortable()
info.AddField("Outbound", "outbound", db.Varchar).
FieldFilterable().
FieldSortable()
info.AddField("Strategy", "strategy", db.Varchar).
FieldFilterable(types.FilterType{
FormType: form.SelectSingle,
Options: types.FieldOptions{
{Text: "Connection", Value: "connection"},
{Text: "Global", Value: "global"},
},
}).
FieldSortable()
info.AddField("Mode", "mode", db.Varchar).
FieldFilterable(types.FilterType{
FormType: form.SelectSingle,
Options: types.FieldOptions{
{Text: "Download", Value: "download"},
{Text: "Upload", Value: "upload"},
{Text: "Duplex", Value: "duplex"},
},
}).
FieldSortable()
info.AddField("Connection type", "connection_type", db.Varchar).
FieldFilterable(types.FilterType{
FormType: form.SelectSingle,
Options: types.FieldOptions{
{Text: "HWID", Value: "hwid"},
{Text: "Mux", Value: "mux"},
{Text: "IP", Value: "ip"},
},
}).
FieldSortable()
info.AddField("Speed", "speed", db.Varchar).
FieldSortable()
info.AddField("Created at", "created_at", db.Datetime).
FieldDisplay(func(model types.FieldModel) interface{} {
t, err := time.Parse(time.RFC3339, model.Value)
if err != nil {
return model.Value
}
return t.Format("2006-01-02 15:04:05")
}).
FieldFilterable(types.FilterType{FormType: form.DatetimeRange}).
FieldSortable()
info.AddField("Updated at", "updated_at", db.Datetime).
FieldDisplay(func(model types.FieldModel) interface{} {
t, err := time.Parse(time.RFC3339, model.Value)
if err != nil {
return model.Value
}
return t.Format("2006-01-02 15:04:05")
}).
FieldFilterable(types.FilterType{FormType: form.DatetimeRange}).
FieldSortable()
info.SetGetDataFn(func(param parameter.Parameters) ([]map[string]interface{}, int) {
filters := make(map[string][]string)
listFilters := map[string][]string{
"offset": {strconv.Itoa((param.PageInt - 1) * param.PageSizeInt)},
"limit": {param.PageSize},
}
for k, v := range param.Fields {
if strings.HasPrefix(k, "__") {
continue
}
key := strings.TrimSuffix(k, "__goadmin")
filters[key] = v
listFilters[key] = v
}
if param.SortField != "" {
if param.SortType == "asc" {
listFilters["sort_asc"] = []string{param.SortField}
} else {
listFilters["sort_desc"] = []string{param.SortField}
}
}
items, err := manager.GetBandwidthLimiters(listFilters)
if err != nil {
logger.Error(err)
return nil, 0
}
count, err := manager.GetBandwidthLimitersCount(filters)
if err != nil {
logger.Error(err)
return nil, 0
}
result := make([]map[string]interface{}, 0, len(items))
for _, item := range items {
var data map[string]interface{}
raw, _ := json.Marshal(item)
json.Unmarshal(raw, &data)
result = append(result, data)
}
return result, count
})
info.SetDeleteFn(func(ids []string) error {
for _, id := range ids {
i, err := strconv.Atoi(id)
if err != nil {
return err
}
if _, err := manager.DeleteBandwidthLimiter(i); err != nil {
return err
}
}
return nil
})
info.SetTable("bandwidth_limiters").SetTitle("Bandwidth Limiters").SetDescription("Bandwidth Limiters")
formList := t.GetForm()
formList.AddField("ID", "id", db.Int, form.Default).
FieldNotAllowAdd().
FieldNotAllowEdit()
formList.AddField("Squads", "squad_ids", db.Varchar, form.Select).
FieldMust().
FieldOptions(squadOptions).
FieldDisableWhenUpdate()
formList.AddField("Username", "username", db.Varchar, form.Text).
FieldMust().
FieldDisplayButCanNotEditWhenUpdate()
formList.AddField("Outbound", "outbound", db.Varchar, form.Text).
FieldMust().
FieldDisplayButCanNotEditWhenUpdate()
formList.AddField("Strategy", "strategy", db.Varchar, form.SelectSingle).
FieldMust().
FieldOptions(types.FieldOptions{
{Text: "Connection", Value: "connection"},
{Text: "Global", Value: "global"},
}).
FieldOnChooseOptionsHide([]string{"", "global"}, "connection_type")
formList.AddField("Mode", "mode", db.Varchar, form.SelectSingle).
FieldMust().
FieldOptions(types.FieldOptions{
{Text: "Download", Value: "download"},
{Text: "Upload", Value: "upload"},
{Text: "Duplex", Value: "duplex"},
})
formList.AddField("Connection type", "connection_type", db.Varchar, form.SelectSingle).
FieldOptions(types.FieldOptions{
{Text: "HWID", Value: "hwid"},
{Text: "Mux", Value: "mux"},
{Text: "IP", Value: "ip"},
})
formList.AddField("Speed", "speed", db.Varchar, form.Text).
FieldMust()
formList.SetInsertFn(func(values mForm.Values) error {
squadIDs := make([]int, len(values["squad_ids[]"]))
for i, rawSquadID := range values["squad_ids[]"] {
squadID, err := strconv.Atoi(rawSquadID)
if err != nil {
return err
}
squadIDs[i] = squadID
}
_, err := manager.CreateBandwidthLimiter(CM.BandwidthLimiterCreate{
SquadIDs: squadIDs,
Username: values.Get("username"),
Outbound: values.Get("outbound"),
Strategy: values.Get("strategy"),
Mode: values.Get("mode"),
ConnectionType: values.Get("connection_type"),
Speed: values.Get("speed"),
})
return err
})
formList.SetUpdateFn(func(values mForm.Values) error {
id, err := strconv.Atoi(values.Get("id"))
if err != nil {
return err
}
_, err = manager.UpdateBandwidthLimiter(id, CM.BandwidthLimiterUpdate{
Username: values.Get("username"),
Outbound: values.Get("outbound"),
Strategy: values.Get("strategy"),
Mode: values.Get("mode"),
ConnectionType: values.Get("connection_type"),
Speed: values.Get("speed"),
})
return err
})
formList.SetTable("bandwidth_limiters").SetTitle("Bandwidth Limiters").SetDescription("Bandwidth Limiters")
return t
}
}

View File

@@ -0,0 +1,261 @@
package tables
import (
"encoding/json"
"strconv"
"strings"
"time"
"github.com/GoAdminGroup/go-admin/context"
"github.com/GoAdminGroup/go-admin/modules/db"
mForm "github.com/GoAdminGroup/go-admin/plugins/admin/modules/form"
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/parameter"
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
"github.com/GoAdminGroup/go-admin/template"
"github.com/GoAdminGroup/go-admin/template/types"
"github.com/GoAdminGroup/go-admin/template/types/form"
"github.com/sagernet/sing-box/log"
CM "github.com/sagernet/sing-box/service/manager/constant"
)
func ConnectionLimiterTableFactory(manager CM.Manager, logger log.Logger) func(ctx *context.Context) table.Table {
return func(ctx *context.Context) table.Table {
connectionLimiterTable := table.NewDefaultTable(ctx, table.Config{
CanAdd: true,
Editable: true,
Deletable: true,
Exportable: true,
PrimaryKey: table.PrimaryKey{
Type: db.Int,
Name: table.DefaultPrimaryKeyName,
},
})
squads, err := manager.GetSquads(map[string][]string{})
if err != nil {
return nil
}
squadsByID := make(map[int]string, len(squads))
squadOptions := make(types.FieldOptions, len(squads))
for i, squad := range squads {
squadsByID[squad.ID] = squad.Name
squadOptions[i] = types.FieldOption{
Text: squad.Name,
Value: strconv.Itoa(squad.ID),
}
}
info := connectionLimiterTable.GetInfo().SetFilterFormLayout(form.LayoutFilter)
info.AddField("ID", "id", db.Int).
FieldSortable()
info.AddField("Squads", "squad_ids", db.Varchar).
FieldDisplay(func(model types.FieldModel) interface{} {
values := model.Row["squad_ids"].([]interface{})
labels := template.HTML("")
labelTpl := label(ctx).SetType("success")
labelValues := make([]string, len(values))
for i, squadID := range values {
labelValues[i] = squadsByID[int(squadID.(float64))]
}
for key, label := range labelValues {
if key == len(labelValues)-1 {
labels += labelTpl.SetContent(template.HTML(label)).GetContent()
} else {
labels += labelTpl.SetContent(template.HTML(label)).GetContent()
}
}
return labels
})
info.AddField("Username", "username", db.Varchar).
FieldFilterable().
FieldSortable()
info.AddField("Outbound", "outbound", db.Varchar).
FieldFilterable().
FieldSortable()
info.AddField("Strategy", "strategy", db.Varchar).
FieldFilterable(types.FilterType{
FormType: form.SelectSingle,
Options: types.FieldOptions{
{Text: "Connection", Value: "connection"},
},
}).
FieldSortable()
info.AddField("Connection type", "connection_type", db.Varchar).
FieldFilterable(types.FilterType{
FormType: form.SelectSingle,
Options: types.FieldOptions{
{Text: "Mux", Value: "mux"},
{Text: "HWID", Value: "hwid"},
{Text: "IP", Value: "ip"},
},
}).
FieldSortable()
info.AddField("Lock type", "lock_type", db.Varchar).
FieldFilterable(types.FilterType{
FormType: form.SelectSingle,
Options: types.FieldOptions{
{Text: "Manager", Value: "manager"},
},
}).
FieldSortable()
info.AddField("Count", "count", db.Int).
FieldSortable()
info.AddField("Created at", "created_at", db.Datetime).
FieldDisplay(func(model types.FieldModel) interface{} {
t, err := time.Parse(time.RFC3339, model.Value)
if err != nil {
return model.Value
}
return t.Format("2006-01-02 15:04:05")
}).
FieldFilterable(types.FilterType{FormType: form.DatetimeRange}).
FieldSortable()
info.AddField("Updated at", "updated_at", db.Datetime).
FieldDisplay(func(model types.FieldModel) interface{} {
t, err := time.Parse(time.RFC3339, model.Value)
if err != nil {
return model.Value
}
return t.Format("2006-01-02 15:04:05")
}).
FieldFilterable(types.FilterType{FormType: form.DatetimeRange}).
FieldSortable()
info.SetGetDataFn(func(param parameter.Parameters) ([]map[string]interface{}, int) {
filters := make(map[string][]string)
listFilters := map[string][]string{
"offset": {strconv.Itoa((param.PageInt - 1) * param.PageSizeInt)},
"limit": {param.PageSize},
}
for k, v := range param.Fields {
if strings.HasPrefix(k, "__") {
continue
}
key := strings.TrimSuffix(k, "__goadmin")
filters[key] = v
listFilters[key] = v
}
if param.SortField != "" {
if param.SortType == "asc" {
listFilters["sort_asc"] = []string{param.SortField}
} else {
listFilters["sort_desc"] = []string{param.SortField}
}
}
items, err := manager.GetConnectionLimiters(listFilters)
if err != nil {
logger.Error(err)
return nil, 0
}
count, err := manager.GetConnectionLimitersCount(filters)
if err != nil {
logger.Error(err)
return nil, 0
}
result := make([]map[string]interface{}, 0, len(items))
for _, item := range items {
var data map[string]interface{}
raw, _ := json.Marshal(item)
json.Unmarshal(raw, &data)
result = append(result, data)
}
return result, count
})
info.SetDeleteFn(func(ids []string) error {
for _, id := range ids {
i, err := strconv.Atoi(id)
if err != nil {
return err
}
if _, err := manager.DeleteConnectionLimiter(i); err != nil {
return err
}
}
return nil
})
info.SetTable("connection_limiters").SetTitle("Connection Limiters").SetDescription("Connection Limiters")
formList := connectionLimiterTable.GetForm()
formList.AddField("ID", "id", db.Int, form.Default).
FieldNotAllowAdd().
FieldNotAllowEdit()
formList.AddField("Squads", "squad_ids", db.Varchar, form.Select).
FieldMust().
FieldOptions(squadOptions).
FieldDisableWhenUpdate()
formList.AddField("Username", "username", db.Varchar, form.Text).
FieldMust().
FieldDisplayButCanNotEditWhenUpdate()
formList.AddField("Outbound", "outbound", db.Varchar, form.Text).
FieldMust().
FieldDisplayButCanNotEditWhenUpdate()
formList.AddField("Strategy", "strategy", db.Varchar, form.SelectSingle).
FieldMust().
FieldOptions(types.FieldOptions{
{Text: "Connection", Value: "connection"},
}).
FieldDefault("connection")
formList.AddField("Connection type", "connection_type", db.Varchar, form.SelectSingle).
FieldOptions(types.FieldOptions{
{Text: "Mux", Value: "mux"},
{Text: "HWID", Value: "hwid"},
{Text: "IP", Value: "ip"},
})
formList.AddField("Lock type", "lock_type", db.Varchar, form.SelectSingle).
FieldOptions(types.FieldOptions{
{Text: "Manager", Value: "manager"},
})
formList.AddField("Count", "count", db.Int, form.Number).
FieldMust().
FieldDefault("0")
formList.SetInsertFn(func(values mForm.Values) error {
squadIDs := make([]int, len(values["squad_ids[]"]))
for i, rawSquadID := range values["squad_ids[]"] {
squadID, err := strconv.Atoi(rawSquadID)
if err != nil {
return err
}
squadIDs[i] = squadID
}
count, err := strconv.ParseUint(values.Get("count"), 10, 32)
if err != nil {
return err
}
_, err = manager.CreateConnectionLimiter(CM.ConnectionLimiterCreate{
SquadIDs: squadIDs,
Username: values.Get("username"),
Outbound: values.Get("outbound"),
Strategy: values.Get("strategy"),
ConnectionType: values.Get("connection_type"),
LockType: values.Get("lock_type"),
Count: uint32(count),
})
return err
})
formList.SetUpdateFn(func(values mForm.Values) error {
id, err := strconv.Atoi(values.Get("id"))
if err != nil {
return err
}
count, err := strconv.ParseUint(values.Get("count"), 10, 32)
if err != nil {
return err
}
_, err = manager.UpdateConnectionLimiter(id, CM.ConnectionLimiterUpdate{
Username: values.Get("username"),
Outbound: values.Get("outbound"),
Strategy: values.Get("strategy"),
ConnectionType: values.Get("connection_type"),
LockType: values.Get("lock_type"),
Count: uint32(count),
})
return err
})
formList.SetTable("connection_limiters").SetTitle("Connection Limiters").SetDescription("Connection Limiters")
return connectionLimiterTable
}
}

View File

@@ -0,0 +1,201 @@
package tables
import (
"encoding/json"
"strconv"
"strings"
"time"
"github.com/GoAdminGroup/go-admin/context"
"github.com/GoAdminGroup/go-admin/modules/config"
"github.com/GoAdminGroup/go-admin/modules/db"
mForm "github.com/GoAdminGroup/go-admin/plugins/admin/modules/form"
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/parameter"
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
"github.com/GoAdminGroup/go-admin/template"
"github.com/GoAdminGroup/go-admin/template/types"
"github.com/GoAdminGroup/go-admin/template/types/form"
"github.com/gofrs/uuid/v5"
"github.com/sagernet/sing-box/log"
CM "github.com/sagernet/sing-box/service/manager/constant"
)
func label(ctx *context.Context) types.LabelAttribute {
return template.Get(ctx, config.GetTheme()).Label().SetType("success")
}
func NodeTableFactory(manager CM.Manager, logger log.Logger) func(ctx *context.Context) (nodeTable table.Table) {
return func(ctx *context.Context) (nodeTable table.Table) {
nodeTable = table.NewDefaultTable(ctx, table.Config{
CanAdd: true,
Editable: true,
Deletable: true,
Exportable: true,
PrimaryKey: table.PrimaryKey{
Type: db.Varchar,
Name: "uuid",
},
})
squads, err := manager.GetSquads(map[string][]string{})
if err != nil {
return nil
}
squadsByID := make(map[int]string, len(squads))
squadOptions := make(types.FieldOptions, len(squads))
for i, squad := range squads {
squadsByID[squad.ID] = squad.Name
squadOptions[i] = types.FieldOption{
Text: squad.Name,
Value: strconv.Itoa(squad.ID),
}
}
info := nodeTable.GetInfo().SetFilterFormLayout(form.LayoutFilter)
info.AddField("UUID", "uuid", db.Varchar).
FieldFilterable().
FieldSortable()
info.AddField("Name", "name", db.Varchar).
FieldFilterable().
FieldSortable()
info.AddField("Squads", "squad_ids", db.Varchar).
FieldDisplay(func(model types.FieldModel) interface{} {
values := model.Row["squad_ids"].([]interface{})
labels := template.HTML("")
labelTpl := label(ctx).SetType("success")
labelValues := make([]string, len(values))
for i, squadID := range values {
labelValues[i] = squadsByID[int(squadID.(float64))]
}
for key, label := range labelValues {
if key == len(labelValues)-1 {
labels += labelTpl.SetContent(template.HTML(label)).GetContent()
} else {
labels += labelTpl.SetContent(template.HTML(label)).GetContent()
}
}
return labels
})
info.AddField("Status", "status", db.Varchar).
FieldDisplay(func(value types.FieldModel) interface{} {
uuid := value.Row["uuid"].(string)
return manager.GetNodeStatus(uuid)
})
info.AddField("Created at", "created_at", db.Datetime).
FieldDisplay(func(model types.FieldModel) interface{} {
t, err := time.Parse(time.RFC3339, model.Value)
if err != nil {
return model.Value
}
return t.Format("2006-01-02 15:04:05")
}).
FieldFilterable(types.FilterType{FormType: form.DatetimeRange}).
FieldSortable()
info.AddField("Updated at", "updated_at", db.Datetime).
FieldDisplay(func(model types.FieldModel) interface{} {
t, err := time.Parse(time.RFC3339, model.Value)
if err != nil {
return model.Value
}
return t.Format("2006-01-02 15:04:05")
}).
FieldFilterable(types.FilterType{FormType: form.DatetimeRange}).
FieldSortable()
info.SetGetDataFn(func(param parameter.Parameters) ([]map[string]interface{}, int) {
filters := make(map[string][]string, len(param.Fields))
listFilters := make(map[string][]string, len(param.Fields)+2)
listFilters["offset"] = []string{strconv.Itoa((param.PageInt - 1) * param.PageSizeInt)}
listFilters["limit"] = []string{param.PageSize}
for key, values := range param.Fields {
if key == "__pk" {
key = "uuid"
} else {
if strings.HasPrefix(key, "__") {
continue
}
key = strings.TrimSuffix(key, "__goadmin")
}
filters[key] = values
listFilters[key] = values
}
if param.SortField != "" {
if param.SortType == "asc" {
listFilters["sort_asc"] = []string{param.SortField}
} else {
listFilters["sort_desc"] = []string{param.SortField}
}
}
nodes, err := manager.GetNodes(listFilters)
if err != nil {
logger.Error(err)
return nil, 0
}
count, err := manager.GetNodesCount(filters)
if err != nil {
logger.Error(err)
return nil, 0
}
result := make([]map[string]interface{}, 0, len(nodes))
for _, node := range nodes {
var data map[string]interface{}
rawData, _ := json.Marshal(node)
json.Unmarshal(rawData, &data)
result = append(result, data)
}
return result, count
})
info.SetDeleteFn(func(ids []string) error {
for _, uuid := range ids {
if _, err := manager.DeleteNode(uuid); err != nil {
return err
}
}
return nil
})
info.SetTable("nodes").SetTitle("Nodes").SetDescription("Nodes")
defaultUUID, _ := uuid.NewV4()
formList := nodeTable.GetForm()
formList.AddField("UUID", "uuid", db.Varchar, form.Text).
FieldMust().
FieldNotAllowEdit().
FieldDefault(defaultUUID.String())
formList.AddField("Name", "name", db.Varchar, form.Text).
FieldMust()
formList.AddField("Squads", "squad_ids", db.Varchar, form.Select).
FieldMust().
FieldOptions(squadOptions).
FieldDisableWhenUpdate()
formList.SetInsertFn(func(values mForm.Values) (err error) {
squadIDs := make([]int, len(values["squad_ids[]"]))
for i, rawSquadID := range values["squad_ids[]"] {
squadID, err := strconv.Atoi(rawSquadID)
if err != nil {
return err
}
squadIDs[i] = squadID
}
_, err = manager.CreateNode(CM.NodeCreate{
UUID: values.Get("uuid"),
Name: values.Get("name"),
SquadIDs: squadIDs,
})
return
})
formList.SetUpdateFn(func(values mForm.Values) (err error) {
uuid := values.Get("uuid")
_, err = manager.UpdateNode(uuid, CM.NodeUpdate{
Name: values.Get("name"),
})
return
})
formList.SetTable("nodes").SetTitle("Nodes").SetDescription("Nodes")
return
}
}

View File

@@ -0,0 +1,164 @@
package tables
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"github.com/GoAdminGroup/go-admin/context"
"github.com/GoAdminGroup/go-admin/modules/db"
mForm "github.com/GoAdminGroup/go-admin/plugins/admin/modules/form"
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/parameter"
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
"github.com/GoAdminGroup/go-admin/template/types"
"github.com/GoAdminGroup/go-admin/template/types/form"
"github.com/go-playground/validator/v10"
"github.com/sagernet/sing-box/log"
CM "github.com/sagernet/sing-box/service/manager/constant"
)
func SquadTableFactory(manager CM.Manager, logger log.Logger) func(ctx *context.Context) (squadTable table.Table) {
return func(ctx *context.Context) (squadTable table.Table) {
squadTable = table.NewDefaultTable(ctx, table.Config{
CanAdd: true,
Editable: true,
Deletable: true,
Exportable: true,
PrimaryKey: table.PrimaryKey{
Type: db.Int,
Name: table.DefaultPrimaryKeyName,
},
})
info := squadTable.GetInfo().SetFilterFormLayout(form.LayoutFilter)
info.AddField("ID", "id", db.Int).
FieldSortable()
info.AddField("Name", "name", db.Varchar).
FieldFilterable().
FieldSortable()
info.AddField("Created At", "created_at", db.Datetime).
FieldDisplay(func(model types.FieldModel) interface{} {
t, err := time.Parse(time.RFC3339, model.Value)
if err != nil {
return model.Value
}
return t.Format("2006-01-02 15:04:05")
}).
FieldSortable().
FieldFilterable(types.FilterType{FormType: form.DatetimeRange})
info.AddField("Updated At", "updated_at", db.Datetime).
FieldDisplay(func(model types.FieldModel) interface{} {
t, err := time.Parse(time.RFC3339, model.Value)
if err != nil {
return model.Value
}
return t.Format("2006-01-02 15:04:05")
}).
FieldSortable().
FieldFilterable(types.FilterType{FormType: form.DatetimeRange})
info.SetGetDataFn(func(param parameter.Parameters) ([]map[string]interface{}, int) {
filters := make(map[string][]string, len(param.Fields))
listFilters := make(map[string][]string, len(param.Fields)+2)
listFilters["offset"] = []string{strconv.Itoa((param.PageInt - 1) * param.PageSizeInt)}
listFilters["limit"] = []string{param.PageSize}
for key, values := range param.Fields {
if key == "__pk" {
key = "pk"
} else if strings.HasPrefix(key, "__") {
continue
} else {
key = strings.TrimSuffix(key, "__goadmin")
}
filters[key] = values
listFilters[key] = values
}
if param.SortField != "" {
if param.SortType == "asc" {
listFilters["sort_asc"] = []string{param.SortField}
} else {
listFilters["sort_desc"] = []string{param.SortField}
}
}
squads, err := manager.GetSquads(listFilters)
if err != nil {
logger.Error(err)
return nil, 0
}
count, err := manager.GetSquadsCount(filters)
if err != nil {
logger.Error(err)
return nil, 0
}
result := make([]map[string]interface{}, 0, len(squads))
for _, squad := range squads {
var data map[string]interface{}
rawData, _ := json.Marshal(squad)
json.Unmarshal(rawData, &data)
result = append(result, data)
}
return result, count
})
info.SetDeleteFn(func(ids []string) error {
for _, id := range ids {
intID, err := strconv.Atoi(id)
if err != nil {
return err
}
if _, err := manager.DeleteSquad(intID); err != nil {
return err
}
}
return nil
})
info.SetTable("squads").SetTitle("Squads").SetDescription("Squads")
formList := squadTable.GetForm()
formList.AddField("ID", "id", db.Int, form.Default).
FieldNotAllowAdd().
FieldNotAllowEdit()
formList.AddField("Name", "name", db.Varchar, form.Text).
FieldMust()
formList.SetInsertFn(func(values mForm.Values) (err error) {
_, err = manager.CreateSquad(CM.SquadCreate{
Name: values.Get("name"),
})
if err != nil {
if ve, ok := err.(validator.ValidationErrors); ok {
var errors []string
for _, e := range ve {
switch e.Tag() {
case "required":
errors = append(errors, e.StructField()+": required field missing")
default:
errors = append(errors, e.StructField()+": invalid request")
}
}
err = fmt.Errorf("%s", strings.Join(errors, "<br>"))
}
}
return
})
formList.SetUpdateFn(func(values mForm.Values) (err error) {
id, err := strconv.Atoi(values.Get("id"))
if err != nil {
return err
}
_, err = manager.UpdateSquad(id, CM.SquadUpdate{
Name: values.Get("name"),
})
return
})
formList.SetTable("squads").SetTitle("Squads").SetDescription("Squads")
return
}
}

View File

@@ -0,0 +1,282 @@
package tables
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"github.com/GoAdminGroup/go-admin/context"
"github.com/GoAdminGroup/go-admin/modules/db"
mForm "github.com/GoAdminGroup/go-admin/plugins/admin/modules/form"
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/parameter"
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
"github.com/GoAdminGroup/go-admin/template"
"github.com/GoAdminGroup/go-admin/template/types"
"github.com/GoAdminGroup/go-admin/template/types/form"
"github.com/go-playground/validator/v10"
"github.com/sagernet/sing-box/log"
CM "github.com/sagernet/sing-box/service/manager/constant"
)
func UserTableFactory(manager CM.Manager, logger log.Logger) func(ctx *context.Context) (userTable table.Table) {
return func(ctx *context.Context) (userTable table.Table) {
userTable = table.NewDefaultTable(ctx, table.Config{
CanAdd: true,
Editable: true,
Deletable: true,
Exportable: true,
PrimaryKey: table.PrimaryKey{
Type: db.Int,
Name: table.DefaultPrimaryKeyName,
},
})
squads, err := manager.GetSquads(map[string][]string{})
if err != nil {
return nil
}
squadsByID := make(map[int]string, len(squads))
squadOptions := make(types.FieldOptions, len(squads))
for i, squad := range squads {
squadsByID[squad.ID] = squad.Name
squadOptions[i] = types.FieldOption{
Text: squad.Name,
Value: strconv.Itoa(squad.ID),
}
}
info := userTable.GetInfo().SetFilterFormLayout(form.LayoutFilter)
info.AddField("ID", "id", db.Int).
FieldSortable()
info.AddField("Squads", "squad_ids", db.Varchar).
FieldDisplay(func(model types.FieldModel) interface{} {
values := model.Row["squad_ids"].([]interface{})
labels := template.HTML("")
labelTpl := label(ctx).SetType("success")
labelValues := make([]string, len(values))
for i, squadID := range values {
labelValues[i] = squadsByID[int(squadID.(float64))]
}
for key, label := range labelValues {
if key == len(labelValues)-1 {
labels += labelTpl.SetContent(template.HTML(label)).GetContent()
} else {
labels += labelTpl.SetContent(template.HTML(label)).GetContent()
}
}
return labels
})
info.AddField("Username", "username", db.Varchar).
FieldFilterable().
FieldSortable()
info.AddField("Type", "type", db.Varchar).
FieldFilterable(
types.FilterType{
FormType: form.SelectSingle,
Options: types.FieldOptions{
{Text: "Hysteria", Value: "hysteria"},
{Text: "Hysteria2", Value: "hysteria2"},
{Text: "Trojan", Value: "trojan"},
{Text: "TUIC", Value: "tuic"},
{Text: "VLESS", Value: "vless"},
{Text: "VMess", Value: "vmess"},
},
},
).
FieldSortable()
info.AddField("Inbound", "inbound", db.Varchar).FieldFilterable().
FieldSortable()
info.AddField("Created at", "created_at", db.Datetime).
FieldDisplay(func(model types.FieldModel) interface{} {
t, err := time.Parse(time.RFC3339, model.Value)
if err != nil {
return model.Value
}
return t.Format("2006-01-02 15:04:05")
}).
FieldFilterable(types.FilterType{FormType: form.DatetimeRange}).
FieldSortable()
info.AddField("Updated at", "updated_at", db.Datetime).
FieldDisplay(func(model types.FieldModel) interface{} {
t, err := time.Parse(time.RFC3339, model.Value)
if err != nil {
return model.Value
}
return t.Format("2006-01-02 15:04:05")
}).
FieldFilterable(types.FilterType{FormType: form.DatetimeRange}).
FieldSortable()
info.SetGetDataFn(func(param parameter.Parameters) ([]map[string]interface{}, int) {
filters := make(map[string][]string, len(param.Fields))
listFilters := make(map[string][]string, len(param.Fields)+2)
listFilters["offset"] = []string{strconv.Itoa((param.PageInt - 1) * param.PageSizeInt)}
listFilters["limit"] = []string{param.PageSize}
for key, values := range param.Fields {
if key == "__pk" {
key = "pk"
} else {
if strings.HasPrefix(key, "__") {
continue
}
key = strings.TrimSuffix(key, "__goadmin")
}
filters[key] = values
listFilters[key] = values
}
if param.SortField != "" {
if param.SortType == "asc" {
listFilters["sort_asc"] = []string{param.SortField}
} else {
listFilters["sort_desc"] = []string{param.SortField}
}
}
users, err := manager.GetUsers(listFilters)
if err != nil {
logger.Error(err)
return nil, 0
}
count, err := manager.GetUsersCount(filters)
if err != nil {
logger.Error(err)
return nil, 0
}
result := make([]map[string]interface{}, 0, len(users))
for _, user := range users {
var data map[string]interface{}
rawData, _ := json.Marshal(user)
json.Unmarshal(rawData, &data)
result = append(result, data)
}
return result, count
})
info.SetDeleteFn(func(ids []string) error {
for _, id := range ids {
value, err := strconv.Atoi(id)
if err != nil {
return err
}
if _, err := manager.DeleteUser(value); err != nil {
return err
}
}
return nil
})
info.SetTable("users").SetTitle("Users").SetDescription("Users")
formList := userTable.GetForm()
formList.AddField("ID", "id", db.Int, form.Default).
FieldNotAllowEdit().
FieldNotAllowAdd()
formList.AddField("Squads", "squad_ids", db.Varchar, form.Select).
FieldMust().
FieldOptions(squadOptions).
FieldDisableWhenUpdate()
formList.AddField("Username", "username", db.Varchar, form.Text).
FieldMust().
FieldDisplayButCanNotEditWhenUpdate()
formList.AddField("Type", "type", db.Varchar, form.SelectSingle).
FieldMust().
FieldDisplayButCanNotEditWhenUpdate().
FieldOptions(types.FieldOptions{
{Text: "Hysteria", Value: "hysteria"},
{Text: "Hysteria2", Value: "hysteria2"},
{Text: "Trojan", Value: "trojan"},
{Text: "TUIC", Value: "tuic"},
{Text: "VLESS", Value: "vless"},
{Text: "VMess", Value: "vmess"},
}).
FieldOnChooseOptionsHide([]string{""}, "inbound").
FieldOnChooseOptionsHide([]string{"", "hysteria", "hysteria2", "shadowsocks", "trojan", "tuic"}, "uuid").
FieldOnChooseOptionsHide([]string{"", "vless", "vmess"}, "password").
FieldOnChooseOptionsHide([]string{"", "hysteria", "hysteria2", "shadowsocks", "trojan", "tuic", "vmess"}, "flow").
FieldOnChooseOptionsHide([]string{"", "hysteria", "hysteria2", "shadowsocks", "trojan", "tuic", "vless"}, "alter_id")
formList.AddField("Inbound", "inbound", db.Varchar, form.Text).
FieldMust().
FieldDisplayButCanNotEditWhenUpdate().
FieldOptionInitFn(func(val types.FieldModel) types.FieldOptions {
return types.FieldOptions{
{Value: val.Value, Text: val.Value, Selected: true},
}
})
formList.AddField("UUID", "uuid", db.Varchar, form.Text)
formList.AddField("Password", "password", db.Varchar, form.Text)
formList.AddField("Flow", "flow", db.Varchar, form.SelectSingle).
FieldOptions(types.FieldOptions{
{Text: "xtls-rprx-vision", Value: "xtls-rprx-vision"},
})
formList.AddField("Alter ID", "alter_id", db.Varchar, form.Number).
FieldDefault("0")
formList.SetInsertFn(func(values mForm.Values) (err error) {
squadIDs := make([]int, len(values["squad_ids[]"]))
for i, rawSquadID := range values["squad_ids[]"] {
squadID, err := strconv.Atoi(rawSquadID)
if err != nil {
return err
}
squadIDs[i] = squadID
}
var alterId int
if value := values.Get("alter_id"); value != "" {
alterId, err = strconv.Atoi(value)
if err != nil {
return err
}
}
_, err = manager.CreateUser(CM.UserCreate{
SquadIDs: squadIDs,
Username: values.Get("username"),
Type: values.Get("type"),
Inbound: values.Get("inbound"),
UUID: values.Get("uuid"),
Password: values.Get("password"),
Flow: values.Get("flow"),
AlterID: alterId,
})
if err != nil {
if ve, ok := err.(validator.ValidationErrors); ok {
var errors []string
for _, e := range ve {
switch e.Tag() {
case "required":
errors = append(errors, e.StructField()+": required field missing")
case "uuid4":
errors = append(errors, e.StructField()+": invalid UUID")
default:
errors = append(errors, e.StructField()+": invalid request")
}
}
err = fmt.Errorf("%s", strings.Join(errors, "<br>"))
}
}
return
})
formList.SetUpdateFn(func(values mForm.Values) (err error) {
id, err := strconv.Atoi(values.Get("id"))
if err != nil {
return err
}
var alterId int
if value := values.Get("alter_id"); value != "" {
alterId, err = strconv.Atoi(value)
if err != nil {
return err
}
}
_, err = manager.UpdateUser(id, CM.UserUpdate{
UUID: values.Get("uuid"),
Password: values.Get("password"),
Flow: values.Get("flow"),
AlterID: alterId,
})
return
})
formList.SetTable("users").SetTitle("Users").SetDescription("Users")
return
}
}

View File

@@ -0,0 +1,164 @@
package constant
import "time"
type Squad struct {
ID int `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
CreatedAt time.Time `json:"created_at" validate:"required"`
UpdatedAt time.Time `json:"updated_at" validate:"required"`
}
type SquadCreate struct {
Name string `json:"name" validate:"required"`
}
type SquadUpdate struct {
Name string `json:"name" validate:"required"`
}
type Node struct {
UUID string `json:"uuid" validate:"required,uuid4"`
Name string `json:"name" validate:"required"`
SquadIDs []int `json:"squad_ids" validate:"required"`
CreatedAt time.Time `json:"created_at" validate:"required"`
UpdatedAt time.Time `json:"updated_at" validate:"required"`
}
type NodeCreate struct {
UUID string `json:"uuid" validate:"required,uuid4"`
Name string `json:"name" validate:"required"`
SquadIDs []int `json:"squad_ids" validate:"required"`
}
type NodeUpdate struct {
Name string `json:"name" validate:"required"`
}
type BaseNode struct {
UUID string `json:"uuid" validate:"required,uuid4"`
Name string `json:"name" validate:"required"`
}
type User struct {
ID int `json:"id" validate:"required"`
SquadIDs []int `json:"squad_ids" validate:"required"`
Username string `json:"username" validate:"required"`
Type string `json:"type" validate:"required"`
Inbound string `json:"inbound" validate:"required"`
UUID string `json:"uuid" validate:"required"`
Password string `json:"password" validate:"required"`
Flow string `json:"flow" validate:"required"`
AlterID int `json:"alter_id" validate:"required"`
CreatedAt time.Time `json:"created_at" validate:"required"`
UpdatedAt time.Time `json:"updated_at" validate:"required"`
}
type UserCreate struct {
SquadIDs []int `json:"squad_ids" validate:"required"`
Username string `json:"username" validate:"required"`
Type string `json:"type" validate:"required,oneof=hysteria hysteria2 trojan tuic vless vmess"`
Inbound string `json:"inbound" validate:"required"`
UUID string `json:"uuid" validate:"omitempty,uuid4"`
Password string `json:"password" validate:"omitempty"`
Flow string `json:"flow" validate:"omitempty"`
AlterID int `json:"alter_id" validate:"omitempty"`
}
type UserUpdate struct {
UUID string `json:"uuid" validate:"omitempty,uuid4"`
Password string `json:"password" validate:"omitempty"`
Flow string `json:"flow" validate:"omitempty"`
AlterID int `json:"alter_id" validate:"omitempty"`
}
type BaseUser struct {
UUID string `json:"uuid" validate:"omitempty,uuid4"`
Password string `json:"password" validate:"omitempty"`
Flow string `json:"flow" validate:"omitempty"`
AlterID int `json:"alter_id" validate:"omitempty"`
}
type ConnectionLimiter struct {
ID int `json:"id" validate:"required"`
SquadIDs []int `json:"squad_ids" validate:"required"`
Username string `json:"username" validate:"required"`
Outbound string `json:"outbound" validate:"required"`
Strategy string `json:"strategy" validate:"required,oneof=connection"`
ConnectionType string `json:"connection_type" validate:"omitempty,oneof=hwid mux ip"`
LockType string `json:"lock_type" validate:"omitempty,oneof=manager"`
Count uint32 `json:"count" validate:"required"`
CreatedAt time.Time `json:"created_at" validate:"required"`
UpdatedAt time.Time `json:"updated_at" validate:"required"`
}
type ConnectionLimiterCreate struct {
SquadIDs []int `json:"squad_ids" validate:"required"`
Username string `json:"username" validate:"required"`
Outbound string `json:"outbound" validate:"required"`
Strategy string `json:"strategy" validate:"required,oneof=connection"`
ConnectionType string `json:"type" validate:"omitempty,oneof=hwid mux ip"`
LockType string `json:"lock_type" validate:"omitempty,oneof=manager"`
Count uint32 `json:"count" validate:"required"`
}
type ConnectionLimiterUpdate struct {
Username string `json:"username" validate:"required"`
Outbound string `json:"outbound" validate:"required"`
Strategy string `json:"strategy" validate:"required,oneof=connection"`
ConnectionType string `json:"type" validate:"omitempty,oneof=hwid mux ip"`
LockType string `json:"lock_type" validate:"omitempty,oneof=manager"`
Count uint32 `json:"count" validate:"required"`
}
type BaseConnectionLimiter struct {
Username string `json:"username" validate:"required"`
Outbound string `json:"outbound" validate:"required"`
Strategy string `json:"strategy" validate:"required,oneof=connection"`
ConnectionType string `json:"type" validate:"omitempty,oneof=hwid mux ip"`
LockType string `json:"lock_type" validate:"omitempty,oneof=manager"`
Count uint32 `json:"count" validate:"required"`
}
type BandwidthLimiter struct {
ID int `json:"id" validate:"required"`
SquadIDs []int `json:"squad_ids" validate:"required"`
Username string `json:"username" validate:"required"`
Outbound string `json:"outbound" validate:"required"`
Strategy string `json:"strategy" validate:"required"`
Mode string `json:"mode" validate:"required"`
ConnectionType string `json:"connection_type" validate:"omitempty"`
Speed string `json:"speed" validate:"required"`
RawSpeed uint64 `json:"raw_speed" validate:"required"`
CreatedAt time.Time `json:"created_at" validate:"required"`
UpdatedAt time.Time `json:"updated_at" validate:"required"`
}
type BandwidthLimiterCreate struct {
SquadIDs []int `json:"squad_ids" validate:"required"`
Username string `json:"username" validate:"required"`
Outbound string `json:"outbound" validate:"required"`
Strategy string `json:"strategy" validate:"required,oneof=global connection"`
Mode string `json:"mode" validate:"required"`
ConnectionType string `json:"connection_type" validate:"omitempty"`
Speed string `json:"speed" validate:"required"`
}
type BandwidthLimiterUpdate struct {
Username string `json:"username" validate:"required"`
Outbound string `json:"outbound" validate:"required"`
Strategy string `json:"strategy" validate:"required,oneof=global connection"`
Mode string `json:"mode" validate:"required"`
ConnectionType string `json:"connection_type" validate:"omitempty"`
Speed string `json:"speed" validate:"required"`
}
type BaseBandwidthLimiter struct {
Username string `json:"username" validate:"required"`
Outbound string `json:"outbound" validate:"required"`
Strategy string `json:"strategy" validate:"required,oneof=global connection"`
Mode string `json:"mode" validate:"required"`
ConnectionType string `json:"connection_type" validate:"omitempty"`
Speed string `json:"speed" validate:"required"`
RawSpeed uint64 `json:"raw_speed" validate:"required"`
}

View File

@@ -0,0 +1,5 @@
package constant
import E "github.com/sagernet/sing/common/exceptions"
var ErrNotFound = E.New("not found")

View File

@@ -0,0 +1,48 @@
package constant
type NodeManager interface {
AddNode(id string, node ConnectedNode) error
AcquireLock(limiterId int, id string) (string, error)
RefreshLock(limiterId int, id string, handleId string) error
ReleaseLock(limiterId int, id string, handleId string) error
}
type Manager interface {
NodeManager
CreateSquad(user SquadCreate) (Squad, error)
GetSquads(filters map[string][]string) ([]Squad, error)
GetSquadsCount(filters map[string][]string) (int, error)
GetSquad(id int) (Squad, error)
UpdateSquad(id int, user SquadUpdate) (Squad, error)
DeleteSquad(id int) (Squad, error)
CreateNode(node NodeCreate) (Node, error)
GetNodes(filters map[string][]string) ([]Node, error)
GetNodesCount(filters map[string][]string) (int, error)
GetNode(uuid string) (Node, error)
GetNodeStatus(uuid string) string
UpdateNode(uuid string, node NodeUpdate) (Node, error)
DeleteNode(uuid string) (Node, error)
CreateUser(user UserCreate) (User, error)
GetUsers(filters map[string][]string) ([]User, error)
GetUsersCount(filters map[string][]string) (int, error)
GetUser(id int) (User, error)
UpdateUser(id int, user UserUpdate) (User, error)
DeleteUser(id int) (User, error)
CreateBandwidthLimiter(limiter BandwidthLimiterCreate) (BandwidthLimiter, error)
GetBandwidthLimiters(filters map[string][]string) ([]BandwidthLimiter, error)
GetBandwidthLimitersCount(filters map[string][]string) (int, error)
GetBandwidthLimiter(id int) (BandwidthLimiter, error)
UpdateBandwidthLimiter(id int, limiter BandwidthLimiterUpdate) (BandwidthLimiter, error)
DeleteBandwidthLimiter(id int) (BandwidthLimiter, error)
CreateConnectionLimiter(limiter ConnectionLimiterCreate) (ConnectionLimiter, error)
GetConnectionLimiters(filters map[string][]string) ([]ConnectionLimiter, error)
GetConnectionLimitersCount(filters map[string][]string) (int, error)
GetConnectionLimiter(id int) (ConnectionLimiter, error)
UpdateConnectionLimiter(id int, limiter ConnectionLimiterUpdate) (ConnectionLimiter, error)
DeleteConnectionLimiter(id int) (ConnectionLimiter, error)
}

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