mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-05-14 00:51:12 +03:00
Resolve conflicts
This commit is contained in:
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -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 }}
|
||||
|
||||
150
.goreleaser.yaml
150
.goreleaser.yaml
@@ -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
|
||||
12
Makefile
12
Makefile
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
1
box.go
@@ -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)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
//go:build with_quic
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -7,4 +7,5 @@ const (
|
||||
V2RayTransportTypeGRPC = "grpc"
|
||||
V2RayTransportTypeHTTPUpgrade = "httpupgrade"
|
||||
V2RayTransportTypeXHTTP = "xhttp"
|
||||
V2RayTransportTypeKCP = "mkcp"
|
||||
)
|
||||
|
||||
57
examples/bandwidth_limiter/connection.json
Normal file
57
examples/bandwidth_limiter/connection.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 443,
|
||||
"transport": {
|
||||
"type": "http"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
},
|
||||
{
|
||||
"name": "user2",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "bandwidth-limiter",
|
||||
"tag": "bandwidth-limiter",
|
||||
"strategy": "connection",
|
||||
"mode": "duplex", // download, upload
|
||||
"connection_type": "hwid", // mux, ip
|
||||
"speed": "1MB", // 100KB, 1GB, etc.
|
||||
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
|
||||
"rules": [],
|
||||
"final": "direct"
|
||||
}
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "bandwidth-limiter",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
56
examples/bandwidth_limiter/global.json
Normal file
56
examples/bandwidth_limiter/global.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 443,
|
||||
"transport": {
|
||||
"type": "http"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
},
|
||||
{
|
||||
"name": "user2",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "bandwidth-limiter",
|
||||
"tag": "bandwidth-limiter",
|
||||
"strategy": "global",
|
||||
"mode": "duplex", // download, upload
|
||||
"speed": "1MB", // 100KB, 1GB, etc.
|
||||
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
|
||||
"rules": [],
|
||||
"final": "direct"
|
||||
}
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "bandwidth-limiter",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
78
examples/bandwidth_limiter/multi.json
Normal file
78
examples/bandwidth_limiter/multi.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 443,
|
||||
"transport": {
|
||||
"type": "http"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
},
|
||||
{
|
||||
"name": "user2",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "bandwidth-limiter",
|
||||
"tag": "duplex-bandwidth-limiter",
|
||||
"strategy": "global",
|
||||
"mode": "duplex",
|
||||
"speed": "5MB",
|
||||
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
|
||||
"rules": [],
|
||||
"final": "direct"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "bandwidth-limiter",
|
||||
"tag": "upload-bandwidth-limiter",
|
||||
"strategy": "global",
|
||||
"mode": "upload",
|
||||
"speed": "3MB",
|
||||
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
|
||||
"rules": [],
|
||||
"final": "duplex-bandwidth-limiter"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "bandwidth-limiter",
|
||||
"tag": "download-bandwidth-limiter",
|
||||
"strategy": "global",
|
||||
"mode": "download",
|
||||
"speed": "3MB",
|
||||
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
|
||||
"rules": [],
|
||||
"final": "upload-bandwidth-limiter"
|
||||
}
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "download-bandwidth-limiter",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
70
examples/bandwidth_limiter/users.json
Normal file
70
examples/bandwidth_limiter/users.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 443,
|
||||
"transport": {
|
||||
"type": "http"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
},
|
||||
{
|
||||
"name": "user2",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "bandwidth-limiter",
|
||||
"tag": "bandwidth-limiter",
|
||||
"strategy": "users",
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"strategy": "connection", // global
|
||||
"mode": "duplex", // download, upload
|
||||
"connection_type": "hwid", // mux, ip
|
||||
"speed": "5MB", // 100KB, 1GB, etc.
|
||||
},
|
||||
{
|
||||
"name": "user2",
|
||||
"strategy": "connection", // global
|
||||
"mode": "duplex", // download, upload
|
||||
"connection_type": "hwid", // mux, ip
|
||||
"speed": "1MB", // 100KB, 1GB, etc.
|
||||
},
|
||||
],
|
||||
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
|
||||
"rules": [],
|
||||
"final": "direct"
|
||||
}
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "bandwidth-limiter",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
61
examples/bond/client.json
Normal file
61
examples/bond/client.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "bond",
|
||||
"tag": "bond-out",
|
||||
"outbounds": [ // sum of download_ratio and upload_ratio must be 100
|
||||
{
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
"server": "0.0.0.0",
|
||||
"server_port": 443,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"network": "tcp",
|
||||
"bind_interface": ""
|
||||
},
|
||||
"download_ratio": 50,
|
||||
"upload_ratio": 50
|
||||
},
|
||||
{
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
"server": "0.0.0.0",
|
||||
"server_port": 444,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"network": "tcp",
|
||||
"bind_interface": ""
|
||||
},
|
||||
"download_ratio": 50,
|
||||
"upload_ratio": 50
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "bond-out",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
49
examples/bond/client_multi.json
Normal file
49
examples/bond/client_multi.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "bond",
|
||||
"tag": "bond-out",
|
||||
"outbounds": [ // sum of download_ratio and upload_ratio must be 100
|
||||
{
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
"server": "0.0.0.0",
|
||||
"server_port": 443,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"network": "tcp",
|
||||
},
|
||||
"download_ratio": 20,
|
||||
"upload_ratio": 20,
|
||||
"count": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "bond-out",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
61
examples/bond/client_split.json
Normal file
61
examples/bond/client_split.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "bond",
|
||||
"tag": "bond-out",
|
||||
"outbounds": [ // sum of download_ratio and upload_ratio must be 100
|
||||
{
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
"server": "0.0.0.0",
|
||||
"server_port": 443,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"network": "tcp",
|
||||
"bind_interface": ""
|
||||
},
|
||||
"download_ratio": 100,
|
||||
"upload_ratio": 0
|
||||
},
|
||||
{
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
"server": "0.0.0.0",
|
||||
"server_port": 444,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"network": "tcp",
|
||||
"bind_interface": ""
|
||||
},
|
||||
"download_ratio": 0,
|
||||
"upload_ratio": 100
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "bond-out",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
54
examples/bond/server.json
Normal file
54
examples/bond/server.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "bond",
|
||||
"tag": "bond-in",
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 443,
|
||||
"users": [
|
||||
{
|
||||
"name": "user",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "vless",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 444,
|
||||
"users": [
|
||||
{
|
||||
"name": "user",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
],
|
||||
"route": {
|
||||
"final": "direct",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
56
examples/connection_limiter/connection.json
Normal file
56
examples/connection_limiter/connection.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 443,
|
||||
"transport": {
|
||||
"type": "http"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
},
|
||||
{
|
||||
"name": "user2",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "connection-limiter",
|
||||
"tag": "connection-limiter",
|
||||
"strategy": "connection",
|
||||
"connection_type": "hwid", // mux, ip
|
||||
"count": 5,
|
||||
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
|
||||
"rules": [],
|
||||
"final": "direct"
|
||||
}
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "connection-limiter",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
68
examples/connection_limiter/users.json
Normal file
68
examples/connection_limiter/users.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 5000,
|
||||
"transport": {
|
||||
"type": "http"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
},
|
||||
{
|
||||
"name": "user2",
|
||||
"uuid": "6c8c7ffc-a909-4699-af34-e9d9bcb3e6d6"
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "connection-limiter",
|
||||
"tag": "connection-limiter",
|
||||
"strategy": "users",
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"strategy": "connection",
|
||||
"connection_type": "hwid", // mux, ip
|
||||
"count": 5,
|
||||
},
|
||||
{
|
||||
"name": "user2",
|
||||
"strategy": "connection",
|
||||
"connection_type": "hwid", // mux, ip
|
||||
"count": 1,
|
||||
},
|
||||
],
|
||||
"route": { // https://sing-box.sagernet.org/configuration/route/#structure
|
||||
"rules": [],
|
||||
"final": "direct"
|
||||
}
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "connection-limiter",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
61
examples/failover/client.json
Normal file
61
examples/failover/client.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-1-out",
|
||||
"server": "example1.com",
|
||||
"server_port": 443,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
},
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-2-out",
|
||||
"server": "example2.com",
|
||||
"server_port": 443,
|
||||
"uuid": "294fd6bc-4f89-43e7-9228-7900aba396af"
|
||||
},
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-3-out",
|
||||
"server": "example3.com",
|
||||
"server_port": 443,
|
||||
"uuid": "257f20d0-294a-4f07-9f2c-9efee9a37400"
|
||||
},
|
||||
{
|
||||
"type": "failover",
|
||||
"tag": "failover-out",
|
||||
"outbounds": [
|
||||
"vless-1-out",
|
||||
"vless-2-out",
|
||||
"vless-3-out"
|
||||
]
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "failover-out",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
70
examples/manager/manager.json
Normal file
70
examples/manager/manager.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"port": 53,
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
],
|
||||
"final": "direct-out"
|
||||
},
|
||||
"services": [
|
||||
{
|
||||
"type": "manager",
|
||||
"tag": "my-manager",
|
||||
"database": {
|
||||
"driver": "postgresql",
|
||||
"dsn": "postgresql://postgres:postgres@localhost:5432/manager?sslmode=disable"
|
||||
}
|
||||
},
|
||||
{ // http://127.0.0.1:8000
|
||||
// Username: admin
|
||||
// Password: admin
|
||||
"type": "admin-panel",
|
||||
"tag": "my-admin-panel",
|
||||
"listen_port": 8000,
|
||||
"manager": "my-manager",
|
||||
"database": {
|
||||
"driver": "postgresql",
|
||||
"dsn": "postgresql://postgres:postgres@localhost:5432/adminpanel?sslmode=disable"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node-manager-server", // for connecting nodes
|
||||
"listen_port": 7000,
|
||||
"manager": "my-manager",
|
||||
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#inbound
|
||||
"enabled": true,
|
||||
"server_name": "example.com",
|
||||
"certificate_path": "/path/to/fullchain.pem",
|
||||
"key_path": "/path/to/privkey.pem"
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
85
examples/manager/node.json
Normal file
85
examples/manager/node.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 443,
|
||||
"transport": {
|
||||
"type": "http"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
},
|
||||
{
|
||||
"type": "bandwidth-limiter",
|
||||
"tag": "bandwidth-limiter",
|
||||
"strategy": "manager",
|
||||
"route": {
|
||||
"final": "direct-out"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "connection-limiter",
|
||||
"tag": "connection-limiter",
|
||||
"strategy": "manager",
|
||||
"route": {
|
||||
"final": "bandwidth-limiter"
|
||||
}
|
||||
},
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"port": 53,
|
||||
"outbound": "dns-out"
|
||||
}
|
||||
],
|
||||
"final": "connection-limiter"
|
||||
},
|
||||
"services": [
|
||||
{
|
||||
"type": "node",
|
||||
"tag": "my-node",
|
||||
"uuid": "e6eceb84-ad66-474b-8641-142499db7c6e",
|
||||
"manager": "node-manager",
|
||||
"inbounds": ["vless-in"],
|
||||
"bandwidth_limiters": ["bandwidth-limiter"],
|
||||
"connection_limiters": ["connection-limiter"],
|
||||
},
|
||||
{
|
||||
"type": "node-manager-client",
|
||||
"tag": "node-manager",
|
||||
"server": "example.com",
|
||||
"server_port": 7000,
|
||||
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#outbound
|
||||
"enabled": true,
|
||||
"server_name": "example.com",
|
||||
"alpn": "h2" // h3 for QUIC
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
43
examples/mkcp/client.json
Normal file
43
examples/mkcp/client.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-out",
|
||||
"server": "example.com",
|
||||
"server_port": 443,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"packet_encoding": "",
|
||||
"transport": {
|
||||
"type": "kcp",
|
||||
"mtu": 1500
|
||||
}
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "vless-out",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
42
examples/mkcp/server.json
Normal file
42
examples/mkcp/server.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 443,
|
||||
"users": [
|
||||
{
|
||||
"name": "user",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
],
|
||||
"transport": {
|
||||
"type": "kcp",
|
||||
"mtu": 1500
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "direct",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel_server",
|
||||
"type": "tunnel-server",
|
||||
"tag": "tunnel",
|
||||
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13",
|
||||
"users": [
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel_server",
|
||||
"type": "tunnel-server",
|
||||
"tag": "tunnel",
|
||||
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13",
|
||||
"users": [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": [
|
||||
|
||||
88
go.mod
88
go.mod
@@ -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
|
||||
@@ -43,41 +48,54 @@ require (
|
||||
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/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
|
||||
@@ -88,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
|
||||
@@ -103,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
|
||||
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
|
||||
@@ -123,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
|
||||
@@ -133,25 +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/time v0.11.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.2.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
|
||||
|
||||
391
go.sum
391
go.sum
@@ -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,44 +50,93 @@ 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=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20190707035753-2be1aa521ff4/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20200206145737-bbfc9a55622e h1:LzwWXEScfcTu7vUZNlDDWDARoSGEtvlDKK2BYHowNeE=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20200206145737-bbfc9a55622e/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU=
|
||||
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
|
||||
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
|
||||
github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
|
||||
github.com/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=
|
||||
@@ -70,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=
|
||||
@@ -119,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=
|
||||
@@ -137,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=
|
||||
@@ -173,8 +363,6 @@ github.com/sagernet/quic-go v0.52.0-sing-box-mod.3 h1:ySqffGm82rPqI1TUPqmtHIYd12
|
||||
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
|
||||
github.com/sagernet/sing v0.7.18 h1:iZHkaru1/MoHugx3G+9S3WG4owMewKO/KvieE2Pzk4E=
|
||||
github.com/sagernet/sing v0.7.18/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
|
||||
github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
|
||||
github.com/sagernet/sing-quic v0.5.3 h1:K937DKJN98xqyztijRkLJqbBfyV4rEZcYxFyP3EBikU=
|
||||
github.com/sagernet/sing-quic v0.5.3/go.mod h1:evP1e++ZG8TJHVV5HudXV4vWeYzGfCdF4HwSJZcdqkI=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
|
||||
@@ -193,18 +381,28 @@ github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUG
|
||||
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.2.0 h1:o/AAMCZPDCrwat2m0rAicFJ+iHfuzBR4nNueORUiEtM=
|
||||
github.com/shtorm-7/wireguard-go v0.0.1-beta.7-extended-1.2.0/go.mod h1:3Ps4sTih9KeKik6xsMdIa+2TWDgTb+ysnq+ztxespk8=
|
||||
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.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.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=
|
||||
@@ -233,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=
|
||||
@@ -270,69 +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.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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
27
log/id.go
27
log/id.go
@@ -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
13
option/admin_panel.go
Normal 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
16
option/bond.go
Normal 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"`
|
||||
}
|
||||
@@ -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
37
option/limiter.go
Normal 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
11
option/manager.go
Normal 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
9
option/node.go
Normal 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
13
option/node_manager.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package option
|
||||
|
||||
type NodeManagerServerServiceOptions struct {
|
||||
ListenOptions
|
||||
InboundTLSOptionsContainer
|
||||
Manager string `json:"manager"`
|
||||
}
|
||||
|
||||
type NodeManagerClientServiceOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
OutboundTLSOptionsContainer
|
||||
}
|
||||
@@ -21,6 +21,7 @@ type _V2RayTransportOptions struct {
|
||||
GRPCOptions V2RayGRPCOptions `json:"-"`
|
||||
HTTPUpgradeOptions V2RayHTTPUpgradeOptions `json:"-"`
|
||||
XHTTPOptions V2RayXHTTPOptions `json:"-"`
|
||||
KCPOptions V2RayKCPOptions `json:"-"`
|
||||
}
|
||||
|
||||
type V2RayTransportOptions _V2RayTransportOptions
|
||||
@@ -40,6 +41,8 @@ func (o V2RayTransportOptions) MarshalJSON() ([]byte, error) {
|
||||
v = o.HTTPUpgradeOptions
|
||||
case C.V2RayTransportTypeXHTTP:
|
||||
v = o.XHTTPOptions
|
||||
case C.V2RayTransportTypeKCP:
|
||||
v = o.KCPOptions
|
||||
case "":
|
||||
return nil, E.New("missing transport type")
|
||||
default:
|
||||
@@ -67,6 +70,8 @@ func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error {
|
||||
v = &o.HTTPUpgradeOptions
|
||||
case C.V2RayTransportTypeXHTTP:
|
||||
v = &o.XHTTPOptions
|
||||
case C.V2RayTransportTypeKCP:
|
||||
v = &o.KCPOptions
|
||||
default:
|
||||
return E.New("unknown transport type: " + o.Type)
|
||||
}
|
||||
@@ -468,3 +473,64 @@ func (m *V2RayXHTTPXmuxOptions) GetNormalizedHMaxRequestTimes() Xbadoption.Range
|
||||
func (m *V2RayXHTTPXmuxOptions) GetNormalizedHMaxReusableSecs() Xbadoption.Range {
|
||||
return m.HMaxReusableSecs
|
||||
}
|
||||
|
||||
type V2RayKCPOptions struct {
|
||||
MTU uint32 `json:"mtu,omitempty"`
|
||||
TTI uint32 `json:"tti,omitempty"`
|
||||
UplinkCapacity uint32 `json:"uplink_capacity,omitempty"`
|
||||
DownlinkCapacity uint32 `json:"downlink_capacity,omitempty"`
|
||||
Congestion bool `json:"congestion,omitempty"`
|
||||
ReadBufferSize uint32 `json:"read_buffer_size,omitempty"`
|
||||
WriteBufferSize uint32 `json:"write_buffer_size,omitempty"`
|
||||
HeaderType string `json:"header_type,omitempty"`
|
||||
Seed string `json:"seed,omitempty"`
|
||||
}
|
||||
|
||||
func (k *V2RayKCPOptions) GetMTU() uint32 {
|
||||
if k.MTU == 0 {
|
||||
return 1350
|
||||
}
|
||||
return k.MTU
|
||||
}
|
||||
|
||||
func (k *V2RayKCPOptions) GetTTI() uint32 {
|
||||
if k.TTI == 0 {
|
||||
return 50
|
||||
}
|
||||
return k.TTI
|
||||
}
|
||||
|
||||
func (k *V2RayKCPOptions) GetUplinkCapacity() uint32 {
|
||||
if k.UplinkCapacity == 0 {
|
||||
return 12
|
||||
}
|
||||
return k.UplinkCapacity
|
||||
}
|
||||
|
||||
func (k *V2RayKCPOptions) GetDownlinkCapacity() uint32 {
|
||||
if k.DownlinkCapacity == 0 {
|
||||
return 100
|
||||
}
|
||||
return k.DownlinkCapacity
|
||||
}
|
||||
|
||||
func (k *V2RayKCPOptions) GetReadBufferSize() uint32 {
|
||||
if k.ReadBufferSize == 0 {
|
||||
return 1
|
||||
}
|
||||
return k.ReadBufferSize
|
||||
}
|
||||
|
||||
func (k *V2RayKCPOptions) GetWriteBufferSize() uint32 {
|
||||
if k.WriteBufferSize == 0 {
|
||||
return 1
|
||||
}
|
||||
return k.WriteBufferSize
|
||||
}
|
||||
|
||||
func (k *V2RayKCPOptions) GetHeaderType() string {
|
||||
if k.HeaderType == "" {
|
||||
return "none"
|
||||
}
|
||||
return k.HeaderType
|
||||
}
|
||||
|
||||
@@ -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
164
protocol/bond/conn.go
Normal 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
146
protocol/bond/inbound.go
Normal 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
152
protocol/bond/outbound.go
Normal 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
100
protocol/bond/protocol.go
Normal 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
41
protocol/bond/router.go
Normal 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)
|
||||
}
|
||||
96
protocol/group/failover.go
Normal file
96
protocol/group/failover.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
}
|
||||
|
||||
158
protocol/limiter/bandwidth/limiter.go
Normal file
158
protocol/limiter/bandwidth/limiter.go
Normal 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()
|
||||
}
|
||||
146
protocol/limiter/bandwidth/outbound.go
Normal file
146
protocol/limiter/bandwidth/outbound.go
Normal 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
|
||||
}
|
||||
266
protocol/limiter/bandwidth/strategy.go
Normal file
266
protocol/limiter/bandwidth/strategy.go
Normal 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)
|
||||
}
|
||||
37
protocol/limiter/connection/lock.go
Normal file
37
protocol/limiter/connection/lock.go
Normal 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
|
||||
}
|
||||
}
|
||||
204
protocol/limiter/connection/outbound.go
Normal file
204
protocol/limiter/connection/outbound.go
Normal 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()
|
||||
}
|
||||
119
protocol/limiter/connection/strategy.go
Normal file
119
protocol/limiter/connection/strategy.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
400
service/admin_panel/migration/postgresql.go
Normal file
400
service/admin_panel/migration/postgresql.go
Normal 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()
|
||||
}
|
||||
13
service/admin_panel/pages/dashboard.go
Normal file
13
service/admin_panel/pages/dashboard.go
Normal 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
|
||||
}
|
||||
188
service/admin_panel/service.go
Normal file
188
service/admin_panel/service.go
Normal 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,
|
||||
)
|
||||
}
|
||||
20
service/admin_panel/service_stub.go
Normal file
20
service/admin_panel/service_stub.go
Normal 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`)
|
||||
})
|
||||
}
|
||||
259
service/admin_panel/tables/bandwidth_limiter.go
Normal file
259
service/admin_panel/tables/bandwidth_limiter.go
Normal 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
|
||||
}
|
||||
}
|
||||
261
service/admin_panel/tables/connection_limiter.go
Normal file
261
service/admin_panel/tables/connection_limiter.go
Normal 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
|
||||
}
|
||||
}
|
||||
201
service/admin_panel/tables/node.go
Normal file
201
service/admin_panel/tables/node.go
Normal 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
|
||||
}
|
||||
}
|
||||
164
service/admin_panel/tables/squad.go
Normal file
164
service/admin_panel/tables/squad.go
Normal 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
|
||||
}
|
||||
}
|
||||
282
service/admin_panel/tables/user.go
Normal file
282
service/admin_panel/tables/user.go
Normal 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
|
||||
}
|
||||
}
|
||||
164
service/manager/constant/dto.go
Normal file
164
service/manager/constant/dto.go
Normal 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"`
|
||||
}
|
||||
5
service/manager/constant/error.go
Normal file
5
service/manager/constant/error.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package constant
|
||||
|
||||
import E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
var ErrNotFound = E.New("not found")
|
||||
48
service/manager/constant/manager.go
Normal file
48
service/manager/constant/manager.go
Normal 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)
|
||||
}
|
||||
20
service/manager/constant/node.go
Normal file
20
service/manager/constant/node.go
Normal 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
|
||||
}
|
||||
38
service/manager/constant/repository.go
Normal file
38
service/manager/constant/repository.go
Normal 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)
|
||||
}
|
||||
155
service/manager/repository/postgresql/filter.go
Normal file
155
service/manager/repository/postgresql/filter.go
Normal 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
|
||||
}
|
||||
}
|
||||
132
service/manager/repository/postgresql/migration.go
Normal file
132
service/manager/repository/postgresql/migration.go
Normal 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()
|
||||
}
|
||||
1347
service/manager/repository/postgresql/repository.go
Normal file
1347
service/manager/repository/postgresql/repository.go
Normal file
File diff suppressed because it is too large
Load Diff
598
service/manager/service.go
Normal file
598
service/manager/service.go
Normal 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
|
||||
}
|
||||
20
service/manager/service_stub.go
Normal file
20
service/manager/service_stub.go
Normal 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`)
|
||||
})
|
||||
}
|
||||
18
service/node/constant/bandwidth.go
Normal file
18
service/node/constant/bandwidth.go
Normal 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)
|
||||
}
|
||||
18
service/node/constant/connection.go
Normal file
18
service/node/constant/connection.go
Normal 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)
|
||||
}
|
||||
18
service/node/constant/inbound.go
Normal file
18
service/node/constant/inbound.go
Normal 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)
|
||||
}
|
||||
88
service/node/inbound/hysteria.go
Normal file
88
service/node/inbound/hysteria.go
Normal 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()
|
||||
}
|
||||
88
service/node/inbound/hysteria2.go
Normal file
88
service/node/inbound/hysteria2.go
Normal 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()
|
||||
}
|
||||
88
service/node/inbound/trojan.go
Normal file
88
service/node/inbound/trojan.go
Normal 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()
|
||||
}
|
||||
88
service/node/inbound/tuic.go
Normal file
88
service/node/inbound/tuic.go
Normal 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()
|
||||
}
|
||||
88
service/node/inbound/vless.go
Normal file
88
service/node/inbound/vless.go
Normal 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()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user