Add admin panel, manager, node_manager, bandwidth limiter, connection limiter, bonding, failover, vless encryption, mkcp transport

This commit is contained in:
Sergei Maklagin
2026-02-26 22:44:31 +03:00
parent 287fe834db
commit c0aa3480c5
115 changed files with 12582 additions and 301 deletions

View File

@@ -350,7 +350,7 @@ jobs:
mkdir clients/android/app/libs
cp libbox.aar clients/android/app/libs
cd clients/android
./gradlew :app:assemblePlayRelease :app:assembleOtherRelease
./gradlew :app:assemblePlayRelease
env:
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}

View File

@@ -31,6 +31,54 @@ builds:
- 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
@@ -51,8 +99,6 @@ builds:
- with_tailscale
env:
- CGO_ENABLED=0
- GOROOT={{ .Env.GOPATH }}/go_legacy
tool: "{{ .Env.GOPATH }}/go_legacy/bin/go"
targets:
- windows_amd64_v1
- windows_386
@@ -104,91 +150,25 @@ archives:
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 }}'
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'
nfpms:
- id: package
package_name: sing-box
file_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 }}'
builds:
- main
homepage: https://sing-box.sagernet.org/
maintainer: nekohasekai <contact-git@sekai.icu>
description: The universal proxy platform.
license: GPLv3 or later
formats:
- deb
- rpm
- archlinux
# - apk
# - ipk
priority: extra
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: "config|noreplace"
- src: release/config/sing-box.service
dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service
- src: release/config/sing-box.sysusers
dst: /usr/lib/sysusers.d/sing-box.conf
- src: release/config/sing-box.rules
dst: /usr/share/polkit-1/rules.d/sing-box.rules
- src: release/config/sing-box-split-dns.xml
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
- src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash
- src: release/completions/sing-box.fish
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
- src: release/completions/sing-box.zsh
dst: /usr/share/zsh/site-functions/_sing-box
- src: LICENSE
dst: /usr/share/licenses/sing-box/LICENSE
deb:
signature:
key_file: "{{ .Env.NFPM_KEY_PATH }}"
fields:
Bugs: https://github.com/SagerNet/sing-box/issues
rpm:
signature:
key_file: "{{ .Env.NFPM_KEY_PATH }}"
overrides:
apk:
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: config
- src: release/config/sing-box.initd
dst: /etc/init.d/sing-box
- src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash
- src: release/completions/sing-box.fish
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
- src: release/completions/sing-box.zsh
dst: /usr/share/zsh/site-functions/_sing-box
- src: LICENSE
dst: /usr/share/licenses/sing-box/LICENSE
ipk:
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: config
- src: release/config/openwrt.init
dst: /etc/init.d/sing-box
- src: release/config/openwrt.conf
dst: /etc/config/sing-box
source:
enabled: false
name_template: '{{ .ProjectName }}-{{ .Version }}.source'
@@ -200,8 +180,8 @@ signs:
- artifacts: checksum
release:
github:
owner: SagerNet
name: sing-box
owner: shtorm-7
name: sing-box-extended
draft: true
prerelease: auto
mode: replace
@@ -209,5 +189,3 @@ release:
- archive
- package
skip_upload: true
partial:
by: target

View File

@@ -1,6 +1,6 @@
NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD)
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_manager,with_admin_panel
GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH)
@@ -64,14 +64,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
@@ -86,7 +82,7 @@ update_android_version:
go run ./cmd/internal/update_android_version
build_android:
cd ../sing-box-for-android && ./gradlew :app:clean :app:assemblePlayRelease :app:assembleOtherRelease && ./gradlew --stop
cd ../sing-box-for-android && ./gradlew :app:clean :app:assemblePlayRelease && ./gradlew --stop
upload_android:
mkdir -p dist/release_android
@@ -95,7 +91,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

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

@@ -21,6 +21,7 @@ type Outbound 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

@@ -159,6 +159,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 (

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

View File

@@ -1,40 +1,50 @@
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"
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"
)
const (
TypeFailover = "failover"
TypeSelector = "selector"
TypeURLTest = "urltest"
)

View File

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

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": [

92
go.mod
View File

@@ -1,20 +1,25 @@
module github.com/sagernet/sing-box
go 1.24.4
toolchain go1.24.6
go 1.25
require (
github.com/GoAdminGroup/go-admin v1.2.26
github.com/GoAdminGroup/themes v0.0.48
github.com/anytls/sing-anytls v0.0.11
github.com/caddyserver/certmagic v0.23.0
github.com/coder/websocket v1.8.13
github.com/cretz/bine v0.2.0
github.com/enfein/mieru/v3 v3.17.1
github.com/go-chi/chi v1.5.5
github.com/go-chi/chi/v5 v5.2.2
github.com/go-chi/render v1.0.3
github.com/go-playground/validator/v10 v10.30.1
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466
github.com/gofrs/uuid/v5 v5.3.2
github.com/golang-migrate/migrate/v4 v4.19.1
github.com/huandu/go-sqlbuilder v1.38.1
github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f
github.com/lib/pq v1.10.9
github.com/libdns/alidns v1.0.5-libdns.v1.beta1
github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6
github.com/logrusorgru/aurora v2.0.3+incompatible
@@ -30,7 +35,7 @@ require (
github.com/sagernet/gomobile v0.1.8
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3
github.com/sagernet/sing v0.7.13
github.com/sagernet/sing v0.7.14
github.com/sagernet/sing-mux v0.3.3
github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb
github.com/sagernet/sing-shadowsocks v0.2.8
@@ -38,47 +43,59 @@ require (
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
github.com/sagernet/sing-tun v0.7.3
github.com/sagernet/sing-vmess v0.2.7
github.com/sagernet/smux v1.5.34-mod.2
github.com/sagernet/smux v1.5.50-sing-box-mod.1
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2
github.com/sagernet/wireguard-go v0.0.1-beta.7
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.18.0
github.com/vishvananda/netns v0.0.5
go.uber.org/zap v1.27.0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.41.0
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6
golang.org/x/mod v0.27.0
golang.org/x/net v0.43.0
golang.org/x/sys v0.35.0
golang.org/x/time v0.11.0
golang.org/x/crypto v0.47.0
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
golang.org/x/mod v0.32.0
golang.org/x/net v0.49.0
golang.org/x/sys v0.40.0
golang.org/x/time v0.12.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
google.golang.org/grpc v1.73.0
google.golang.org/protobuf v1.36.6
google.golang.org/grpc v1.78.0
google.golang.org/protobuf v1.36.11
howett.net/plist v1.0.1
)
require (
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/nxadm/tail v1.4.11 // indirect
github.com/zeebo/assert v1.3.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
)
require (
github.com/AdguardTeam/golibs v0.32.7 // indirect
github.com/ameshkov/dnscrypt/v2 v2.4.0
github.com/ameshkov/dnsstamps v1.0.3 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/tools v0.41.0 // indirect
)
//replace github.com/sagernet/sing => ../sing
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/360EntSecGroup-Skylar/excelize v1.4.1 // 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
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
@@ -89,12 +106,19 @@ require (
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // 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.11.1 // 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-20250103232110-6a9a0fde9288 // 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 v1.0.0 // 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
@@ -104,19 +128,31 @@ require (
github.com/gorilla/securecookie v1.1.2 // 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/illarion/gonotify/v2 v2.0.3 // 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/pgx/v5 v5.8.0
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/libdns/libdns v1.1.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
github.com/mdlayher/sdnotify v1.0.0 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // 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/patrickmn/go-cache v2.1.0+incompatible
github.com/pierrec/lz4/v4 v4.1.25 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus-community/pro-bing v0.4.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
@@ -124,6 +160,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.6 // 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/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 // indirect
@@ -134,24 +171,33 @@ require (
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
github.com/tevino/abool v1.2.0 // 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/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
golang.org/x/term v0.34.0 // indirect
golang.org/x/term v0.39.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-20250324211829-b45e905df463 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // 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
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.1-beta.7-extended-1.1.0
replace github.com/sagernet/tailscale => github.com/shtorm-7/tailscale v1.80.3-sing-box-1.12-mod.2-extended-1.0.0
replace github.com/sagernet/sing-mux => github.com/shtorm-7/sing-mux v0.3.4-extended-1.0.0
replace github.com/sagernet/sing-dns => github.com/shtorm-7/sing-dns v0.4.6-extended-1.0.0
replace github.com/ameshkov/dnscrypt/v2 => github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0

407
go.sum
View File

@@ -1,19 +1,45 @@
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=
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=
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
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/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU=
@@ -24,11 +50,17 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
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.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
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=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -37,32 +69,74 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
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/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
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/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/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/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.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=
github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
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.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 h1:KbX3Z3CgiYlbaavUq3Cj9/MjpO+88S7/AGXzynVDv84=
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
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=
@@ -71,46 +145,107 @@ github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
github.com/gofrs/uuid/v5 v5.3.2/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 v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.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/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 h1:fiJdrgVBkjZ5B1HJ2WQwNOaXB+QyYcNXTA3t1XYLz0M=
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
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.38.1 h1:kajV1CFJQIrJgyTONhQFheJLRFnwDmTnU6e3CfFP5GQ=
github.com/huandu/go-sqlbuilder v1.38.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/illarion/gonotify/v2 v2.0.3 h1:B6+SKPo/0Sw8cRJh1aLzNEeNVFfzE3c6N+o+vyxM+9A=
github.com/illarion/gonotify/v2 v2.0.3/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE=
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-20250417080101-5f8cf70e8c5f h1:dd33oobuIv9PcBVqvbEiCXEbNTomOHyj3WFuC5YiPRU=
github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f/go.mod h1:zhFlBeJssZ1YBCMZ5Lzu1pX4vhftDvU10WUVb1uXKtM=
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/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
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/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
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.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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/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/alidns v1.0.5-libdns.v1.beta1 h1:txHK7UxDed3WFBDjrTZPuMn8X+WmhjBTTAMW5xdy5pQ=
github.com/libdns/alidns v1.0.5-libdns.v1.beta1/go.mod h1:ystHmPwcGoWjPrGpensQSMY9VoCx4cpR2hXNlwk9H/g=
github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6 h1:3MGrVWs2COjMkQR17oUw1zMIPbm2YAzxDC3oGVZvQs8=
@@ -120,6 +255,14 @@ github.com/libdns/libdns v1.1.0 h1:9ze/tWvt7Df6sbhOJRB8jT33GHEHpEQXdtkE3hPthbU=
github.com/libdns/libdns v1.1.0/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/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
@@ -138,19 +281,65 @@ github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0=
github.com/miekg/dns v1.1.67/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
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.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU=
github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=
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/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/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.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
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=
@@ -172,11 +361,8 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3 h1:ySqffGm82rPqI1TUPqmtHIYd12pfEGScygnOxjTL56w=
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
github.com/sagernet/sing v0.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.7.13 h1:XNYgd8e3cxMULs/LLJspdn/deHrnPWyrrglNHeCUAYM=
github.com/sagernet/sing v0.7.13/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.3 h1:YFgt9plMWzH994BMZLmyKL37PdIVaIilwP0Jg+EcLfw=
github.com/sagernet/sing-mux v0.3.3/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=
github.com/sagernet/sing v0.7.14 h1:5QQRDCUvYNOMyVp3LuK/hYEBAIv0VsbD3x/l9zH467s=
github.com/sagernet/sing v0.7.14/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb h1:5Wx3XeTiKrrrcrAky7Hc1bO3CGxrvho2Vu5b/adlEIM=
github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb/go.mod h1:evP1e++ZG8TJHVV5HudXV4vWeYzGfCdF4HwSJZcdqkI=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
@@ -189,31 +375,34 @@ github.com/sagernet/sing-tun v0.7.3 h1:MFnAir+l24ElEyxdfwtY8mqvUUL9nPnL9TDYLkOmV
github.com/sagernet/sing-tun v0.7.3/go.mod h1:pUEjh9YHQ2gJT6Lk0TYDklh3WJy7lz+848vleGM3JPM=
github.com/sagernet/sing-vmess v0.2.7 h1:2ee+9kO0xW5P4mfe6TYVWf9VtY8k1JhNysBqsiYj0sk=
github.com/sagernet/sing-vmess v0.2.7/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
github.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4=
github.com/sagernet/smux v1.5.34-mod.2/go.mod h1:0KW0+R+ycvA2INW4gbsd7BNyg+HEfLIAxa5N02/28Zc=
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/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.80.3-sing-box-1.12-mod.2-extended-1.0.0 h1:Yp4dIRwiwLda9JXyGMHkfYRr2r01NarkzsNd/oi10dk=
github.com/shtorm-7/tailscale v1.80.3-sing-box-1.12-mod.2-extended-1.0.0/go.mod h1:+znUAXWwgcgza5mb5do8j9RC95rpY9lbSc/TyEyCGa4=
github.com/shtorm-7/wireguard-go v0.0.1-beta.7-extended-1.1.0 h1:bTmx3NiEeH7mdgsifyNUxIEAA0wokRMSm8iS/hln6n0=
github.com/shtorm-7/wireguard-go v0.0.1-beta.7-extended-1.1.0/go.mod h1:DHxMTUaBGHP3tf8nJ/N8AkcoJDD0PHECLhTfLsw+ylQ=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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=
@@ -242,29 +431,37 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
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=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
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=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -279,70 +476,140 @@ 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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
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.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
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.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
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/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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
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.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
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.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
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-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/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=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
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-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/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.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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.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-20231202080848-1f7806d17489 h1:ze1vwAdliUAr68RQ5NtufWaXaOg8WUO2OACzEV+TNdE=
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
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,10 +19,13 @@ 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"
protocolDNS "github.com/sagernet/sing-box/protocol/dns"
"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"
@@ -37,6 +40,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"
@@ -66,6 +74,8 @@ func InboundRegistry() *inbound.Registry {
vless.RegisterInbound(registry)
anytls.RegisterInbound(registry)
bond.RegisterInbound(registry)
registerQUICInbounds(registry)
registerStubForRemovedInbounds(registry)
@@ -80,6 +90,7 @@ func OutboundRegistry() *outbound.Registry {
block.RegisterOutbound(registry)
protocolDNS.RegisterOutbound(registry)
group.RegisterFailover(registry)
group.RegisterSelector(registry)
group.RegisterURLTest(registry)
@@ -95,6 +106,11 @@ func OutboundRegistry() *outbound.Registry {
mieru.RegisterOutbound(registry)
anytls.RegisterOutbound(registry)
bond.RegisterOutbound(registry)
bandwidth.RegisterOutbound(registry)
connection.RegisterOutbound(registry)
registerQUICOutbounds(registry)
registerWireGuardOutbound(registry)
registerStubForRemovedOutbounds(registry)
@@ -137,6 +153,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)
}
@@ -250,3 +255,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

@@ -33,15 +33,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
}

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
}

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

@@ -0,0 +1,146 @@
package bond
import (
"context"
"errors"
"net"
"sync"
"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/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 sync.Mutex
}
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.TypeTunnelServer, tag),
logger: logger,
router: uot.NewRouter(router, logger),
conns: cache.New(C.TCPConnectTimeout, time.Second),
}
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()
defer inbound.mtx.Unlock()
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 {
request, err := ReadRequest(conn)
if err != nil {
return err
}
h.mtx.Lock()
defer h.mtx.Unlock()
var ratioConns map[uint8]*ratioConn
rawRatioConns, ok := h.conns.Get(request.UUID.String())
if ok {
ratioConns = rawRatioConns.(map[uint8]*ratioConn)
} else {
ratioConns = make(map[uint8]*ratioConn, request.Count)
h.conns.SetDefault(request.UUID.String(), 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()
}
return E.New("invalid ratios")
}
conn = NewBondedConn(conns, downloadRatios, uploadRatios)
metadata.Inbound = h.Tag()
metadata.InboundType = C.TypeBond
metadata.Destination = request.Destination
h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
}
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.TypeTunnelClient, 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)
}

View File

@@ -0,0 +1,96 @@
package group
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"
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
}
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)),
}
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 {
return s.tags[0]
}
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
}
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
}
return conn, nil
}
return nil, err
}

View File

@@ -180,3 +180,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

@@ -213,3 +213,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

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

@@ -170,3 +170,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

@@ -138,6 +138,17 @@ 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) {
if h.tlsConfig != nil && h.transport == nil {
tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig)

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

@@ -14,7 +14,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"
@@ -303,7 +303,7 @@ func (m *ConnectionManager) connectionCopy(ctx context.Context, source net.Conn,
} else {
if err == nil {
m.logger.DebugContext(ctx, "connection download finished")
} else if !E.IsClosedOrCanceled(err) && !strings.Contains(err.Error(), "NO_ERROR") {
} else if !E.IsClosedOrCanceled(err) && !strings.Contains(err.Error(), "NO_ERROR") && !strings.Contains(err.Error(), "response body closed") {
m.logger.ErrorContext(ctx, "connection download closed: ", err)
} else {
m.logger.TraceContext(ctx, "connection download closed")

View File

@@ -15,8 +15,8 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
R "github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing-mux"
"github.com/sagernet/sing-vmess"
mux "github.com/sagernet/sing-mux"
vmess "github.com/sagernet/sing-vmess"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
@@ -123,12 +123,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 {
@@ -234,12 +233,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

@@ -30,7 +30,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
@@ -53,6 +55,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),
@@ -159,6 +162,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)
}

View File

@@ -0,0 +1,20 @@
package constant
type ConnectedNode interface {
UpdateUser(user User)
UpdateUsers(users []User)
DeleteUser(user User)
UpdateConnectionLimiter(limiter ConnectionLimiter)
UpdateConnectionLimiters(limiter []ConnectionLimiter)
DeleteConnectionLimiter(limiter ConnectionLimiter)
UpdateBandwidthLimiter(limiter BandwidthLimiter)
UpdateBandwidthLimiters(limiter []BandwidthLimiter)
DeleteBandwidthLimiter(limiter BandwidthLimiter)
IsLocal() bool
IsOnline() bool
Close() error
}

View File

@@ -0,0 +1,38 @@
package constant
type Repository interface {
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)
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)
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)
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)
}

View File

@@ -0,0 +1,155 @@
package postgresql
import (
"encoding/json"
"strconv"
"github.com/huandu/go-sqlbuilder"
"github.com/sagernet/sing/common/byteformats"
)
type Filter func(sb *sqlbuilder.SelectBuilder, value []string) error
func EqualFilter(field string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
sb.Where(sb.Equal(field, value[0]))
return nil
}
}
func EqualOrNullFilter(field string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
sb.Where(sb.Or(sb.Equal(field, value[0]), sb.IsNull(field)))
return nil
}
}
func GreaterThanFilter(field string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
sb.Where(sb.GreaterThan(field, value[0]))
return nil
}
}
func LessThanFilter(field string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
sb.Where(sb.LessThan(field, value[0]))
return nil
}
}
func GreaterEqualThanFilter(field string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
sb.Where(sb.GreaterEqualThan(field, value[0]))
return nil
}
}
func LessEqualThanFilter(field string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
sb.Where(sb.LessEqualThan(field, value[0]))
return nil
}
}
func SpeedGreaterEqualThanFilter(field string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
bytesSpeed, err := json.Marshal(value[0])
if err != nil {
return err
}
speed := &byteformats.NetworkBytesCompat{}
err = speed.UnmarshalJSON(bytesSpeed)
if err != nil {
return err
}
sb.Where(sb.GreaterEqualThan(field, speed.Value()))
return nil
}
}
func SpeedLessEqualThanFilter(field string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
bytesSpeed, err := json.Marshal(value[0])
if err != nil {
return err
}
speed := &byteformats.NetworkBytesCompat{}
err = speed.UnmarshalJSON(bytesSpeed)
if err != nil {
return err
}
sb.Where(sb.LessEqualThan(field, speed.Value()))
return nil
}
}
func ExistsAndWhereInFilter(subquery *sqlbuilder.SelectBuilder, field string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
values := make([]interface{}, len(value))
for i, v := range value {
values[i] = v
}
subquery.Where(subquery.In(field, values...))
sb.Where(sb.Exists(subquery))
return nil
}
}
func SortAscFilter() Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
sb.OrderByAsc(value[0])
return nil
}
}
func SortDescFilter() Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
sb.OrderByDesc(value[0])
return nil
}
}
func ReplacedSortAscFilter(replace map[string]string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
if replacedValue, ok := replace[value[0]]; ok {
sb.OrderByAsc(replacedValue)
} else {
sb.OrderByAsc(value[0])
}
return nil
}
}
func ReplacedSortDescFilter(replace map[string]string) Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
if replacedValue, ok := replace[value[0]]; ok {
sb.OrderByDesc(replacedValue)
} else {
sb.OrderByDesc(value[0])
}
return nil
}
}
func LimitFilter() Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
limit, err := strconv.Atoi(value[0])
if err != nil {
return err
}
sb.Limit(limit)
return nil
}
}
func OffsetFilter() Filter {
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
offset, err := strconv.Atoi(value[0])
if err != nil {
return err
}
sb.Offset(offset)
return nil
}
}

View File

@@ -0,0 +1,132 @@
package postgresql
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": `
CREATE TABLE squads (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
CREATE TABLE nodes (
uuid VARCHAR(36) PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
CREATE TABLE node_to_squad (
node_uuid VARCHAR(36) NOT NULL,
squad_id INTEGER NOT NULL,
PRIMARY KEY (node_uuid, squad_id),
FOREIGN KEY (node_uuid) REFERENCES nodes(uuid) ON DELETE CASCADE,
FOREIGN KEY (squad_id) REFERENCES squads(id) ON DELETE RESTRICT
);
CREATE TABLE users (
id SERIAL PRIMARY KEY,
node_uuid VARCHAR(36),
username TEXT NOT NULL,
type TEXT NOT NULL,
inbound TEXT NOT NULL,
uuid TEXT NOT NULL,
password TEXT NOT NULL,
flow TEXT NOT NULL,
alter_id INTEGER NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
UNIQUE (username, inbound)
);
CREATE TABLE user_to_squad (
user_id INTEGER NOT NULL,
squad_id INTEGER NOT NULL,
PRIMARY KEY (user_id, squad_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (squad_id) REFERENCES squads(id) ON DELETE RESTRICT
);
CREATE TABLE connection_limiters (
id SERIAL PRIMARY KEY,
username TEXT NOT NULL,
outbound TEXT NOT NULL,
strategy TEXT NOT NULL,
connection_type TEXT NOT NULL,
lock_type TEXT NOT NULL,
count INTEGER NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
UNIQUE (username, outbound)
);
CREATE TABLE connection_limiter_to_squad (
connection_limiter_id INTEGER NOT NULL,
squad_id INTEGER NOT NULL,
PRIMARY KEY (connection_limiter_id, squad_id),
FOREIGN KEY (connection_limiter_id) REFERENCES connection_limiters(id) ON DELETE CASCADE,
FOREIGN KEY (squad_id) REFERENCES squads(id) ON DELETE RESTRICT
);
CREATE TABLE bandwidth_limiters (
id SERIAL PRIMARY KEY,
username TEXT NOT NULL,
outbound TEXT NOT NULL,
strategy TEXT NOT NULL,
mode TEXT NOT NULL,
connection_type TEXT NOT NULL,
speed TEXT NOT NULL,
raw_speed BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
UNIQUE (username, outbound)
);
CREATE TABLE bandwidth_limiter_to_squad (
bandwidth_limiter_id INTEGER NOT NULL,
squad_id INTEGER NOT NULL,
PRIMARY KEY (bandwidth_limiter_id, squad_id),
FOREIGN KEY (bandwidth_limiter_id) REFERENCES bandwidth_limiters(id) ON DELETE CASCADE,
FOREIGN KEY (squad_id) REFERENCES squads(id) ON DELETE RESTRICT
);
`,
"1_initialize_schema.down.sql": `
DROP TABLE IF EXISTS squas;
DROP TABLE IF EXISTS nodes;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS bandwidth_limiters;
DROP TABLE IF EXISTS connection_limiters;
`,
}
func Migrate(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()
}

File diff suppressed because it is too large Load Diff

598
service/manager/service.go Normal file
View File

@@ -0,0 +1,598 @@
//go:build with_manager
package manager
import (
"context"
"strconv"
"sync"
"time"
"github.com/go-playground/validator/v10"
"github.com/gofrs/uuid/v5"
"github.com/patrickmn/go-cache"
"github.com/sagernet/sing-box/adapter"
boxService "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"
"github.com/sagernet/sing-box/service/manager/constant"
"github.com/sagernet/sing-box/service/manager/repository/postgresql"
E "github.com/sagernet/sing/common/exceptions"
)
func RegisterService(registry *boxService.Registry) {
boxService.Register[option.ManagerServiceOptions](registry, C.TypeManager, NewService)
}
type Service struct {
boxService.Adapter
ctx context.Context
logger log.ContextLogger
repository constant.Repository
nodes map[string]constant.ConnectedNode
limiterLocks map[int]map[string]*cache.Cache
userValidator *validator.Validate
defaultValidator *validator.Validate
mtx sync.RWMutex
connLockMtx sync.Mutex
}
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.ManagerServiceOptions) (adapter.Service, error) {
var repository constant.Repository
var err error
switch options.Database.Driver {
case "postgresql":
repository, err = postgresql.NewPostgreSQLRepository(ctx, options.Database.DSN)
if err != nil {
return nil, err
}
default:
return nil, E.New("unknown driver \"", options.Database.Driver, "\"")
}
userValidator := validator.New()
userValidator.RegisterStructValidation(func(sl validator.StructLevel) {
user := sl.Current().Interface().(constant.UserCreate)
switch user.Type {
case "vless":
if user.UUID == "" {
sl.ReportError(user.UUID, "uuid", "UUID", "required", "")
}
case "vmess":
if user.UUID == "" {
sl.ReportError(user.UUID, "uuid", "UUID", "required", "")
}
if user.AlterID == 0 {
sl.ReportError(user.AlterID, "alter_id", "AlterID", "required", "")
}
case "trojan", "shadowsocks", "hysteria", "hysteria2":
if user.Password == "" {
sl.ReportError(user.Password, "password", "Password", "required", "")
}
case "tuic":
if user.UUID == "" {
sl.ReportError(user.UUID, "uuid", "UUID", "required", "")
}
if user.Password == "" {
sl.ReportError(user.Password, "password", "Password", "required", "")
}
}
}, constant.UserCreate{})
return &Service{
Adapter: boxService.NewAdapter(C.TypeManager, tag),
ctx: ctx,
logger: logger,
repository: repository,
nodes: make(map[string]constant.ConnectedNode, 0),
limiterLocks: make(map[int]map[string]*cache.Cache),
userValidator: userValidator,
defaultValidator: validator.New(),
}, nil
}
func (s *Service) CreateSquad(node constant.SquadCreate) (constant.Squad, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
err := s.defaultValidator.Struct(node)
if err != nil {
return constant.Squad{}, err
}
createdSquad, err := s.repository.CreateSquad(node)
if err != nil {
return createdSquad, err
}
return createdSquad, nil
}
func (s *Service) GetSquads(filters map[string][]string) ([]constant.Squad, error) {
return s.repository.GetSquads(filters)
}
func (s *Service) GetSquadsCount(filters map[string][]string) (int, error) {
return s.repository.GetSquadsCount(filters)
}
func (s *Service) GetSquad(id int) (constant.Squad, error) {
return s.repository.GetSquad(id)
}
func (s *Service) UpdateSquad(id int, squad constant.SquadUpdate) (constant.Squad, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
err := s.defaultValidator.Struct(squad)
if err != nil {
return constant.Squad{}, err
}
updatedSquad, err := s.repository.UpdateSquad(id, squad)
if err != nil {
return updatedSquad, err
}
return updatedSquad, nil
}
func (s *Service) DeleteSquad(id int) (constant.Squad, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
deletedSquad, err := s.repository.DeleteSquad(id)
if err != nil {
return deletedSquad, err
}
return deletedSquad, nil
}
func (s *Service) CreateNode(node constant.NodeCreate) (constant.Node, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
err := s.defaultValidator.Struct(node)
if err != nil {
return constant.Node{}, err
}
createdNode, err := s.repository.CreateNode(node)
if err != nil {
return createdNode, err
}
return createdNode, nil
}
func (s *Service) GetNodes(filters map[string][]string) ([]constant.Node, error) {
return s.repository.GetNodes(filters)
}
func (s *Service) GetNodesCount(filters map[string][]string) (int, error) {
return s.repository.GetNodesCount(filters)
}
func (s *Service) GetNode(uuid string) (constant.Node, error) {
return s.repository.GetNode(uuid)
}
func (s *Service) GetNodeStatus(uuid string) string {
s.mtx.RLock()
defer s.mtx.RUnlock()
node, ok := s.nodes[uuid]
if !ok || !node.IsOnline() {
return "offline"
}
return "online"
}
func (s *Service) UpdateNode(uuid string, node constant.NodeUpdate) (constant.Node, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
err := s.defaultValidator.Struct(node)
if err != nil {
return constant.Node{}, err
}
updatedNode, err := s.repository.UpdateNode(uuid, node)
if err != nil {
return updatedNode, err
}
return updatedNode, nil
}
func (s *Service) DeleteNode(uuid string) (constant.Node, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
deletedNode, err := s.repository.DeleteNode(uuid)
if err != nil {
return deletedNode, err
}
node, ok := s.nodes[uuid]
if ok {
node.Close()
delete(s.nodes, uuid)
}
return deletedNode, nil
}
func (s *Service) CreateUser(user constant.UserCreate) (constant.User, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
err := s.userValidator.Struct(user)
if err != nil {
return constant.User{}, err
}
createdUser, err := s.repository.CreateUser(user)
if err != nil {
return createdUser, err
}
nodes, err := s.repository.GetNodes(map[string][]string{
"squad_id_in": convertIntSliceToStringSlice(createdUser.SquadIDs),
})
if err != nil {
s.closeAllNodes()
return createdUser, err
}
for _, node := range nodes {
if node, ok := s.nodes[node.UUID]; ok {
node.UpdateUser(createdUser)
}
}
return createdUser, nil
}
func (s *Service) GetUsers(filters map[string][]string) ([]constant.User, error) {
return s.repository.GetUsers(filters)
}
func (s *Service) GetUsersCount(filters map[string][]string) (int, error) {
return s.repository.GetUsersCount(filters)
}
func (s *Service) GetUser(id int) (constant.User, error) {
return s.repository.GetUser(id)
}
func (s *Service) UpdateUser(id int, user constant.UserUpdate) (constant.User, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
updatedUser, err := s.repository.UpdateUser(id, user)
if err != nil {
return updatedUser, err
}
nodes, err := s.repository.GetNodes(map[string][]string{
"squad_id_in": convertIntSliceToStringSlice(updatedUser.SquadIDs),
})
if err != nil {
s.closeAllNodes()
return updatedUser, err
}
for _, node := range nodes {
if node, ok := s.nodes[node.UUID]; ok {
node.UpdateUser(updatedUser)
}
}
return updatedUser, nil
}
func (s *Service) DeleteUser(id int) (constant.User, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
deletedUser, err := s.repository.DeleteUser(id)
if err != nil {
return deletedUser, err
}
nodes, err := s.repository.GetNodes(map[string][]string{
"squad_id_in": convertIntSliceToStringSlice(deletedUser.SquadIDs),
})
if err != nil {
s.closeAllNodes()
return deletedUser, err
}
for _, node := range nodes {
if node, ok := s.nodes[node.UUID]; ok {
node.DeleteUser(deletedUser)
}
}
return deletedUser, nil
}
func (s *Service) CreateConnectionLimiter(limiter constant.ConnectionLimiterCreate) (constant.ConnectionLimiter, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
err := s.defaultValidator.Struct(limiter)
if err != nil {
return constant.ConnectionLimiter{}, err
}
createdLimiter, err := s.repository.CreateConnectionLimiter(limiter)
if err != nil {
return createdLimiter, err
}
nodes, err := s.repository.GetNodes(map[string][]string{
"squad_id_in": convertIntSliceToStringSlice(createdLimiter.SquadIDs),
})
if err != nil {
s.closeAllNodes()
return createdLimiter, err
}
for _, node := range nodes {
if node, ok := s.nodes[node.UUID]; ok {
node.UpdateConnectionLimiter(createdLimiter)
}
}
return createdLimiter, nil
}
func (s *Service) GetConnectionLimiters(filters map[string][]string) ([]constant.ConnectionLimiter, error) {
return s.repository.GetConnectionLimiters(filters)
}
func (s *Service) GetConnectionLimitersCount(filters map[string][]string) (int, error) {
return s.repository.GetConnectionLimitersCount(filters)
}
func (s *Service) GetConnectionLimiter(id int) (constant.ConnectionLimiter, error) {
return s.repository.GetConnectionLimiter(id)
}
func (s *Service) UpdateConnectionLimiter(id int, limiter constant.ConnectionLimiterUpdate) (constant.ConnectionLimiter, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
err := s.defaultValidator.Struct(limiter)
if err != nil {
return constant.ConnectionLimiter{}, err
}
updatedLimiter, err := s.repository.UpdateConnectionLimiter(id, limiter)
if err != nil {
return updatedLimiter, err
}
nodes, err := s.repository.GetNodes(map[string][]string{
"squad_id_in": convertIntSliceToStringSlice(updatedLimiter.SquadIDs),
})
if err != nil {
s.closeAllNodes()
return updatedLimiter, err
}
for _, node := range nodes {
if node, ok := s.nodes[node.UUID]; ok {
node.UpdateConnectionLimiter(updatedLimiter)
}
}
if limiter.LockType != "manager" {
s.connLockMtx.Lock()
defer s.connLockMtx.Unlock()
delete(s.limiterLocks, id)
}
return updatedLimiter, nil
}
func (s *Service) DeleteConnectionLimiter(id int) (constant.ConnectionLimiter, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
deletedLimiter, err := s.repository.DeleteConnectionLimiter(id)
if err != nil {
return deletedLimiter, err
}
nodes, err := s.repository.GetNodes(map[string][]string{
"squad_id_in": convertIntSliceToStringSlice(deletedLimiter.SquadIDs),
})
if err != nil {
s.closeAllNodes()
return deletedLimiter, err
}
for _, node := range nodes {
if node, ok := s.nodes[node.UUID]; ok {
node.DeleteConnectionLimiter(deletedLimiter)
}
}
if deletedLimiter.LockType == "manager" {
s.connLockMtx.Lock()
defer s.connLockMtx.Unlock()
delete(s.limiterLocks, id)
}
return deletedLimiter, nil
}
func (s *Service) CreateBandwidthLimiter(limiter constant.BandwidthLimiterCreate) (constant.BandwidthLimiter, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
err := s.defaultValidator.Struct(limiter)
if err != nil {
return constant.BandwidthLimiter{}, err
}
createdLimiter, err := s.repository.CreateBandwidthLimiter(limiter)
if err != nil {
return createdLimiter, err
}
nodes, err := s.repository.GetNodes(map[string][]string{
"squad_id_in": convertIntSliceToStringSlice(createdLimiter.SquadIDs),
})
if err != nil {
s.closeAllNodes()
return createdLimiter, err
}
for _, node := range nodes {
if node, ok := s.nodes[node.UUID]; ok {
node.UpdateBandwidthLimiter(createdLimiter)
}
}
return createdLimiter, nil
}
func (s *Service) GetBandwidthLimiters(filters map[string][]string) ([]constant.BandwidthLimiter, error) {
return s.repository.GetBandwidthLimiters(filters)
}
func (s *Service) GetBandwidthLimitersCount(filters map[string][]string) (int, error) {
return s.repository.GetBandwidthLimitersCount(filters)
}
func (s *Service) GetBandwidthLimiter(id int) (constant.BandwidthLimiter, error) {
return s.repository.GetBandwidthLimiter(id)
}
func (s *Service) UpdateBandwidthLimiter(id int, limiter constant.BandwidthLimiterUpdate) (constant.BandwidthLimiter, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
err := s.defaultValidator.Struct(limiter)
if err != nil {
return constant.BandwidthLimiter{}, err
}
updatedLimiter, err := s.repository.UpdateBandwidthLimiter(id, limiter)
if err != nil {
return updatedLimiter, err
}
nodes, err := s.repository.GetNodes(map[string][]string{
"squad_id_in": convertIntSliceToStringSlice(updatedLimiter.SquadIDs),
})
if err != nil {
s.closeAllNodes()
return updatedLimiter, err
}
for _, node := range nodes {
if node, ok := s.nodes[node.UUID]; ok {
node.UpdateBandwidthLimiter(updatedLimiter)
}
}
return updatedLimiter, nil
}
func (s *Service) DeleteBandwidthLimiter(id int) (constant.BandwidthLimiter, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
deletedLimiter, err := s.repository.DeleteBandwidthLimiter(id)
if err != nil {
return deletedLimiter, err
}
nodes, err := s.repository.GetNodes(map[string][]string{
"squad_id_in": convertIntSliceToStringSlice(deletedLimiter.SquadIDs),
})
if err != nil {
s.closeAllNodes()
return deletedLimiter, err
}
for _, node := range nodes {
if node, ok := s.nodes[node.UUID]; ok {
node.DeleteBandwidthLimiter(deletedLimiter)
}
}
return deletedLimiter, nil
}
func (s *Service) AddNode(uuid string, node constant.ConnectedNode) error {
s.mtx.Lock()
defer s.mtx.Unlock()
var node_ constant.Node
var err error
node_, err = s.repository.GetNode(uuid)
if err != nil {
return err
}
squadIDs := convertIntSliceToStringSlice(node_.SquadIDs)
users, err := s.repository.GetUsers(map[string][]string{
"squad_id_in": squadIDs,
})
if err != nil {
return err
}
node.UpdateUsers(users)
bandwidthLimiters, err := s.repository.GetBandwidthLimiters(map[string][]string{
"squad_id_in": squadIDs,
})
if err != nil {
return err
}
node.UpdateBandwidthLimiters(bandwidthLimiters)
connectionLimiters, err := s.repository.GetConnectionLimiters(map[string][]string{
"squad_id_in": squadIDs,
})
if err != nil {
return err
}
node.UpdateConnectionLimiters(connectionLimiters)
s.nodes[uuid] = node
return nil
}
func (s *Service) AcquireLock(limiterId int, id string) (string, error) {
s.connLockMtx.Lock()
defer s.connLockMtx.Unlock()
limiter, err := s.repository.GetConnectionLimiter(limiterId)
if err != nil {
return "", err
}
if limiter.LockType != "manager" {
return "", E.New("invalid lock type")
}
locks, ok := s.limiterLocks[limiterId]
if !ok {
locks = make(map[string]*cache.Cache)
s.limiterLocks[limiter.ID] = locks
}
lock, ok := locks[id]
if !ok {
if len(locks) == int(limiter.Count) {
return "", E.New("not enough free locks")
}
lock = cache.New(time.Second*30, time.Second)
lock.OnEvicted(func(_ string, _ interface{}) {
s.connLockMtx.Lock()
defer s.connLockMtx.Unlock()
if lock.ItemCount() == 0 {
delete(locks, id)
}
})
locks[id] = lock
}
handleID, err := uuid.NewV4()
if err != nil {
return "", err
}
lock.SetDefault(handleID.String(), new(struct{}))
return handleID.String(), nil
}
func (s *Service) RefreshLock(limiterId int, id string, handleId string) error {
s.connLockMtx.Lock()
defer s.connLockMtx.Unlock()
locks, ok := s.limiterLocks[limiterId]
if !ok {
return E.New("limiter not found")
}
lock, ok := locks[id]
if !ok {
return E.New("lock not found")
}
err := lock.Replace(handleId, new(struct{}), time.Second*30)
return err
}
func (s *Service) ReleaseLock(limiterId int, id string, handleId string) error {
s.connLockMtx.Lock()
defer s.connLockMtx.Unlock()
locks, ok := s.limiterLocks[limiterId]
if !ok {
return E.New("limiter not found")
}
lock, ok := locks[id]
if !ok {
return E.New("lock not found")
}
go lock.Delete(handleId)
return nil
}
func (s *Service) Start(stage adapter.StartStage) error {
return nil
}
func (s *Service) Close() error {
return nil
}
func (s *Service) closeAllNodes() {
for _, node := range s.nodes {
node.Close()
}
}
func convertIntSliceToStringSlice(values []int) []string {
result := make([]string, len(values))
for i, v := range values {
result[i] = strconv.Itoa(v)
}
return result
}

View File

@@ -0,0 +1,20 @@
//go:build !with_manager
package manager
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.ManagerServiceOptions](registry, C.TypeManager, func(ctx context.Context, logger log.ContextLogger, tag string, options option.ManagerServiceOptions) (adapter.Service, error) {
return nil, E.New(`Manager is not included in this build, rebuild with -tags with_manager`)
})
}

View File

@@ -0,0 +1,18 @@
package constant
import (
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/service/manager/constant"
)
type BandwidthLimiterManager interface {
AddBandwidthLimiterStrategyManager(outbound adapter.Outbound) error
GetBandwidthLimiterStrategyManager(tag string) (BandwidthLimiterStrategyManager, bool)
GetBandwidthLimiterStrategyManagerTags() []string
}
type BandwidthLimiterStrategyManager interface {
UpdateBandwidthLimiter(limiter C.BandwidthLimiter)
UpdateBandwidthLimiters(limiter []C.BandwidthLimiter)
DeleteBandwidthLimiter(username string)
}

View File

@@ -0,0 +1,18 @@
package constant
import (
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/service/manager/constant"
)
type ConnectionLimiterManager interface {
AddConnectionLimiterStrategyManager(outbound adapter.Outbound) error
GetConnectionLimiterStrategyManager(tag string) (ConnectionLimiterStrategyManager, bool)
GetConnectionLimiterStrategyManagerTags() []string
}
type ConnectionLimiterStrategyManager interface {
UpdateConnectionLimiter(limiter C.ConnectionLimiter)
UpdateConnectionLimiters(limiter []C.ConnectionLimiter)
DeleteConnectionLimiter(username string)
}

View File

@@ -0,0 +1,18 @@
package constant
import (
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/service/manager/constant"
)
type InboundManager interface {
AddUserManager(inbound adapter.Inbound) error
GetUserManager(tag string) (UserManager, bool)
GetUserManagerTags() []string
}
type UserManager interface {
UpdateUser(user C.User)
UpdateUsers(users []C.User)
DeleteUser(username string)
}

View File

@@ -0,0 +1,88 @@
package inbound
import (
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/hysteria"
CM "github.com/sagernet/sing-box/service/manager/constant"
"github.com/sagernet/sing-box/service/node/constant"
)
type HysteriaManager struct {
access sync.Mutex
inbounds map[string]*HysteriaUserManager
}
func NewHysteriaManager() *HysteriaManager {
return &HysteriaManager{
inbounds: make(map[string]*HysteriaUserManager),
}
}
func (m *HysteriaManager) AddUserManager(inbound adapter.Inbound) error {
m.access.Lock()
defer m.access.Unlock()
m.inbounds[inbound.Tag()] = &HysteriaUserManager{
inbound: inbound.(*hysteria.Inbound),
usersMap: make(map[string]option.HysteriaUser),
}
return nil
}
func (m *HysteriaManager) GetUserManager(tag string) (constant.UserManager, bool) {
m.access.Lock()
defer m.access.Unlock()
inbound, ok := m.inbounds[tag]
return inbound, ok
}
func (m *HysteriaManager) GetUserManagerTags() []string {
m.access.Lock()
defer m.access.Unlock()
tags := make([]string, 0, len(m.inbounds))
for tag, _ := range m.inbounds {
tags = append(tags, tag)
}
return tags
}
type HysteriaUserManager struct {
inbound *hysteria.Inbound
usersMap map[string]option.HysteriaUser
mtx sync.Mutex
}
func (i *HysteriaUserManager) postUpdate() {
users := make([]option.HysteriaUser, 0, len(i.usersMap))
for _, user := range i.usersMap {
users = append(users, user)
}
i.inbound.UpdateUsers(users)
}
func (i *HysteriaUserManager) UpdateUser(user CM.User) {
i.mtx.Lock()
defer i.mtx.Unlock()
i.usersMap[user.Username] = option.HysteriaUser{Name: user.Username, AuthString: user.Password}
i.postUpdate()
}
func (i *HysteriaUserManager) UpdateUsers(users []CM.User) {
i.mtx.Lock()
defer i.mtx.Unlock()
clear(i.usersMap)
for _, user := range users {
i.usersMap[user.Username] = option.HysteriaUser{Name: user.Username, AuthString: user.Password}
}
i.postUpdate()
}
func (i *HysteriaUserManager) DeleteUser(username string) {
i.mtx.Lock()
defer i.mtx.Unlock()
delete(i.usersMap, username)
i.postUpdate()
}

View File

@@ -0,0 +1,88 @@
package inbound
import (
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/hysteria2"
CM "github.com/sagernet/sing-box/service/manager/constant"
"github.com/sagernet/sing-box/service/node/constant"
)
type Hysteria2Manager struct {
access sync.Mutex
inbounds map[string]*Hysteria2UserManager
}
func NewHysteria2Manager() *Hysteria2Manager {
return &Hysteria2Manager{
inbounds: make(map[string]*Hysteria2UserManager),
}
}
func (m *Hysteria2Manager) AddUserManager(inbound adapter.Inbound) error {
m.access.Lock()
defer m.access.Unlock()
m.inbounds[inbound.Tag()] = &Hysteria2UserManager{
inbound: inbound.(*hysteria2.Inbound),
usersMap: make(map[string]option.Hysteria2User),
}
return nil
}
func (m *Hysteria2Manager) GetUserManager(tag string) (constant.UserManager, bool) {
m.access.Lock()
defer m.access.Unlock()
inbound, ok := m.inbounds[tag]
return inbound, ok
}
func (m *Hysteria2Manager) GetUserManagerTags() []string {
m.access.Lock()
defer m.access.Unlock()
tags := make([]string, 0, len(m.inbounds))
for tag, _ := range m.inbounds {
tags = append(tags, tag)
}
return tags
}
type Hysteria2UserManager struct {
inbound *hysteria2.Inbound
usersMap map[string]option.Hysteria2User
mtx sync.Mutex
}
func (i *Hysteria2UserManager) postUpdate() {
users := make([]option.Hysteria2User, 0, len(i.usersMap))
for _, user := range i.usersMap {
users = append(users, user)
}
i.inbound.UpdateUsers(users)
}
func (i *Hysteria2UserManager) UpdateUser(user CM.User) {
i.mtx.Lock()
defer i.mtx.Unlock()
i.usersMap[user.Username] = option.Hysteria2User{Name: user.Username, Password: user.Password}
i.postUpdate()
}
func (i *Hysteria2UserManager) UpdateUsers(users []CM.User) {
i.mtx.Lock()
defer i.mtx.Unlock()
clear(i.usersMap)
for _, user := range users {
i.usersMap[user.Username] = option.Hysteria2User{Name: user.Username, Password: user.Password}
}
i.postUpdate()
}
func (i *Hysteria2UserManager) DeleteUser(username string) {
i.mtx.Lock()
defer i.mtx.Unlock()
delete(i.usersMap, username)
i.postUpdate()
}

View File

@@ -0,0 +1,88 @@
package inbound
import (
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/trojan"
CM "github.com/sagernet/sing-box/service/manager/constant"
"github.com/sagernet/sing-box/service/node/constant"
)
type TrojanManager struct {
access sync.Mutex
inbounds map[string]*TrojanUserManager
}
func NewTrojanManager() *TrojanManager {
return &TrojanManager{
inbounds: make(map[string]*TrojanUserManager),
}
}
func (m *TrojanManager) AddUserManager(inbound adapter.Inbound) error {
m.access.Lock()
defer m.access.Unlock()
m.inbounds[inbound.Tag()] = &TrojanUserManager{
inbound: inbound.(*trojan.Inbound),
usersMap: make(map[string]option.TrojanUser),
}
return nil
}
func (m *TrojanManager) GetUserManager(tag string) (constant.UserManager, bool) {
m.access.Lock()
defer m.access.Unlock()
inbound, ok := m.inbounds[tag]
return inbound, ok
}
func (m *TrojanManager) GetUserManagerTags() []string {
m.access.Lock()
defer m.access.Unlock()
tags := make([]string, 0, len(m.inbounds))
for tag, _ := range m.inbounds {
tags = append(tags, tag)
}
return tags
}
type TrojanUserManager struct {
inbound *trojan.Inbound
usersMap map[string]option.TrojanUser
mtx sync.Mutex
}
func (i *TrojanUserManager) postUpdate() {
users := make([]option.TrojanUser, 0, len(i.usersMap))
for _, user := range i.usersMap {
users = append(users, user)
}
i.inbound.UpdateUsers(users)
}
func (i *TrojanUserManager) UpdateUser(user CM.User) {
i.mtx.Lock()
defer i.mtx.Unlock()
i.usersMap[user.Username] = option.TrojanUser{Name: user.Username, Password: user.Password}
i.postUpdate()
}
func (i *TrojanUserManager) UpdateUsers(users []CM.User) {
i.mtx.Lock()
defer i.mtx.Unlock()
clear(i.usersMap)
for _, user := range users {
i.usersMap[user.Username] = option.TrojanUser{Name: user.Username, Password: user.Password}
}
i.postUpdate()
}
func (i *TrojanUserManager) DeleteUser(username string) {
i.mtx.Lock()
defer i.mtx.Unlock()
delete(i.usersMap, username)
i.postUpdate()
}

View File

@@ -0,0 +1,88 @@
package inbound
import (
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/tuic"
CM "github.com/sagernet/sing-box/service/manager/constant"
"github.com/sagernet/sing-box/service/node/constant"
)
type TUICManager struct {
access sync.Mutex
inbounds map[string]*TUICUserManager
}
func NewTUICManager() *TUICManager {
return &TUICManager{
inbounds: make(map[string]*TUICUserManager),
}
}
func (m *TUICManager) AddUserManager(inbound adapter.Inbound) error {
m.access.Lock()
defer m.access.Unlock()
m.inbounds[inbound.Tag()] = &TUICUserManager{
inbound: inbound.(*tuic.Inbound),
usersMap: make(map[string]option.TUICUser),
}
return nil
}
func (m *TUICManager) GetUserManager(tag string) (constant.UserManager, bool) {
m.access.Lock()
defer m.access.Unlock()
inbound, ok := m.inbounds[tag]
return inbound, ok
}
func (m *TUICManager) GetUserManagerTags() []string {
m.access.Lock()
defer m.access.Unlock()
tags := make([]string, 0, len(m.inbounds))
for tag, _ := range m.inbounds {
tags = append(tags, tag)
}
return tags
}
type TUICUserManager struct {
inbound *tuic.Inbound
usersMap map[string]option.TUICUser
mtx sync.Mutex
}
func (i *TUICUserManager) postUpdate() {
users := make([]option.TUICUser, 0, len(i.usersMap))
for _, user := range i.usersMap {
users = append(users, user)
}
i.inbound.UpdateUsers(users)
}
func (i *TUICUserManager) UpdateUser(user CM.User) {
i.mtx.Lock()
defer i.mtx.Unlock()
i.usersMap[user.Username] = option.TUICUser{Name: user.Username, UUID: user.UUID, Password: user.Password}
i.postUpdate()
}
func (i *TUICUserManager) UpdateUsers(users []CM.User) {
i.mtx.Lock()
defer i.mtx.Unlock()
clear(i.usersMap)
for _, user := range users {
i.usersMap[user.Username] = option.TUICUser{Name: user.Username, UUID: user.UUID, Password: user.Password}
}
i.postUpdate()
}
func (i *TUICUserManager) DeleteUser(username string) {
i.mtx.Lock()
defer i.mtx.Unlock()
delete(i.usersMap, username)
i.postUpdate()
}

View File

@@ -0,0 +1,88 @@
package inbound
import (
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/vless"
CM "github.com/sagernet/sing-box/service/manager/constant"
"github.com/sagernet/sing-box/service/node/constant"
)
type VLESSManager struct {
access sync.Mutex
inbounds map[string]*VLESSUserManager
}
func NewVLESSManager() *VLESSManager {
return &VLESSManager{
inbounds: make(map[string]*VLESSUserManager),
}
}
func (m *VLESSManager) AddUserManager(inbound adapter.Inbound) error {
m.access.Lock()
defer m.access.Unlock()
m.inbounds[inbound.Tag()] = &VLESSUserManager{
inbound: inbound.(*vless.Inbound),
usersMap: make(map[string]option.VLESSUser),
}
return nil
}
func (m *VLESSManager) GetUserManager(tag string) (constant.UserManager, bool) {
m.access.Lock()
defer m.access.Unlock()
inbound, ok := m.inbounds[tag]
return inbound, ok
}
func (m *VLESSManager) GetUserManagerTags() []string {
m.access.Lock()
defer m.access.Unlock()
tags := make([]string, 0, len(m.inbounds))
for tag, _ := range m.inbounds {
tags = append(tags, tag)
}
return tags
}
type VLESSUserManager struct {
inbound *vless.Inbound
usersMap map[string]option.VLESSUser
mtx sync.Mutex
}
func (i *VLESSUserManager) postUpdate() {
users := make([]option.VLESSUser, 0, len(i.usersMap))
for _, user := range i.usersMap {
users = append(users, user)
}
i.inbound.UpdateUsers(users)
}
func (i *VLESSUserManager) UpdateUser(user CM.User) {
i.mtx.Lock()
defer i.mtx.Unlock()
i.usersMap[user.Username] = option.VLESSUser{Name: user.Username, UUID: user.UUID, Flow: user.Flow}
i.postUpdate()
}
func (i *VLESSUserManager) UpdateUsers(users []CM.User) {
i.mtx.Lock()
defer i.mtx.Unlock()
clear(i.usersMap)
for _, user := range users {
i.usersMap[user.Username] = option.VLESSUser{Name: user.Username, UUID: user.UUID, Flow: user.Flow}
}
i.postUpdate()
}
func (i *VLESSUserManager) DeleteUser(username string) {
i.mtx.Lock()
defer i.mtx.Unlock()
delete(i.usersMap, username)
i.postUpdate()
}

View File

@@ -0,0 +1,88 @@
package inbound
import (
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/vmess"
CM "github.com/sagernet/sing-box/service/manager/constant"
"github.com/sagernet/sing-box/service/node/constant"
)
type VMessManager struct {
inbounds map[string]*VMessUserManager
mtx sync.Mutex
}
func NewVMessManager() *VMessManager {
return &VMessManager{
inbounds: make(map[string]*VMessUserManager),
}
}
func (m *VMessManager) AddUserManager(inbound adapter.Inbound) error {
m.mtx.Lock()
defer m.mtx.Unlock()
m.inbounds[inbound.Tag()] = &VMessUserManager{
inbound: inbound.(*vmess.Inbound),
usersMap: make(map[string]option.VMessUser),
}
return nil
}
func (m *VMessManager) GetUserManager(tag string) (constant.UserManager, bool) {
m.mtx.Lock()
defer m.mtx.Unlock()
inbound, ok := m.inbounds[tag]
return inbound, ok
}
func (m *VMessManager) GetUserManagerTags() []string {
m.mtx.Lock()
defer m.mtx.Unlock()
tags := make([]string, 0, len(m.inbounds))
for tag, _ := range m.inbounds {
tags = append(tags, tag)
}
return tags
}
type VMessUserManager struct {
inbound *vmess.Inbound
usersMap map[string]option.VMessUser
mtx sync.Mutex
}
func (i *VMessUserManager) postUpdate() {
users := make([]option.VMessUser, 0, len(i.usersMap))
for _, user := range i.usersMap {
users = append(users, user)
}
i.inbound.UpdateUsers(users)
}
func (i *VMessUserManager) UpdateUser(user CM.User) {
i.mtx.Lock()
defer i.mtx.Unlock()
i.usersMap[user.Username] = option.VMessUser{Name: user.Username, UUID: user.UUID, AlterId: user.AlterID}
i.postUpdate()
}
func (i *VMessUserManager) UpdateUsers(users []CM.User) {
i.mtx.Lock()
defer i.mtx.Unlock()
clear(i.usersMap)
for _, user := range users {
i.usersMap[user.Username] = option.VMessUser{Name: user.Username, UUID: user.UUID, AlterId: user.AlterID}
}
i.postUpdate()
}
func (i *VMessUserManager) DeleteUser(username string) {
i.mtx.Lock()
defer i.mtx.Unlock()
delete(i.usersMap, username)
i.postUpdate()
}

View File

@@ -0,0 +1,107 @@
package limiter
import (
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/protocol/limiter/bandwidth"
CM "github.com/sagernet/sing-box/service/manager/constant"
"github.com/sagernet/sing-box/service/node/constant"
E "github.com/sagernet/sing/common/exceptions"
)
type ManagedBandwidthStrategy interface {
UpdateStrategies(strategies map[string]bandwidth.BandwidthStrategy)
}
type BandwidthLimiterManager struct {
managers map[string]*BandwidthLimiterStrategyManager
mtx sync.Mutex
}
func NewBandwidthLimiterManager() *BandwidthLimiterManager {
return &BandwidthLimiterManager{
managers: make(map[string]*BandwidthLimiterStrategyManager),
}
}
func (m *BandwidthLimiterManager) AddBandwidthLimiterStrategyManager(outbound adapter.Outbound) error {
m.mtx.Lock()
defer m.mtx.Unlock()
limiter, ok := outbound.(*bandwidth.Outbound)
if !ok {
return E.New("invalid bandwidth limiter: ", outbound.Tag())
}
strategy, ok := limiter.GetStrategy().(ManagedBandwidthStrategy)
if !ok {
return E.New("strategy for outbound ", outbound.Tag(), " is not manager")
}
m.managers[outbound.Tag()] = &BandwidthLimiterStrategyManager{
strategy: strategy,
strategiesMap: make(map[string]bandwidth.BandwidthStrategy),
}
return nil
}
func (m *BandwidthLimiterManager) GetBandwidthLimiterStrategyManager(tag string) (constant.BandwidthLimiterStrategyManager, bool) {
m.mtx.Lock()
defer m.mtx.Unlock()
manager, ok := m.managers[tag]
return manager, ok
}
func (m *BandwidthLimiterManager) GetBandwidthLimiterStrategyManagerTags() []string {
m.mtx.Lock()
defer m.mtx.Unlock()
tags := make([]string, 0, len(m.managers))
for tag, _ := range m.managers {
tags = append(tags, tag)
}
return tags
}
type BandwidthLimiterStrategyManager struct {
strategy ManagedBandwidthStrategy
strategiesMap map[string]bandwidth.BandwidthStrategy
mtx sync.Mutex
}
func (i *BandwidthLimiterStrategyManager) postUpdate() {
i.strategy.UpdateStrategies(i.strategiesMap)
}
func (i *BandwidthLimiterStrategyManager) UpdateBandwidthLimiter(limiter CM.BandwidthLimiter) {
i.mtx.Lock()
defer i.mtx.Unlock()
strategy, err := bandwidth.CreateStrategy(limiter.Strategy, limiter.Mode, limiter.ConnectionType, limiter.RawSpeed)
if err != nil {
return
}
i.strategiesMap[limiter.Username] = strategy
i.postUpdate()
}
func (i *BandwidthLimiterStrategyManager) UpdateBandwidthLimiters(limiters []CM.BandwidthLimiter) {
i.mtx.Lock()
defer i.mtx.Unlock()
clear(i.strategiesMap)
newStrategiesMap := make(map[string]bandwidth.BandwidthStrategy)
for _, limiter := range limiters {
strategy, err := bandwidth.CreateStrategy(limiter.Strategy, limiter.Mode, limiter.ConnectionType, limiter.RawSpeed)
if err != nil {
return
}
newStrategiesMap[limiter.Username] = strategy
}
i.strategiesMap = newStrategiesMap
i.postUpdate()
}
func (i *BandwidthLimiterStrategyManager) DeleteBandwidthLimiter(username string) {
i.mtx.Lock()
defer i.mtx.Unlock()
delete(i.strategiesMap, username)
i.postUpdate()
}

View File

@@ -0,0 +1,195 @@
package limiter
import (
"context"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/protocol/limiter/connection"
CM "github.com/sagernet/sing-box/service/manager/constant"
"github.com/sagernet/sing-box/service/node/constant"
E "github.com/sagernet/sing/common/exceptions"
)
type ManagedConnectionStrategy interface {
UpdateStrategies(strategies map[string]connection.ConnectionStrategy)
}
type ConnectionLimiterManager struct {
nodeManager CM.NodeManager
managers map[string]*ConnectionLimiterStrategyManager
logger log.Logger
mtx sync.Mutex
}
func NewConnectionLimiterManager(nodeManager CM.NodeManager, logger log.Logger) *ConnectionLimiterManager {
return &ConnectionLimiterManager{
nodeManager: nodeManager,
managers: make(map[string]*ConnectionLimiterStrategyManager),
logger: logger,
}
}
func (m *ConnectionLimiterManager) AddConnectionLimiterStrategyManager(outbound adapter.Outbound) error {
m.mtx.Lock()
defer m.mtx.Unlock()
limiter, ok := outbound.(*connection.Outbound)
if !ok {
return E.New("invalid connection limiter: ", outbound.Tag())
}
strategy, ok := limiter.GetStrategy().(ManagedConnectionStrategy)
if !ok {
return E.New("strategy ", strategy, " is not manager")
}
m.managers[outbound.Tag()] = &ConnectionLimiterStrategyManager{
strategy: strategy,
strategiesMap: make(map[string]connection.ConnectionStrategy),
manager: m,
}
return nil
}
func (m *ConnectionLimiterManager) GetConnectionLimiterStrategyManager(tag string) (constant.ConnectionLimiterStrategyManager, bool) {
m.mtx.Lock()
defer m.mtx.Unlock()
manager, ok := m.managers[tag]
return manager, ok
}
func (m *ConnectionLimiterManager) GetConnectionLimiterStrategyManagerTags() []string {
m.mtx.Lock()
defer m.mtx.Unlock()
tags := make([]string, 0, len(m.managers))
for tag, _ := range m.managers {
tags = append(tags, tag)
}
return tags
}
type ConnectionLimiterStrategyManager struct {
strategy ManagedConnectionStrategy
strategiesMap map[string]connection.ConnectionStrategy
tag string
manager *ConnectionLimiterManager
mtx sync.Mutex
}
func (i *ConnectionLimiterStrategyManager) postUpdate() {
i.strategy.UpdateStrategies(i.strategiesMap)
}
func (i *ConnectionLimiterStrategyManager) UpdateConnectionLimiter(limiter CM.ConnectionLimiter) {
i.mtx.Lock()
defer i.mtx.Unlock()
lock, err := i.createLock(limiter)
if err != nil {
return
}
strategy, err := connection.CreateStrategy(limiter.Strategy, limiter.ConnectionType, lock)
if err != nil {
return
}
i.strategiesMap[limiter.Username] = strategy
i.postUpdate()
}
func (i *ConnectionLimiterStrategyManager) UpdateConnectionLimiters(limiters []CM.ConnectionLimiter) {
i.mtx.Lock()
defer i.mtx.Unlock()
clear(i.strategiesMap)
newStrategiesMap := make(map[string]connection.ConnectionStrategy)
for _, limiter := range limiters {
lock, err := i.createLock(limiter)
if err != nil {
return
}
strategy, err := connection.CreateStrategy(limiter.Strategy, limiter.ConnectionType, lock)
if err != nil {
return
}
newStrategiesMap[limiter.Username] = strategy
}
i.strategiesMap = newStrategiesMap
i.postUpdate()
}
func (i *ConnectionLimiterStrategyManager) DeleteConnectionLimiter(username string) {
i.mtx.Lock()
defer i.mtx.Unlock()
delete(i.strategiesMap, username)
i.postUpdate()
}
func (i *ConnectionLimiterStrategyManager) createLock(limiter CM.ConnectionLimiter) (connection.LockIDGetter, error) {
switch limiter.LockType {
case "manager":
return i.newManagerLock(limiter.ID), nil
case "":
return connection.NewDefaultLock(limiter.Count), nil
default:
return nil, E.New("unknown lock type \"", limiter.LockType, "\"")
}
}
type ManagerLock struct {
handleId string
ctx context.Context
cancel context.CancelFunc
handles uint32
}
func (i *ConnectionLimiterStrategyManager) newManagerLock(limiterId int) connection.LockIDGetter {
conns := make(map[string]*ManagerLock)
mtx := sync.Mutex{}
return func(id string) (connection.CloseHandlerFunc, context.Context, error) {
mtx.Lock()
defer mtx.Unlock()
conn, ok := conns[id]
if !ok {
nodeManager := i.manager.nodeManager
handleId, err := nodeManager.AcquireLock(limiterId, id)
if err != nil {
return nil, nil, err
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
for {
select {
case <-ctx.Done():
return
case <-time.After(time.Second * 5):
err := nodeManager.RefreshLock(limiterId, id, handleId)
if err != nil {
cancel()
return
}
}
}
}()
conn = &ManagerLock{
handleId: handleId,
ctx: ctx,
cancel: cancel,
}
conns[id] = conn
}
conn.handles++
var once sync.Once
return func() {
once.Do(func() {
mtx.Lock()
defer mtx.Unlock()
conn.handles--
if conn.handles == 0 {
conn.cancel()
i.manager.nodeManager.ReleaseLock(limiterId, id, conn.handleId)
delete(conns, id)
}
})
}, conn.ctx, nil
}
}

235
service/node/service.go Normal file
View File

@@ -0,0 +1,235 @@
package node
import (
"context"
"sync"
"github.com/sagernet/sing-box/adapter"
boxService "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"
CM "github.com/sagernet/sing-box/service/manager/constant"
"github.com/sagernet/sing-box/service/node/constant"
"github.com/sagernet/sing-box/service/node/inbound"
"github.com/sagernet/sing-box/service/node/limiter"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/service"
)
func RegisterService(registry *boxService.Registry) {
boxService.Register[option.NodeServiceOptions](registry, C.TypeNode, NewService)
}
type Service struct {
boxService.Adapter
ctx context.Context
logger log.ContextLogger
inboundManagers map[string]constant.InboundManager
bandwidthManager constant.BandwidthLimiterManager
connectionManager constant.ConnectionLimiterManager
options option.NodeServiceOptions
mtx sync.Mutex
}
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.NodeServiceOptions) (adapter.Service, error) {
return &Service{
Adapter: boxService.NewAdapter(C.TypeManager, tag),
ctx: ctx,
logger: logger,
options: options,
}, nil
}
func (s *Service) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
boxManager := service.FromContext[adapter.ServiceManager](s.ctx)
serviceManager, ok := boxManager.Get(s.options.Manager)
if !ok {
return E.New("manager ", s.options.Manager, " not found")
}
nodeManager, ok := serviceManager.(CM.NodeManager)
if !ok {
return E.New("invalid ", s.options.Manager, " manager")
}
inboundManager := service.FromContext[adapter.InboundManager](s.ctx)
outboundManager := service.FromContext[adapter.OutboundManager](s.ctx)
s.inboundManagers = map[string]constant.InboundManager{
"hysteria": inbound.NewHysteriaManager(),
"hysteria2": inbound.NewHysteria2Manager(),
"trojan": inbound.NewTrojanManager(),
"tuic": inbound.NewTUICManager(),
"vless": inbound.NewVLESSManager(),
"vmess": inbound.NewVMessManager(),
}
s.connectionManager = limiter.NewConnectionLimiterManager(nodeManager, s.logger)
s.bandwidthManager = limiter.NewBandwidthLimiterManager()
for _, tag := range s.options.Inbounds {
inbound, ok := inboundManager.Get(tag)
if !ok {
return E.New("inbound ", tag, " not found")
}
inboundManager, ok := s.inboundManagers[inbound.Type()]
if !ok {
return E.New("inbound manager for ", tag, " not found")
}
err := inboundManager.AddUserManager(inbound)
if err != nil {
return err
}
}
for _, limiter := range s.options.ConnectionLimiters {
outbound, ok := outboundManager.Outbound(limiter)
if !ok {
return E.New("outbound ", limiter, " not found")
}
err := s.connectionManager.AddConnectionLimiterStrategyManager(outbound)
if err != nil {
return err
}
}
for _, limiter := range s.options.BandwidthLimiters {
outbound, ok := outboundManager.Outbound(limiter)
if !ok {
return E.New("outbound ", limiter, " not found")
}
err := s.bandwidthManager.AddBandwidthLimiterStrategyManager(outbound)
if err != nil {
return err
}
}
return nodeManager.AddNode(s.options.UUID, s)
}
func (s *Service) UpdateUser(user CM.User) {
s.mtx.Lock()
defer s.mtx.Unlock()
manager, ok := s.inboundManagers[user.Type]
if !ok {
return
}
userManager, ok := manager.GetUserManager(user.Inbound)
if !ok {
return
}
userManager.UpdateUser(user)
}
func (s *Service) UpdateUsers(users []CM.User) {
s.mtx.Lock()
defer s.mtx.Unlock()
typedUsers := make(map[string][]CM.User)
for _, user := range users {
u, ok := typedUsers[user.Type]
if !ok {
typedUsers[user.Type] = make([]CM.User, 0)
}
typedUsers[user.Type] = append(u, user)
}
for type_, users := range typedUsers {
manager, ok := s.inboundManagers[type_]
if !ok {
continue
}
for _, user := range users {
userManager, ok := manager.GetUserManager(user.Inbound)
if !ok {
continue
}
userManager.UpdateUsers(users)
}
}
}
func (s *Service) DeleteUser(user CM.User) {
s.mtx.Lock()
defer s.mtx.Unlock()
manager, ok := s.inboundManagers[user.Type]
if !ok {
return
}
userManager, ok := manager.GetUserManager(user.Inbound)
if !ok {
return
}
userManager.DeleteUser(user.Username)
}
func (s *Service) UpdateConnectionLimiter(limiter CM.ConnectionLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
manager, ok := s.connectionManager.GetConnectionLimiterStrategyManager(limiter.Outbound)
if !ok {
return
}
manager.UpdateConnectionLimiter(limiter)
}
func (s *Service) UpdateConnectionLimiters(limiters []CM.ConnectionLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
for _, limiter := range limiters {
manager, ok := s.connectionManager.GetConnectionLimiterStrategyManager(limiter.Outbound)
if !ok {
continue
}
manager.UpdateConnectionLimiters(limiters)
}
}
func (s *Service) DeleteConnectionLimiter(limiter CM.ConnectionLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
manager, ok := s.connectionManager.GetConnectionLimiterStrategyManager(limiter.Outbound)
if !ok {
return
}
manager.DeleteConnectionLimiter(limiter.Username)
}
func (s *Service) UpdateBandwidthLimiter(limiter CM.BandwidthLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
manager, ok := s.bandwidthManager.GetBandwidthLimiterStrategyManager(limiter.Outbound)
if !ok {
return
}
manager.UpdateBandwidthLimiter(limiter)
}
func (s *Service) UpdateBandwidthLimiters(limiters []CM.BandwidthLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
for _, limiter := range limiters {
manager, ok := s.bandwidthManager.GetBandwidthLimiterStrategyManager(limiter.Outbound)
if !ok {
continue
}
manager.UpdateBandwidthLimiters(limiters)
}
}
func (s *Service) DeleteBandwidthLimiter(limiter CM.BandwidthLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
manager, ok := s.bandwidthManager.GetBandwidthLimiterStrategyManager(limiter.Outbound)
if !ok {
return
}
manager.DeleteBandwidthLimiter(limiter.Username)
}
func (s *Service) IsLocal() bool {
return true
}
func (s *Service) IsOnline() bool {
return true
}
func (s *Service) Close() error {
return nil
}

View File

@@ -0,0 +1,274 @@
package client
import (
"context"
"net"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
boxService "github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/common/dialer"
"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"
CM "github.com/sagernet/sing-box/service/manager/constant"
pb "github.com/sagernet/sing-box/service/node_manager/manager"
"github.com/sagernet/sing/common"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
)
func RegisterService(registry *boxService.Registry) {
boxService.Register[option.NodeManagerClientServiceOptions](registry, C.TypeNodeManagerClient, NewService)
}
type Service struct {
boxService.Adapter
ctx context.Context
logger log.ContextLogger
dialer N.Dialer
creds credentials.TransportCredentials
options option.NodeManagerClientServiceOptions
conn *grpc.ClientConn
mtx sync.Mutex
}
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.NodeManagerClientServiceOptions) (adapter.Service, error) {
outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
if err != nil {
return nil, err
}
creds := insecure.NewCredentials()
if options.TLS != nil {
tlsConfig, err := tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS))
if err != nil {
return nil, err
}
creds = &tlsCreds{tlsConfig}
}
return &Service{
Adapter: boxService.NewAdapter(C.TypeManager, tag),
ctx: ctx,
logger: logger,
dialer: outboundDialer,
creds: creds,
options: options,
}, nil
}
func (s *Service) AddNode(uuid string, node CM.ConnectedNode) error {
go func() {
isRetry := false
for {
if !isRetry {
select {
case <-s.ctx.Done():
return
default:
isRetry = true
}
} else {
select {
case <-time.After(5 * time.Second):
break
case <-s.ctx.Done():
return
}
}
conn, err := s.getConn()
if err != nil {
s.logger.Error(err)
continue
}
client := pb.NewManagerClient(conn)
stream, err := client.AddNode(s.ctx, &pb.Node{Uuid: uuid})
if err != nil {
s.logger.Error(err)
continue
}
err = s.handler(node, stream)
if err != nil {
s.logger.Error(err)
continue
}
}
}()
return nil
}
func (s *Service) AcquireLock(limiterId int, id string) (string, error) {
conn, err := s.getConn()
if err != nil {
return "", err
}
client := pb.NewManagerClient(conn)
lockReply, err := client.AcquireLock(s.ctx, &pb.AcquireLockRequest{LimiterId: int32(limiterId), Id: id})
if err != nil {
return "", err
}
return lockReply.HandleId, err
}
func (s *Service) RefreshLock(limiterId int, id string, handleId string) error {
conn, err := s.getConn()
if err != nil {
return err
}
client := pb.NewManagerClient(conn)
_, err = client.RefreshLock(s.ctx, &pb.LockData{LimiterId: int32(limiterId), Id: id, HandleId: handleId})
return err
}
func (s *Service) ReleaseLock(limiterId int, id string, handleId string) error {
conn, err := s.getConn()
if err != nil {
return err
}
client := pb.NewManagerClient(conn)
_, err = client.ReleaseLock(s.ctx, &pb.LockData{LimiterId: int32(limiterId), Id: id, HandleId: handleId})
return err
}
func (s *Service) Start(stage adapter.StartStage) error {
return nil
}
func (s *Service) Close() error {
return nil
}
func (s *Service) getConn() (*grpc.ClientConn, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
if s.conn != nil {
state := s.conn.GetState()
if state != connectivity.Shutdown && state != connectivity.TransientFailure {
return s.conn, nil
}
}
for {
conn, err := s.createConn()
if err != nil {
return nil, err
}
s.conn = conn
return conn, nil
}
}
func (s *Service) createConn() (*grpc.ClientConn, error) {
conn, err := grpc.NewClient(
s.options.ServerOptions.Build().String(),
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
return s.dialer.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddr(addr))
}),
grpc.WithTransportCredentials(s.creds),
)
if err != nil {
return nil, err
}
return conn, nil
}
func (s *Service) handler(node CM.ConnectedNode, stream grpc.ServerStreamingClient[pb.NodeData]) error {
for {
data, err := stream.Recv()
if err != nil {
return err
}
switch data.Op {
case pb.OpType_updateUser:
s.logger.DebugContext(s.ctx, "update user")
node.UpdateUser(s.convertUser(data.Data.(*pb.NodeData_User).User))
case pb.OpType_updateUsers:
s.logger.DebugContext(s.ctx, "update users")
users := data.Data.(*pb.NodeData_Users).Users.Values
convertedUsers := make([]CM.User, len(users))
for i, user := range users {
convertedUsers[i] = s.convertUser(user)
}
node.UpdateUsers(convertedUsers)
case pb.OpType_deleteUser:
s.logger.DebugContext(s.ctx, "delete user")
node.DeleteUser(s.convertUser(data.Data.(*pb.NodeData_User).User))
case pb.OpType_updateConnectionLimiter:
s.logger.DebugContext(s.ctx, "update connection limiter")
node.UpdateConnectionLimiter(s.convertConnectionLimiter(data.Data.(*pb.NodeData_ConnectionLimiter).ConnectionLimiter))
case pb.OpType_updateConnectionLimiters:
s.logger.DebugContext(s.ctx, "update connection limiters")
limiters := data.Data.(*pb.NodeData_ConnectionLimiters).ConnectionLimiters.Values
convertedLimiters := make([]CM.ConnectionLimiter, len(limiters))
for i, limiter := range limiters {
convertedLimiters[i] = s.convertConnectionLimiter(limiter)
}
node.UpdateConnectionLimiters(convertedLimiters)
case pb.OpType_deleteConnectionLimiter:
s.logger.DebugContext(s.ctx, "delete connection limiter")
node.DeleteConnectionLimiter(s.convertConnectionLimiter(data.Data.(*pb.NodeData_ConnectionLimiter).ConnectionLimiter))
case pb.OpType_updateBandwidthLimiter:
s.logger.DebugContext(s.ctx, "update bandwidth limiter")
node.UpdateBandwidthLimiter(s.convertBandwidthLimiter(data.Data.(*pb.NodeData_BandwidthLimiter).BandwidthLimiter))
case pb.OpType_updateBandwidthLimiters:
s.logger.DebugContext(s.ctx, "update bandwidth limiters")
limiters := data.Data.(*pb.NodeData_BandwidthLimiters).BandwidthLimiters.Values
convertedLimiters := make([]CM.BandwidthLimiter, len(limiters))
for i, limiter := range limiters {
convertedLimiters[i] = s.convertBandwidthLimiter(limiter)
}
node.UpdateBandwidthLimiters(convertedLimiters)
case pb.OpType_deleteBandwidthLimiter:
s.logger.DebugContext(s.ctx, "delete bandwidth limiter")
node.DeleteBandwidthLimiter(s.convertBandwidthLimiter(data.Data.(*pb.NodeData_BandwidthLimiter).BandwidthLimiter))
}
}
}
func (s *Service) convertUser(user *pb.User) CM.User {
return CM.User{
ID: int(user.Id),
Username: user.Username,
Type: user.Type,
Inbound: user.Inbound,
UUID: user.Uuid,
Password: user.Password,
Flow: user.Flow,
AlterID: int(user.AlterId),
}
}
func (s *Service) convertBandwidthLimiter(limiter *pb.BandwidthLimiter) CM.BandwidthLimiter {
return CM.BandwidthLimiter{
ID: int(limiter.Id),
Username: limiter.Username,
Outbound: limiter.Outbound,
Strategy: limiter.Strategy,
Mode: limiter.Mode,
ConnectionType: limiter.ConnectionType,
Speed: limiter.Speed,
RawSpeed: limiter.RawSpeed,
}
}
func (s *Service) convertConnectionLimiter(limiter *pb.ConnectionLimiter) CM.ConnectionLimiter {
return CM.ConnectionLimiter{
ID: int(limiter.Id),
Username: limiter.Username,
Outbound: limiter.Outbound,
Strategy: limiter.Strategy,
ConnectionType: limiter.ConnectionType,
LockType: limiter.LockType,
Count: limiter.Count,
}
}

View File

@@ -0,0 +1,44 @@
package client
import (
"context"
"net"
"github.com/sagernet/sing-box/common/tls"
E "github.com/sagernet/sing/common/exceptions"
"google.golang.org/grpc/credentials"
)
type tlsCreds struct {
// TLS configuration
config tls.Config
}
func (c tlsCreds) Info() credentials.ProtocolInfo {
return credentials.ProtocolInfo{
SecurityProtocol: "tls",
SecurityVersion: "1.2",
ServerName: c.config.ServerName(),
}
}
func (c *tlsCreds) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
conn, err := tls.ClientHandshake(ctx, rawConn, c.config)
if err != nil {
return nil, nil, err
}
return conn, credentials.TLSInfo{State: conn.ConnectionState()}, err
}
func (c *tlsCreds) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
return nil, nil, E.New("not implemented")
}
func (c *tlsCreds) Clone() credentials.TransportCredentials {
return &tlsCreds{config: c.config.Clone()}
}
func (c *tlsCreds) OverrideServerName(serverNameOverride string) error {
c.config.SetServerName(serverNameOverride)
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,104 @@
syntax = "proto3";
option go_package = "github.com/sagernet/sing-box/service/remotemanager/manager";
package manager.v1;
service Manager {
rpc AddNode (Node) returns (stream NodeData);
rpc AcquireLock(AcquireLockRequest) returns (LockData);
rpc RefreshLock(LockData) returns (Empty);
rpc ReleaseLock(LockData) returns (Empty);
}
message Node {
string uuid = 1;
}
enum OpType {
updateUsers = 0;
updateUser = 1;
deleteUser = 2;
updateBandwidthLimiters = 3;
updateBandwidthLimiter = 4;
deleteBandwidthLimiter = 5;
updateConnectionLimiters = 6;
updateConnectionLimiter = 7;
deleteConnectionLimiter = 8;
}
message User {
int32 id = 1;
string username = 3;
string type = 4;
string inbound = 5;
string uuid = 6;
string password = 7;
string flow = 8;
int32 alter_id = 9;
}
message UserList {
repeated User values = 1;
}
message BandwidthLimiter {
int32 id = 1;
string username = 3;
string outbound = 4;
string strategy = 5;
string mode = 6;
string connection_type = 7;
string speed = 8;
uint64 raw_speed = 9;
}
message BandwidthLimiterList {
repeated BandwidthLimiter values = 1;
}
message ConnectionLimiter {
int32 id = 1;
string username = 3;
string outbound = 4;
string strategy = 5;
string connection_type = 6;
string lock_type = 7;
uint32 count = 8;
}
message ConnectionLimiterList {
repeated ConnectionLimiter values = 1;
}
message NodeData {
OpType op = 1;
oneof data {
UserList users = 2;
User user = 3;
BandwidthLimiterList bandwidth_limiters = 4;
BandwidthLimiter bandwidth_limiter = 5;
ConnectionLimiterList connection_limiters = 6;
ConnectionLimiter connection_limiter = 7;
}
}
message AcquireLockRequest {
int32 limiter_id = 1;
string id = 2;
}
message LockData {
int32 limiter_id = 1;
string id = 2;
string handleId = 3;
}
message Empty {
}

View File

@@ -0,0 +1,239 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.0
// - protoc v6.33.1
// source: manager/manager.proto
package manager
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
Manager_AddNode_FullMethodName = "/manager.v1.Manager/AddNode"
Manager_AcquireLock_FullMethodName = "/manager.v1.Manager/AcquireLock"
Manager_RefreshLock_FullMethodName = "/manager.v1.Manager/RefreshLock"
Manager_ReleaseLock_FullMethodName = "/manager.v1.Manager/ReleaseLock"
)
// ManagerClient is the client API for Manager service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ManagerClient interface {
AddNode(ctx context.Context, in *Node, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NodeData], error)
AcquireLock(ctx context.Context, in *AcquireLockRequest, opts ...grpc.CallOption) (*LockData, error)
RefreshLock(ctx context.Context, in *LockData, opts ...grpc.CallOption) (*Empty, error)
ReleaseLock(ctx context.Context, in *LockData, opts ...grpc.CallOption) (*Empty, error)
}
type managerClient struct {
cc grpc.ClientConnInterface
}
func NewManagerClient(cc grpc.ClientConnInterface) ManagerClient {
return &managerClient{cc}
}
func (c *managerClient) AddNode(ctx context.Context, in *Node, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NodeData], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &Manager_ServiceDesc.Streams[0], Manager_AddNode_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[Node, NodeData]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Manager_AddNodeClient = grpc.ServerStreamingClient[NodeData]
func (c *managerClient) AcquireLock(ctx context.Context, in *AcquireLockRequest, opts ...grpc.CallOption) (*LockData, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(LockData)
err := c.cc.Invoke(ctx, Manager_AcquireLock_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *managerClient) RefreshLock(ctx context.Context, in *LockData, opts ...grpc.CallOption) (*Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Empty)
err := c.cc.Invoke(ctx, Manager_RefreshLock_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *managerClient) ReleaseLock(ctx context.Context, in *LockData, opts ...grpc.CallOption) (*Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Empty)
err := c.cc.Invoke(ctx, Manager_ReleaseLock_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// ManagerServer is the server API for Manager service.
// All implementations must embed UnimplementedManagerServer
// for forward compatibility.
type ManagerServer interface {
AddNode(*Node, grpc.ServerStreamingServer[NodeData]) error
AcquireLock(context.Context, *AcquireLockRequest) (*LockData, error)
RefreshLock(context.Context, *LockData) (*Empty, error)
ReleaseLock(context.Context, *LockData) (*Empty, error)
mustEmbedUnimplementedManagerServer()
}
// UnimplementedManagerServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedManagerServer struct{}
func (UnimplementedManagerServer) AddNode(*Node, grpc.ServerStreamingServer[NodeData]) error {
return status.Error(codes.Unimplemented, "method AddNode not implemented")
}
func (UnimplementedManagerServer) AcquireLock(context.Context, *AcquireLockRequest) (*LockData, error) {
return nil, status.Error(codes.Unimplemented, "method AcquireLock not implemented")
}
func (UnimplementedManagerServer) RefreshLock(context.Context, *LockData) (*Empty, error) {
return nil, status.Error(codes.Unimplemented, "method RefreshLock not implemented")
}
func (UnimplementedManagerServer) ReleaseLock(context.Context, *LockData) (*Empty, error) {
return nil, status.Error(codes.Unimplemented, "method ReleaseLock not implemented")
}
func (UnimplementedManagerServer) mustEmbedUnimplementedManagerServer() {}
func (UnimplementedManagerServer) testEmbeddedByValue() {}
// UnsafeManagerServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ManagerServer will
// result in compilation errors.
type UnsafeManagerServer interface {
mustEmbedUnimplementedManagerServer()
}
func RegisterManagerServer(s grpc.ServiceRegistrar, srv ManagerServer) {
// If the following call panics, it indicates UnimplementedManagerServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&Manager_ServiceDesc, srv)
}
func _Manager_AddNode_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(Node)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(ManagerServer).AddNode(m, &grpc.GenericServerStream[Node, NodeData]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Manager_AddNodeServer = grpc.ServerStreamingServer[NodeData]
func _Manager_AcquireLock_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AcquireLockRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ManagerServer).AcquireLock(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Manager_AcquireLock_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ManagerServer).AcquireLock(ctx, req.(*AcquireLockRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Manager_RefreshLock_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(LockData)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ManagerServer).RefreshLock(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Manager_RefreshLock_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ManagerServer).RefreshLock(ctx, req.(*LockData))
}
return interceptor(ctx, in, info, handler)
}
func _Manager_ReleaseLock_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(LockData)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ManagerServer).ReleaseLock(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Manager_ReleaseLock_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ManagerServer).ReleaseLock(ctx, req.(*LockData))
}
return interceptor(ctx, in, info, handler)
}
// Manager_ServiceDesc is the grpc.ServiceDesc for Manager service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Manager_ServiceDesc = grpc.ServiceDesc{
ServiceName: "manager.v1.Manager",
HandlerType: (*ManagerServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "AcquireLock",
Handler: _Manager_AcquireLock_Handler,
},
{
MethodName: "RefreshLock",
Handler: _Manager_RefreshLock_Handler,
},
{
MethodName: "ReleaseLock",
Handler: _Manager_ReleaseLock_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "AddNode",
Handler: _Manager_AddNode_Handler,
ServerStreams: true,
},
},
Metadata: "manager/manager.proto",
}

View File

@@ -0,0 +1,203 @@
package server
import (
"context"
"sync"
"github.com/sagernet/sing-box/log"
CS "github.com/sagernet/sing-box/service/manager/constant"
pb "github.com/sagernet/sing-box/service/node_manager/manager"
E "github.com/sagernet/sing/common/exceptions"
"google.golang.org/grpc"
)
type RemoteNode struct {
ctx context.Context
logger log.ContextLogger
stream grpc.ServerStreamingServer[pb.NodeData]
errChan chan error
mtx sync.Mutex
}
func NewRemoteNode(ctx context.Context, logger log.ContextLogger, stream grpc.ServerStreamingServer[pb.NodeData]) (*RemoteNode, chan error) {
errChan := make(chan error)
return &RemoteNode{
ctx: ctx,
logger: logger,
stream: stream,
errChan: errChan,
}, errChan
}
func (s *RemoteNode) UpdateUser(user CS.User) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.send(&pb.NodeData{
Op: pb.OpType_updateUser,
Data: &pb.NodeData_User{User: s.convertUser(user)},
})
}
func (s *RemoteNode) UpdateUsers(users []CS.User) {
s.mtx.Lock()
defer s.mtx.Unlock()
pbUsers := make([]*pb.User, len(users))
for i, user := range users {
pbUsers[i] = s.convertUser(user)
}
s.send(&pb.NodeData{
Op: pb.OpType_updateUsers,
Data: &pb.NodeData_Users{Users: &pb.UserList{Values: pbUsers}},
})
}
func (s *RemoteNode) DeleteUser(user CS.User) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.send(&pb.NodeData{
Op: pb.OpType_deleteUser,
Data: &pb.NodeData_User{User: s.convertUser(user)},
})
}
func (s *RemoteNode) UpdateConnectionLimiter(limiter CS.ConnectionLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.send(&pb.NodeData{
Op: pb.OpType_updateConnectionLimiter,
Data: &pb.NodeData_ConnectionLimiter{ConnectionLimiter: s.convertConnectionLimiter(limiter)},
})
}
func (s *RemoteNode) UpdateConnectionLimiters(limiters []CS.ConnectionLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
pbLimiters := make([]*pb.ConnectionLimiter, len(limiters))
for i, limiters := range limiters {
pbLimiters[i] = s.convertConnectionLimiter(limiters)
}
s.send(&pb.NodeData{
Op: pb.OpType_updateConnectionLimiters,
Data: &pb.NodeData_ConnectionLimiters{ConnectionLimiters: &pb.ConnectionLimiterList{Values: pbLimiters}},
})
}
func (s *RemoteNode) DeleteConnectionLimiter(limiter CS.ConnectionLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.send(&pb.NodeData{
Op: pb.OpType_deleteConnectionLimiter,
Data: &pb.NodeData_ConnectionLimiter{ConnectionLimiter: s.convertConnectionLimiter(limiter)},
})
}
func (s *RemoteNode) UpdateBandwidthLimiter(limiter CS.BandwidthLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.send(&pb.NodeData{
Op: pb.OpType_updateBandwidthLimiter,
Data: &pb.NodeData_BandwidthLimiter{BandwidthLimiter: s.convertBandwidthLimiter(limiter)},
})
}
func (s *RemoteNode) UpdateBandwidthLimiters(limiters []CS.BandwidthLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
pbLimiters := make([]*pb.BandwidthLimiter, len(limiters))
for i, limiters := range limiters {
pbLimiters[i] = s.convertBandwidthLimiter(limiters)
}
s.send(&pb.NodeData{
Op: pb.OpType_updateBandwidthLimiters,
Data: &pb.NodeData_BandwidthLimiters{BandwidthLimiters: &pb.BandwidthLimiterList{Values: pbLimiters}},
})
}
func (s *RemoteNode) DeleteBandwidthLimiter(limiter CS.BandwidthLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.send(&pb.NodeData{
Op: pb.OpType_deleteBandwidthLimiter,
Data: &pb.NodeData_BandwidthLimiter{BandwidthLimiter: s.convertBandwidthLimiter(limiter)},
})
}
func (s *RemoteNode) IsLocal() bool {
return false
}
func (s *RemoteNode) IsOnline() bool {
s.mtx.Lock()
defer s.mtx.Unlock()
select {
case <-s.stream.Context().Done():
return false
default:
return true
}
}
func (s *RemoteNode) Close() error {
s.close(E.New("server connection is closed"))
return nil
}
func (s *RemoteNode) send(data *pb.NodeData) {
select {
case <-s.ctx.Done():
s.close(E.New("server connection is closed"))
return
case <-s.stream.Context().Done():
s.close(E.New("client connection is closed"))
return
default:
}
err := s.stream.Send(data)
if err != nil {
s.close(err)
}
}
func (s *RemoteNode) close(err error) {
s.errChan <- err
close(s.errChan)
}
func (s *RemoteNode) convertUser(user CS.User) *pb.User {
return &pb.User{
Id: int32(user.ID),
Username: user.Username,
Type: user.Type,
Inbound: user.Inbound,
Uuid: user.UUID,
Password: user.Password,
Flow: user.Flow,
AlterId: int32(user.AlterID),
}
}
func (s *RemoteNode) convertConnectionLimiter(limiter CS.ConnectionLimiter) *pb.ConnectionLimiter {
return &pb.ConnectionLimiter{
Id: int32(limiter.ID),
Username: limiter.Username,
Outbound: limiter.Outbound,
Strategy: limiter.Strategy,
ConnectionType: limiter.ConnectionType,
LockType: limiter.LockType,
Count: limiter.Count,
}
}
func (s *RemoteNode) convertBandwidthLimiter(limiter CS.BandwidthLimiter) *pb.BandwidthLimiter {
return &pb.BandwidthLimiter{
Id: int32(limiter.ID),
Username: limiter.Username,
Outbound: limiter.Outbound,
Strategy: limiter.Strategy,
Mode: limiter.Mode,
ConnectionType: limiter.ConnectionType,
Speed: limiter.Speed,
RawSpeed: limiter.RawSpeed,
}
}

View File

@@ -0,0 +1,139 @@
package server
import (
"context"
"errors"
"sync"
"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"
CM "github.com/sagernet/sing-box/service/manager/constant"
pb "github.com/sagernet/sing-box/service/node_manager/manager"
"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"
"golang.org/x/net/http2"
"google.golang.org/grpc"
)
func RegisterService(registry *boxService.Registry) {
boxService.Register[option.NodeManagerServerServiceOptions](registry, C.TypeNodeManagerServer, NewService)
}
type Service struct {
pb.UnimplementedManagerServer
boxService.Adapter
ctx context.Context
logger log.ContextLogger
listener *listener.Listener
tlsConfig tls.ServerConfig
grpcServer *grpc.Server
manager CM.Manager
options option.NodeManagerServerServiceOptions
mtx sync.Mutex
}
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.NodeManagerServerServiceOptions) (adapter.Service, error) {
return &Service{
Adapter: boxService.NewAdapter(C.TypeManager, tag),
ctx: ctx,
logger: logger,
listener: listener.New(listener.Options{
Context: ctx,
Logger: logger,
Network: []string{N.NetworkTCP},
Listen: options.ListenOptions,
}),
options: options,
}, nil
}
func (s *Service) AddNode(node *pb.Node, stream grpc.ServerStreamingServer[pb.NodeData]) error {
remoteNode, errChan := NewRemoteNode(s.ctx, s.logger, stream)
err := s.manager.AddNode(node.Uuid, remoteNode)
if err != nil {
if err == CM.ErrNotFound {
return err
} else {
s.logger.Error(err)
return E.New("internal error")
}
}
return <-errChan
}
func (s *Service) AcquireLock(ctx context.Context, request *pb.AcquireLockRequest) (*pb.LockData, error) {
handleId, err := s.manager.AcquireLock(int(request.LimiterId), request.Id)
if err != nil {
return nil, err
}
return &pb.LockData{HandleId: handleId}, nil
}
func (s *Service) RefreshLock(ctx context.Context, data *pb.LockData) (*pb.Empty, error) {
return nil, s.manager.RefreshLock(int(data.LimiterId), data.Id, data.HandleId)
}
func (s *Service) ReleaseLock(ctx context.Context, data *pb.LockData) (*pb.Empty, error) {
return nil, s.manager.ReleaseLock(int(data.LimiterId), data.Id, data.HandleId)
}
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")
}
s.manager, ok = service.(CM.Manager)
if !ok {
return E.New("invalid", s.options.Manager, " manager")
}
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.grpcServer = grpc.NewServer()
pb.RegisterManagerServer(s.grpcServer, s)
go func() {
err = s.grpcServer.Serve(tcpListener)
if err != nil && !errors.Is(err, grpc.ErrServerStopped) {
s.logger.Error("serve error: ", err)
}
}()
return nil
}
func (s *Service) Close() error {
return nil
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/transport/v2rayhttp"
"github.com/sagernet/sing-box/transport/v2rayhttpupgrade"
"github.com/sagernet/sing-box/transport/v2raykcp"
"github.com/sagernet/sing-box/transport/v2raywebsocket"
xhttp "github.com/sagernet/sing-box/transport/v2rayxhttp"
E "github.com/sagernet/sing/common/exceptions"
@@ -42,6 +43,8 @@ func NewServerTransport(ctx context.Context, logger logger.ContextLogger, option
return v2rayhttpupgrade.NewServer(ctx, logger, options.HTTPUpgradeOptions, tlsConfig, handler)
case C.V2RayTransportTypeXHTTP:
return xhttp.NewServer(ctx, logger, options.XHTTPOptions, tlsConfig, handler)
case C.V2RayTransportTypeKCP:
return v2raykcp.NewServer(ctx, logger, options.KCPOptions, tlsConfig, handler)
default:
return nil, E.New("unknown transport type: " + options.Type)
}
@@ -67,6 +70,8 @@ func NewClientTransport(ctx context.Context, dialer N.Dialer, serverAddr M.Socks
return v2rayhttpupgrade.NewClient(ctx, dialer, serverAddr, options.HTTPUpgradeOptions, tlsConfig)
case C.V2RayTransportTypeXHTTP:
return xhttp.NewClient(ctx, dialer, serverAddr, options.XHTTPOptions, tlsConfig)
case C.V2RayTransportTypeKCP:
return v2raykcp.NewClient(ctx, dialer, serverAddr, options.KCPOptions, tlsConfig)
default:
return nil, E.New("unknown transport type: " + options.Type)
}

View File

@@ -265,3 +265,14 @@ func DupContext(ctx context.Context) context.Context {
}
return log.ContextWithID(context.Background(), id)
}
func HWIDContext(ctx context.Context, headers http.Header) context.Context {
for key, values := range headers {
if strings.ToLower(key) == "x-hwid" {
if len(values) != 0 {
return context.WithValue(ctx, "hwid", values[0])
}
}
}
return ctx
}

View File

@@ -133,7 +133,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
if requestBody != nil {
conn = bufio.NewCachedConn(conn, requestBody)
}
s.handler.NewConnectionEx(DupContext(request.Context()), conn, source, M.Socksaddr{}, nil)
s.handler.NewConnectionEx(HWIDContext(DupContext(request.Context()), request.Header), conn, source, M.Socksaddr{}, nil)
} else {
writer.WriteHeader(http.StatusOK)
done := make(chan struct{})
@@ -141,7 +141,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
NewHTTPConn(request.Body, writer),
writer.(http.Flusher),
})
s.handler.NewConnectionEx(request.Context(), conn, source, M.Socksaddr{}, N.OnceClose(func(it error) {
s.handler.NewConnectionEx(HWIDContext(request.Context(), request.Header), conn, source, M.Socksaddr{}, N.OnceClose(func(it error) {
close(done)
}))
<-done

View File

@@ -112,7 +112,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
s.invalidRequest(writer, request, http.StatusInternalServerError, E.Cause(err, "hijack failed"))
return
}
s.handler.NewConnectionEx(v2rayhttp.DupContext(request.Context()), conn, sHttp.SourceAddress(request), M.Socksaddr{}, nil)
s.handler.NewConnectionEx(v2rayhttp.HWIDContext(v2rayhttp.DupContext(request.Context()), request.Header), conn, sHttp.SourceAddress(request), M.Socksaddr{}, nil)
}
func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Request, statusCode int, err error) {

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