mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-06-09 21:08:15 +03:00
Compare commits
4 Commits
v1.13.12-e
...
v1.13.12-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ff7a84afe | ||
|
|
e363c2ff78 | ||
|
|
195a33379d | ||
|
|
9b3da79c32 |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -22,4 +22,11 @@ CLAUDE.md
|
||||
AGENTS.md
|
||||
/.claude/
|
||||
dist
|
||||
logs
|
||||
logs
|
||||
/*.so
|
||||
/*.log
|
||||
/*.db-shm
|
||||
/*.db-wal
|
||||
/*.db.backup.*
|
||||
/test_download.bin
|
||||
/wget-log
|
||||
@@ -11,6 +11,7 @@ builds:
|
||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
||||
- -s
|
||||
- -buildid=
|
||||
- -checklinkname=0
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
@@ -22,8 +23,13 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOTOOLCHAIN=local
|
||||
@@ -54,6 +60,11 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
targets:
|
||||
- linux_mips
|
||||
- linux_mips_softfloat
|
||||
@@ -107,9 +118,14 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_naive_outbound
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_purego
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
@@ -134,9 +150,14 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_naive_outbound
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_purego
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
@@ -161,9 +182,14 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_naive_outbound
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_purego
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
@@ -188,9 +214,14 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_naive_outbound
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_purego
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
@@ -215,8 +246,13 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
@@ -258,8 +294,13 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_musl
|
||||
env:
|
||||
@@ -309,6 +350,11 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
targets:
|
||||
- linux_mips
|
||||
- linux_mips_softfloat
|
||||
@@ -372,16 +418,6 @@ archives:
|
||||
files:
|
||||
- LICENSE
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}-{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||
- id: archive-naive-glibc
|
||||
<<: *template
|
||||
builds:
|
||||
- naive-glibc
|
||||
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 }}-glibc'
|
||||
- id: archive-naive-musl
|
||||
<<: *template
|
||||
builds:
|
||||
- naive-musl
|
||||
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 }}-musl'
|
||||
- id: archive-naive-purego-linux-amd64
|
||||
<<: *template
|
||||
builds:
|
||||
@@ -418,11 +454,16 @@ archives:
|
||||
- src: dist/naive-purego-windows-arm64_*/libcronet*
|
||||
strip_parent: true
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-purego'
|
||||
- id: archive-legacy
|
||||
- id: archive-naive-glibc
|
||||
<<: *template
|
||||
builds:
|
||||
- legacy
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-legacy'
|
||||
- naive-glibc
|
||||
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 }}-glibc'
|
||||
- id: archive-naive-musl
|
||||
<<: *template
|
||||
builds:
|
||||
- naive-musl
|
||||
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 }}-musl'
|
||||
- id: archive-compressed
|
||||
<<: *template
|
||||
builds:
|
||||
|
||||
3
Makefile
3
Makefile
@@ -123,12 +123,11 @@ build_android:
|
||||
upload_android:
|
||||
mkdir -p dist/release_android
|
||||
cp ../sing-box-for-android/app/build/outputs/apk/other/release/*.apk dist/release_android
|
||||
cp ../sing-box-for-android/app/build/outputs/apk/otherLegacy/release/*.apk dist/release_android
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
|
||||
./codeberg-release.sh --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
|
||||
rm -rf dist/release_android
|
||||
|
||||
release_android: lib_android update_android_version build_android
|
||||
release_android: lib_android update_android_version build_android upload_android
|
||||
|
||||
publish_android:
|
||||
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop
|
||||
|
||||
@@ -8,11 +8,14 @@ Sing-box with extended features.
|
||||
|
||||
## 🔥 Features
|
||||
|
||||
### Outbounds
|
||||
### Protocols
|
||||
- **WARP** — Cloudflare WARP integration through WireGuard
|
||||
- **MASQUE** — Cloudflare MASQUE proxy over QUIC / HTTP-2
|
||||
- **MTProxy** — Telegram MTProxy server with FakeTLS and domain fronting
|
||||
- **Mieru** — Secure, hard to classify, hard to probe network protocol
|
||||
- **OpenVPN** — OpenVPN client with tls-auth, tls-crypt, and tls-crypt-v2 support
|
||||
- **TrustTunnel** — AdGuard's obfuscated VPN protocol, indistinguishable from HTTPS traffic
|
||||
- **Sudoku** — Traffic obfuscation protocol based on 4×4 Sudoku puzzles with low-entropy fingerprints
|
||||
- **VPN** — Routed tunnel over any TCP sing-box protocol
|
||||
- **Bond** — Link aggregation for increasing throughput
|
||||
- **Fallback** — Outbound group with priority-based switching
|
||||
|
||||
@@ -3,6 +3,8 @@ package provider
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -34,10 +36,11 @@ type Adapter struct {
|
||||
callbackAccess sync.Mutex
|
||||
callbacks list.List[adapter.ProviderUpdateCallback]
|
||||
|
||||
link string
|
||||
enabled bool
|
||||
timeout time.Duration
|
||||
interval time.Duration
|
||||
link string
|
||||
enabled bool
|
||||
removeEmojis bool
|
||||
timeout time.Duration
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
func NewAdapter(ctx context.Context, router adapter.Router, outbound adapter.OutboundManager, logFactory log.Factory, logger log.ContextLogger, providerTag string, providerType string, options option.ProviderHealthCheckOptions) Adapter {
|
||||
@@ -68,6 +71,10 @@ func NewAdapter(ctx context.Context, router adapter.Router, outbound adapter.Out
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) SetRemoveEmojis(remove bool) {
|
||||
a.removeEmojis = remove
|
||||
}
|
||||
|
||||
func (a *Adapter) Start() error {
|
||||
a.history = service.FromContext[adapter.URLTestHistoryStorage](a.ctx)
|
||||
if a.history == nil {
|
||||
@@ -102,6 +109,10 @@ func (a *Adapter) Outbound(tag string) (adapter.Outbound, bool) {
|
||||
}
|
||||
|
||||
func (a *Adapter) UpdateOutbounds(oldOpts []option.Outbound, newOpts []option.Outbound) {
|
||||
if a.removeEmojis {
|
||||
removeEmojisFromTags(newOpts)
|
||||
}
|
||||
uniquifyTags(newOpts)
|
||||
a.removeUseless(newOpts)
|
||||
var (
|
||||
oldOptByTag = make(map[string]option.Outbound)
|
||||
@@ -265,3 +276,24 @@ func (a *Adapter) removeUseless(newOpts []option.Outbound) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func uniquifyTags(opts []option.Outbound) {
|
||||
count := make(map[string]int)
|
||||
for i, opt := range opts {
|
||||
count[opt.Tag]++
|
||||
if count[opt.Tag] > 1 {
|
||||
opts[i].Tag = F.ToString(opt.Tag, " #", count[opt.Tag])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeEmojisFromTags(opts []option.Outbound) {
|
||||
for i, opt := range opts {
|
||||
cleaned := emojiRegex.ReplaceAllString(opt.Tag, "")
|
||||
cleaned = multiSpaceRegex.ReplaceAllString(cleaned, " ")
|
||||
opts[i].Tag = strings.TrimSpace(cleaned)
|
||||
}
|
||||
}
|
||||
|
||||
var emojiRegex = regexp.MustCompile(`[\x{1F1E0}-\x{1F1FF}\x{1F300}-\x{1F9FF}\x{2600}-\x{27BF}\x{FE00}-\x{FE0F}\x{200D}]+`)
|
||||
var multiSpaceRegex = regexp.MustCompile(`\s{2,}`)
|
||||
|
||||
@@ -63,7 +63,7 @@ func init() {
|
||||
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0")
|
||||
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0")
|
||||
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_masque", "with_mtproxy", "with_utls", "with_naive_outbound", "with_clash_api", "badlinkname", "tfogo_checklinkname0")
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_masque", "with_mtproxy", "with_trusttunnel", "with_openvpn", "with_sudoku", "with_utls", "with_naive_outbound", "with_clash_api", "badlinkname", "tfogo_checklinkname0")
|
||||
darwinTags = append(darwinTags, "with_dhcp", "grpcnotrace")
|
||||
// memcTags = append(memcTags, "with_tailscale")
|
||||
sharedTags = append(sharedTags, "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird")
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
package byteformats
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
var (
|
||||
unitNames = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
|
||||
iUnitNames = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
||||
kUnitNames = []string{"kB", "MB", "GB", "TB", "PB", "EB"}
|
||||
kiUnitNames = []string{"KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
||||
)
|
||||
|
||||
func formatBytes(s uint64, base float64, sizes []string) string {
|
||||
if s < 10 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
e := math.Floor(logn(float64(s), base))
|
||||
suffix := sizes[int(e)]
|
||||
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
}
|
||||
|
||||
func formatKBytes(s uint64, base float64, sizes []string) string {
|
||||
if s == 0 {
|
||||
return fmt.Sprintf("0 %s", sizes[0])
|
||||
}
|
||||
e := math.Floor(logn(float64(s), base))
|
||||
if e < 1 {
|
||||
e = 1
|
||||
}
|
||||
suffix := sizes[int(e)-1]
|
||||
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
}
|
||||
|
||||
func logn(n, b float64) float64 {
|
||||
return math.Log(n) / math.Log(b)
|
||||
}
|
||||
|
||||
func FormatBytes(s uint64) string {
|
||||
return formatBytes(s, 1000, unitNames)
|
||||
}
|
||||
|
||||
func FormatMemoryBytes(s uint64) string {
|
||||
return formatBytes(s, 1024, unitNames)
|
||||
}
|
||||
|
||||
func FormatIBytes(s uint64) string {
|
||||
return formatBytes(s, 1024, iUnitNames)
|
||||
}
|
||||
|
||||
func FormatKBytes(s uint64) string {
|
||||
return formatKBytes(s, 1000, kUnitNames)
|
||||
}
|
||||
|
||||
func FormatMemoryKBytes(s uint64) string {
|
||||
return formatKBytes(s, 1024, kUnitNames)
|
||||
}
|
||||
|
||||
func FormatKIBytes(s uint64) string {
|
||||
return formatKBytes(s, 1024, kiUnitNames)
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
package byteformats
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
Byte = 1 << (iota * 10)
|
||||
KiByte
|
||||
MiByte
|
||||
GiByte
|
||||
TiByte
|
||||
PiByte
|
||||
EiByte
|
||||
)
|
||||
|
||||
const (
|
||||
KByte = Byte * 1000
|
||||
MByte = KByte * 1000
|
||||
GByte = MByte * 1000
|
||||
TByte = GByte * 1000
|
||||
PByte = TByte * 1000
|
||||
EByte = PByte * 1000
|
||||
)
|
||||
|
||||
var unitValueTable = map[string]uint64{
|
||||
"b": Byte,
|
||||
"k": KByte,
|
||||
"kb": KByte,
|
||||
"ki": KiByte,
|
||||
"kib": KiByte,
|
||||
"m": MByte,
|
||||
"mb": MByte,
|
||||
"mi": MiByte,
|
||||
"mib": MiByte,
|
||||
"g": GByte,
|
||||
"gb": GByte,
|
||||
"gi": GiByte,
|
||||
"gib": GiByte,
|
||||
"t": TByte,
|
||||
"tb": TByte,
|
||||
"ti": TiByte,
|
||||
"tib": TiByte,
|
||||
"p": PByte,
|
||||
"pb": PByte,
|
||||
"pi": PiByte,
|
||||
"pib": PiByte,
|
||||
"e": EByte,
|
||||
"eb": EByte,
|
||||
"ei": EiByte,
|
||||
"eib": EiByte,
|
||||
}
|
||||
|
||||
var memoryUnitValueTable = map[string]uint64{
|
||||
"b": Byte,
|
||||
"k": KiByte,
|
||||
"kb": KiByte,
|
||||
"m": MiByte,
|
||||
"mb": MiByte,
|
||||
"g": GiByte,
|
||||
"gb": GiByte,
|
||||
"t": TiByte,
|
||||
"tb": TiByte,
|
||||
"p": PiByte,
|
||||
"pb": PiByte,
|
||||
"e": EiByte,
|
||||
"eb": EiByte,
|
||||
}
|
||||
|
||||
var networkUnitValueTable = map[string]uint64{
|
||||
"Bps": Byte,
|
||||
"Kbps": KByte / 8,
|
||||
"KBps": KByte,
|
||||
"Mbps": MByte / 8,
|
||||
"MBps": MByte,
|
||||
"Gbps": GByte / 8,
|
||||
"GBps": GByte,
|
||||
"Tbps": TByte / 8,
|
||||
"TBps": TByte,
|
||||
"Pbps": PByte / 8,
|
||||
"PBps": PByte,
|
||||
"Ebps": EByte / 8,
|
||||
"EBps": EByte,
|
||||
}
|
||||
|
||||
type rawBytes struct {
|
||||
value uint64
|
||||
unit string
|
||||
unitValue uint64
|
||||
}
|
||||
|
||||
func (b rawBytes) MarshalJSON() ([]byte, error) {
|
||||
if b.unit == "" {
|
||||
return json.Marshal(b.value)
|
||||
}
|
||||
return json.Marshal(strconv.FormatUint(b.value/b.unitValue, 10) + b.unit)
|
||||
}
|
||||
|
||||
func parseUnit(b *rawBytes, unitTable map[string]uint64, caseSensitive bool, bytes []byte) error {
|
||||
var intValue int64
|
||||
err := json.Unmarshal(bytes, &intValue)
|
||||
if err == nil {
|
||||
b.value = uint64(intValue)
|
||||
b.unit = ""
|
||||
b.unitValue = 1
|
||||
return nil
|
||||
}
|
||||
var stringValue string
|
||||
err = json.Unmarshal(bytes, &stringValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(stringValue) == "" {
|
||||
b.value = 0
|
||||
b.unit = ""
|
||||
b.unitValue = 1
|
||||
return nil
|
||||
}
|
||||
unitIndex := 0
|
||||
for i, c := range stringValue {
|
||||
if c < '0' || c > '9' {
|
||||
unitIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if unitIndex == 0 {
|
||||
return fmt.Errorf("invalid format: %s", stringValue)
|
||||
}
|
||||
value, err := strconv.ParseUint(stringValue[:unitIndex], 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse %s: %w", stringValue[:unitIndex], err)
|
||||
}
|
||||
rawUnit := stringValue[unitIndex:]
|
||||
var unit string
|
||||
if caseSensitive {
|
||||
unit = strings.TrimSpace(rawUnit)
|
||||
} else {
|
||||
unit = strings.TrimSpace(strings.ToLower(rawUnit))
|
||||
}
|
||||
unitValue, loaded := unitTable[unit]
|
||||
if !loaded {
|
||||
return fmt.Errorf("unsupported unit: %s", rawUnit)
|
||||
}
|
||||
b.value = value * unitValue
|
||||
b.unit = rawUnit
|
||||
b.unitValue = unitValue
|
||||
return nil
|
||||
}
|
||||
|
||||
type Bytes struct {
|
||||
rawBytes
|
||||
}
|
||||
|
||||
func (b *Bytes) Value() uint64 {
|
||||
if b == nil {
|
||||
return 0
|
||||
}
|
||||
return b.value
|
||||
}
|
||||
|
||||
func (b *Bytes) UnmarshalJSON(bytes []byte) error {
|
||||
return parseUnit(&b.rawBytes, unitValueTable, false, bytes)
|
||||
}
|
||||
|
||||
type MemoryBytes struct {
|
||||
rawBytes
|
||||
}
|
||||
|
||||
func (m *MemoryBytes) Value() uint64 {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
return m.value
|
||||
}
|
||||
|
||||
func (m *MemoryBytes) UnmarshalJSON(bytes []byte) error {
|
||||
return parseUnit(&m.rawBytes, memoryUnitValueTable, false, bytes)
|
||||
}
|
||||
|
||||
type NetworkBytes struct {
|
||||
rawBytes
|
||||
}
|
||||
|
||||
func (n *NetworkBytes) Value() uint64 {
|
||||
if n == nil {
|
||||
return 0
|
||||
}
|
||||
return n.value
|
||||
}
|
||||
|
||||
func (n *NetworkBytes) UnmarshalJSON(bytes []byte) error {
|
||||
return parseUnit(&n.rawBytes, networkUnitValueTable, true, bytes)
|
||||
}
|
||||
|
||||
type NetworkBytesCompat struct {
|
||||
rawBytes
|
||||
}
|
||||
|
||||
func (n *NetworkBytesCompat) Value() uint64 {
|
||||
if n == nil {
|
||||
return 0
|
||||
}
|
||||
return n.value
|
||||
}
|
||||
|
||||
func (n *NetworkBytesCompat) UnmarshalJSON(bytes []byte) error {
|
||||
err := parseUnit(&n.rawBytes, networkUnitValueTable, true, bytes)
|
||||
if err != nil {
|
||||
newErr := parseUnit(&n.rawBytes, unitValueTable, false, bytes)
|
||||
if newErr == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package byteformats_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/common/byteformats"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNetworkBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMap := map[string]uint64{
|
||||
"1 Bps": byteformats.Byte,
|
||||
"1 Kbps": byteformats.KByte / 8,
|
||||
"1 KBps": byteformats.KByte,
|
||||
"1 Mbps": byteformats.MByte / 8,
|
||||
"1 MBps": byteformats.MByte,
|
||||
"1 Gbps": byteformats.GByte / 8,
|
||||
"1 GBps": byteformats.GByte,
|
||||
"1 Tbps": byteformats.TByte / 8,
|
||||
"1 TBps": byteformats.TByte,
|
||||
"1 Pbps": byteformats.PByte / 8,
|
||||
"1 PBps": byteformats.PByte,
|
||||
"1k": byteformats.KByte,
|
||||
"1m": byteformats.MByte,
|
||||
}
|
||||
for k, v := range testMap {
|
||||
var nb byteformats.NetworkBytesCompat
|
||||
require.NoError(t, json.Unmarshal([]byte("\""+k+"\""), &nb))
|
||||
require.Equal(t, v, nb.Value())
|
||||
b, err := json.Marshal(nb)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "\""+k+"\"", string(b))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemoryBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMap := map[string]uint64{
|
||||
"1 B": byteformats.Byte,
|
||||
"1 KB": byteformats.KiByte,
|
||||
"1 MB": byteformats.MiByte,
|
||||
"1 GB": byteformats.GiByte,
|
||||
"1 TB": byteformats.TiByte,
|
||||
"1 PB": byteformats.PiByte,
|
||||
}
|
||||
for k, v := range testMap {
|
||||
var mb byteformats.MemoryBytes
|
||||
require.NoError(t, json.Unmarshal([]byte("\""+k+"\""), &mb))
|
||||
require.Equal(t, v, mb.Value())
|
||||
b, err := json.Marshal(mb)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "\""+k+"\"", string(b))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMap := map[string]uint64{
|
||||
"1 B": byteformats.Byte,
|
||||
"1 KB": byteformats.KByte,
|
||||
"1 KiB": byteformats.KiByte,
|
||||
"1 MB": byteformats.MByte,
|
||||
"1 MiB": byteformats.MiByte,
|
||||
"1 GB": byteformats.GByte,
|
||||
"1 GiB": byteformats.GiByte,
|
||||
"1 TB": byteformats.TByte,
|
||||
"1 TiB": byteformats.TiByte,
|
||||
"1 PB": byteformats.PByte,
|
||||
"1 PiB": byteformats.PiByte,
|
||||
"1 EB": byteformats.EByte,
|
||||
"1 EiB": byteformats.EiByte,
|
||||
"1k": byteformats.KByte,
|
||||
"1m": byteformats.MByte,
|
||||
"1g": byteformats.GByte,
|
||||
"1t": byteformats.TByte,
|
||||
"1p": byteformats.PByte,
|
||||
"1e": byteformats.EByte,
|
||||
"1K": byteformats.KByte,
|
||||
"1M": byteformats.MByte,
|
||||
"1G": byteformats.GByte,
|
||||
"1T": byteformats.TByte,
|
||||
"1P": byteformats.PByte,
|
||||
"1E": byteformats.EByte,
|
||||
"1Ki": byteformats.KiByte,
|
||||
"1Mi": byteformats.MiByte,
|
||||
"1Gi": byteformats.GiByte,
|
||||
"1Ti": byteformats.TiByte,
|
||||
"1Pi": byteformats.PiByte,
|
||||
"1Ei": byteformats.EiByte,
|
||||
"1KiB": byteformats.KiByte,
|
||||
"1MiB": byteformats.MiByte,
|
||||
"1GiB": byteformats.GiByte,
|
||||
"1TiB": byteformats.TiByte,
|
||||
"1PiB": byteformats.PiByte,
|
||||
"1EiB": byteformats.EiByte,
|
||||
"1kB": byteformats.KByte,
|
||||
"1mB": byteformats.MByte,
|
||||
"1gB": byteformats.GByte,
|
||||
"1tB": byteformats.TByte,
|
||||
"1pB": byteformats.PByte,
|
||||
"1eB": byteformats.EByte,
|
||||
}
|
||||
for k, v := range testMap {
|
||||
var mb byteformats.Bytes
|
||||
require.NoError(t, json.Unmarshal([]byte("\""+k+"\""), &mb))
|
||||
require.Equal(t, v, mb.Value())
|
||||
b, err := json.Marshal(mb)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "\""+k+"\"", string(b))
|
||||
}
|
||||
}
|
||||
38
common/onclose/conn.go
Normal file
38
common/onclose/conn.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package onclose
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type CloseHandlerFunc = func()
|
||||
|
||||
type Conn struct {
|
||||
net.Conn
|
||||
onClose func()
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func NewConn(conn net.Conn, onClose func()) *Conn {
|
||||
return &Conn{Conn: conn, onClose: onClose}
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
c.once.Do(c.onClose)
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
type PacketConn struct {
|
||||
net.PacketConn
|
||||
onClose func()
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func NewPacketConn(conn net.PacketConn, onClose func()) *PacketConn {
|
||||
return &PacketConn{PacketConn: conn, onClose: onClose}
|
||||
}
|
||||
|
||||
func (c *PacketConn) Close() error {
|
||||
c.once.Do(c.onClose)
|
||||
return c.PacketConn.Close()
|
||||
}
|
||||
135
common/tls/openvpn_client.go
Normal file
135
common/tls/openvpn_client.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
func NewOpenVPNClient(ctx context.Context, logger logger.ContextLogger, options option.OpenVPNTLSOptions) (Config, error) {
|
||||
ca := options.CA
|
||||
if ca == "" && options.CAPath != "" {
|
||||
data, err := os.ReadFile(options.CAPath)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read ca_path")
|
||||
}
|
||||
ca = string(data)
|
||||
}
|
||||
certificate := options.Certificate
|
||||
if certificate == "" && options.CertificatePath != "" {
|
||||
data, err := os.ReadFile(options.CertificatePath)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read certificate_path")
|
||||
}
|
||||
certificate = string(data)
|
||||
}
|
||||
key := options.Key
|
||||
if key == "" && options.KeyPath != "" {
|
||||
data, err := os.ReadFile(options.KeyPath)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read key_path")
|
||||
}
|
||||
key = string(data)
|
||||
}
|
||||
if strings.TrimSpace(ca) == "" {
|
||||
return nil, E.New("openvpn: missing ca certificate")
|
||||
}
|
||||
if block, _ := pem.Decode([]byte(ca)); block == nil {
|
||||
return nil, E.New("openvpn: ca is not valid PEM")
|
||||
}
|
||||
hasCert := strings.TrimSpace(certificate) != "" || strings.TrimSpace(key) != ""
|
||||
if hasCert {
|
||||
if strings.TrimSpace(certificate) == "" || strings.TrimSpace(key) == "" {
|
||||
return nil, E.New("openvpn: certificate and key must both be set")
|
||||
}
|
||||
if block, _ := pem.Decode([]byte(certificate)); block == nil {
|
||||
return nil, E.New("openvpn: certificate is not valid PEM")
|
||||
}
|
||||
if block, _ := pem.Decode([]byte(key)); block == nil {
|
||||
return nil, E.New("openvpn: key is not valid PEM")
|
||||
}
|
||||
}
|
||||
roots := x509.NewCertPool()
|
||||
if !roots.AppendCertsFromPEM([]byte(ca)) {
|
||||
return nil, E.New("openvpn: failed to parse ca certificate")
|
||||
}
|
||||
var tlsConfig tls.Config
|
||||
tlsConfig.RootCAs = roots
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
if options.CipherSuites != nil {
|
||||
find:
|
||||
for _, cipherSuite := range options.CipherSuites {
|
||||
for _, tlsCipherSuite := range tls.CipherSuites() {
|
||||
if cipherSuite == tlsCipherSuite.Name {
|
||||
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID)
|
||||
continue find
|
||||
}
|
||||
}
|
||||
return nil, E.New("unknown cipher_suite: ", cipherSuite)
|
||||
}
|
||||
}
|
||||
tlsConfig.VerifyConnection = func(cs tls.ConnectionState) error {
|
||||
if len(cs.PeerCertificates) == 0 {
|
||||
return E.New("openvpn: server did not provide certificate")
|
||||
}
|
||||
cert := cs.PeerCertificates[0]
|
||||
intermediates := x509.NewCertPool()
|
||||
for _, intermediate := range cs.PeerCertificates[1:] {
|
||||
intermediates.AddCert(intermediate)
|
||||
}
|
||||
_, err := cert.Verify(x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
Intermediates: intermediates,
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if options.VerifyX509Name != "" {
|
||||
cn := cert.Subject.CommonName
|
||||
switch options.VerifyX509NameMode {
|
||||
case "name-prefix":
|
||||
if !strings.HasPrefix(cn, options.VerifyX509Name) {
|
||||
return E.New("openvpn: server CN ", cn, " does not match prefix ", options.VerifyX509Name)
|
||||
}
|
||||
case "name-suffix":
|
||||
if !strings.HasSuffix(cn, options.VerifyX509Name) {
|
||||
return E.New("openvpn: server CN ", cn, " does not match suffix ", options.VerifyX509Name)
|
||||
}
|
||||
default:
|
||||
if cn != options.VerifyX509Name {
|
||||
return E.New("openvpn: server CN ", cn, " does not match ", options.VerifyX509Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if hasCert {
|
||||
cert, err := tls.X509KeyPair([]byte(certificate), []byte(key))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "openvpn: parse client certificate/key")
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
var config Config = &STDClientConfig{ctx, &tlsConfig, false, 0, false}
|
||||
if options.KernelRx || options.KernelTx {
|
||||
if !C.IsLinux {
|
||||
return nil, E.New("kTLS is only supported on Linux")
|
||||
}
|
||||
config = &KTLSClientConfig{
|
||||
Config: config,
|
||||
logger: logger,
|
||||
kernelTx: options.KernelTx,
|
||||
kernelRx: options.KernelRx,
|
||||
}
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
@@ -13,10 +13,12 @@ const (
|
||||
TypeShadowsocks = "shadowsocks"
|
||||
TypeVMess = "vmess"
|
||||
TypeTrojan = "trojan"
|
||||
TypeTrustTunnel = "trusttunnel"
|
||||
TypeNaive = "naive"
|
||||
TypeWireGuard = "wireguard"
|
||||
TypeWARP = "warp"
|
||||
TypeMASQUE = "masque"
|
||||
TypeOpenVPN = "openvpn"
|
||||
TypeMTProxy = "mtproxy"
|
||||
TypeParser = "parser"
|
||||
TypeHysteria = "hysteria"
|
||||
@@ -25,6 +27,7 @@ const (
|
||||
TypeShadowTLS = "shadowtls"
|
||||
TypeMieru = "mieru"
|
||||
TypeAnyTLS = "anytls"
|
||||
TypeSudoku = "sudoku"
|
||||
TypeShadowsocksR = "shadowsocksr"
|
||||
TypeVLESS = "vless"
|
||||
TypeTUIC = "tuic"
|
||||
@@ -84,6 +87,8 @@ func ProxyDisplayName(proxyType string) string {
|
||||
return "VMess"
|
||||
case TypeTrojan:
|
||||
return "Trojan"
|
||||
case TypeTrustTunnel:
|
||||
return "TrustTunnel"
|
||||
case TypeNaive:
|
||||
return "Naive"
|
||||
case TypeWireGuard:
|
||||
@@ -92,6 +97,8 @@ func ProxyDisplayName(proxyType string) string {
|
||||
return "WARP"
|
||||
case TypeMASQUE:
|
||||
return "MASQUE"
|
||||
case TypeOpenVPN:
|
||||
return "OpenVPN"
|
||||
case TypeMTProxy:
|
||||
return "MTProxy"
|
||||
case TypeParser:
|
||||
@@ -120,6 +127,8 @@ func ProxyDisplayName(proxyType string) string {
|
||||
return "Mieru"
|
||||
case TypeAnyTLS:
|
||||
return "AnyTLS"
|
||||
case TypeSudoku:
|
||||
return "Sudoku"
|
||||
case TypeFallback:
|
||||
return "Fallback"
|
||||
case TypeTailscale:
|
||||
|
||||
@@ -31,10 +31,11 @@
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
|
||||
| `vless` | [VLESS](./vless/) | TCP |
|
||||
| `anytls` | [AnyTLS](./anytls/) | TCP |
|
||||
| `mieru` | [Mieru](./mieru/) | :material-close: |
|
||||
| `tun` | [Tun](./tun/) | :material-close: |
|
||||
| `redirect` | [Redirect](./redirect/) | :material-close: |
|
||||
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
|
||||
|
||||
#### tag
|
||||
|
||||
The tag of the inbound.
|
||||
The tag of the inbound.
|
||||
|
||||
@@ -31,10 +31,11 @@
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
|
||||
| `vless` | [VLESS](./vless/) | TCP |
|
||||
| `anytls` | [AnyTLS](./anytls/) | TCP |
|
||||
| `mieru` | [Mieru](./mieru/) | :material-close: |
|
||||
| `tun` | [Tun](./tun/) | :material-close: |
|
||||
| `redirect` | [Redirect](./redirect/) | :material-close: |
|
||||
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
|
||||
|
||||
#### tag
|
||||
|
||||
入站的标签。
|
||||
入站的标签。
|
||||
|
||||
49
docs/configuration/inbound/mieru.md
Normal file
49
docs/configuration/inbound/mieru.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "mieru",
|
||||
"tag": "mieru-in",
|
||||
|
||||
... // Listen Fields
|
||||
|
||||
"transport": "TCP",
|
||||
"users": [
|
||||
{
|
||||
"name": "asdf",
|
||||
"password": "hjkl"
|
||||
}
|
||||
],
|
||||
"traffic_pattern": "GgQIARAK",
|
||||
}
|
||||
```
|
||||
|
||||
### Listen Fields
|
||||
|
||||
See [Listen Fields](/configuration/shared/listen/) for details.
|
||||
|
||||
### Fields
|
||||
|
||||
#### transport
|
||||
|
||||
==Required==
|
||||
|
||||
Transmission protocol. Allowed values are `TCP` and `UDP`.
|
||||
|
||||
#### users
|
||||
|
||||
==Required==
|
||||
|
||||
A list of mieru user name and password.
|
||||
|
||||
#### traffic_pattern
|
||||
|
||||
A base64 string to fine tune network behavior.
|
||||
|
||||
#### user_hint_is_mandatory
|
||||
|
||||
If proxy client doesn't sent user hint, proxy server will refuse the connection.
|
||||
49
docs/configuration/inbound/mieru.zh.md
Normal file
49
docs/configuration/inbound/mieru.zh.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "mieru",
|
||||
"tag": "mieru-in",
|
||||
|
||||
... // 监听字段
|
||||
|
||||
"transport": "TCP",
|
||||
"users": [
|
||||
{
|
||||
"name": "asdf",
|
||||
"password": "hjkl"
|
||||
}
|
||||
],
|
||||
"traffic_pattern": "GgQIARAK",
|
||||
}
|
||||
```
|
||||
|
||||
### 监听字段
|
||||
|
||||
参阅 [监听字段](/zh/configuration/shared/listen/)。
|
||||
|
||||
### 字段
|
||||
|
||||
#### transport
|
||||
|
||||
==必填==
|
||||
|
||||
通信协议。可设为 `TCP` 或 `UDP`。
|
||||
|
||||
#### users
|
||||
|
||||
==必填==
|
||||
|
||||
一组 mieru 用户名和密码。
|
||||
|
||||
#### traffic_pattern
|
||||
|
||||
一个 base64 字符串用于微调网络行为。
|
||||
|
||||
#### user_hint_is_mandatory
|
||||
|
||||
客户端若不发送用户提示,代理服务器将拒绝连接。
|
||||
@@ -32,6 +32,7 @@
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
||||
| `mieru` | [Mieru](./mieru/) |
|
||||
| `anytls` | [AnyTLS](./anytls/) |
|
||||
| `mieru` | [Mieru](./mieru/) |
|
||||
| `tor` | [Tor](./tor/) |
|
||||
| `ssh` | [SSH](./ssh/) |
|
||||
| `dns` | [DNS](./dns/) |
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
||||
| `mieru` | [Mieru](./mieru/) |
|
||||
| `anytls` | [AnyTLS](./anytls/) |
|
||||
| `mieru` | [Mieru](./mieru/) |
|
||||
| `tor` | [Tor](./tor/) |
|
||||
| `ssh` | [SSH](./ssh/) |
|
||||
| `dns` | [DNS](./dns/) |
|
||||
|
||||
@@ -19,6 +19,7 @@ icon: material/new-box
|
||||
"username": "asdf",
|
||||
"password": "hjkl",
|
||||
"multiplexing": "MULTIPLEXING_LOW",
|
||||
"traffic_pattern": "GgQIARAK",
|
||||
|
||||
... // Dial Fields
|
||||
}
|
||||
@@ -48,7 +49,7 @@ Must set at least one field between `server_port` and `server_ports`.
|
||||
|
||||
==Required==
|
||||
|
||||
Transmission protocol. The only allowed value is `TCP`.
|
||||
Transmission protocol. Allowed values are `TCP` and `UDP`.
|
||||
|
||||
#### username
|
||||
|
||||
@@ -66,6 +67,10 @@ mieru password.
|
||||
|
||||
Multiplexing level. Supported values are `MULTIPLEXING_OFF`, `MULTIPLEXING_LOW`, `MULTIPLEXING_MIDDLE`, `MULTIPLEXING_HIGH`. `MULTIPLEXING_OFF` disables multiplexing.
|
||||
|
||||
#### traffic_pattern
|
||||
|
||||
A base64 string to fine tune network behavior.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
|
||||
@@ -19,6 +19,7 @@ icon: material/new-box
|
||||
"username": "asdf",
|
||||
"password": "hjkl",
|
||||
"multiplexing": "MULTIPLEXING_LOW",
|
||||
"traffic_pattern": "GgQIARAK",
|
||||
|
||||
... // 拨号字段
|
||||
}
|
||||
@@ -48,7 +49,7 @@ icon: material/new-box
|
||||
|
||||
==必填==
|
||||
|
||||
通信协议。仅可设为 `TCP`。
|
||||
通信协议。可设为 `TCP` 或 `UDP`。
|
||||
|
||||
#### username
|
||||
|
||||
@@ -66,6 +67,10 @@ mieru 密码。
|
||||
|
||||
多路复用设置。可以设为 `MULTIPLEXING_OFF`,`MULTIPLEXING_LOW`,`MULTIPLEXING_MIDDLE`,`MULTIPLEXING_HIGH`。其中 `MULTIPLEXING_OFF` 会关闭多路复用功能。
|
||||
|
||||
#### traffic_pattern
|
||||
|
||||
一个 base64 字符串用于微调网络行为。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
||||
|
||||
@@ -64,6 +64,8 @@
|
||||
"listen_port": 7000,
|
||||
"manager": "my-manager",
|
||||
"api_key": "change-me-secret",
|
||||
"keep_alive": "10s",
|
||||
"keep_alive_timeout": "5s",
|
||||
// Enable TLS for production deployments (the node connects via gRPC over h2):
|
||||
// "tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#inbound
|
||||
// "enabled": true,
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
"address": "example.com",
|
||||
"port": 10001,
|
||||
"public_key": "3nk7jdnkcL95Fc/z+GCiH7jOovEKhFkLIGPT+U/uLEQ=",
|
||||
"allowed_ips": ["0.0.0.0/0"],
|
||||
"reserved": "AAAA"
|
||||
"allowed_ips": ["0.0.0.0/0"]
|
||||
}
|
||||
],
|
||||
"udp_timeout": "5m0s",
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
"use_ipv6": false,
|
||||
"profile": {
|
||||
"detour": "direct",
|
||||
// For getting existing MASQUE device profile, else sing-box will create new profile
|
||||
"id": "",
|
||||
"auth_token": ""
|
||||
},
|
||||
@@ -37,14 +36,15 @@
|
||||
"udp_keepalive_period": "30s",
|
||||
"udp_initial_packet_size": 0,
|
||||
"reconnect_delay": "5s",
|
||||
// TLS fields for HTTP2
|
||||
"insecure": false,
|
||||
"cipher_suites": [],
|
||||
"curve_preferences": [],
|
||||
"fragment": false,
|
||||
"record_fragment": false,
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false
|
||||
"tls": {
|
||||
"insecure": false,
|
||||
"cipher_suites": [],
|
||||
"curve_preferences": [],
|
||||
"fragment": false,
|
||||
"record_fragment": false,
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false
|
||||
}
|
||||
// Dial Fields
|
||||
}
|
||||
],
|
||||
@@ -53,4 +53,4 @@
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,14 +27,16 @@
|
||||
"tag": "mieru-out",
|
||||
"server": "example.com",
|
||||
"server_port": 27017,
|
||||
"server_ports": "27017-27019",
|
||||
"server_ports": [
|
||||
"27017-27019"
|
||||
],
|
||||
"transport": "TCP",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
// valid: MULTIPLEXING_DEFAULT / MULTIPLEXING_OFF / MULTIPLEXING_LOW
|
||||
// MULTIPLEXING_MIDDLE / MULTIPLEXING_HIGH
|
||||
"multiplexing": "MULTIPLEXING_LOW"
|
||||
// Dial Fields
|
||||
"multiplexing": "MULTIPLEXING_LOW",
|
||||
"traffic_pattern": "GgQIARAK"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
|
||||
29
examples/mieru/server.json
Normal file
29
examples/mieru/server.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mieru",
|
||||
"tag": "mieru-in",
|
||||
"listen_port": 27017,
|
||||
"transport": "TCP",
|
||||
"users": [
|
||||
{
|
||||
"name": "username",
|
||||
"password": "password"
|
||||
}
|
||||
],
|
||||
"traffic_pattern": "GgQIARAK"
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "direct"
|
||||
}
|
||||
}
|
||||
46
examples/openvpn/auth-user-pass.json
Normal file
46
examples/openvpn/auth-user-pass.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "info"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "openvpn",
|
||||
"tag": "openvpn-out",
|
||||
"servers": [
|
||||
{
|
||||
"server": "vpn.example.com",
|
||||
"server_port": 1194
|
||||
}
|
||||
],
|
||||
"proto": "udp", // udp, tcp
|
||||
"username": "myuser",
|
||||
"password": "mypassword",
|
||||
"tls_crypt": "-----BEGIN OpenVPN Static key V1-----\n...\n-----END OpenVPN Static key V1-----",
|
||||
// or: "tls_crypt_path": "/path/to/ta.key",
|
||||
"tls": {
|
||||
"ca": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
||||
// or: "ca_path": "/path/to/ca.crt",
|
||||
"cipher_suites": [],
|
||||
"verify_x509_name": "",
|
||||
"verify_x509_name_mode": "", // name-prefix, name-suffix, exact (default)
|
||||
"fragment": false,
|
||||
"fragment_fallback_delay": "300ms",
|
||||
"record_fragment": false,
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false
|
||||
}
|
||||
// Dial Fields
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "openvpn-out",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
51
examples/openvpn/tls-auth.json
Normal file
51
examples/openvpn/tls-auth.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "info"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "openvpn",
|
||||
"tag": "openvpn-out",
|
||||
"servers": [
|
||||
{
|
||||
"server": "vpn.example.com",
|
||||
"server_port": 1194
|
||||
}
|
||||
],
|
||||
"proto": "udp", // udp, tcp
|
||||
"cipher": "AES-256-CBC",
|
||||
"auth": "SHA1",
|
||||
"tls_auth": "-----BEGIN OpenVPN Static key V1-----\n...\n-----END OpenVPN Static key V1-----",
|
||||
// or: "tls_auth_path": "/path/to/ta.key",
|
||||
"key_direction": 1,
|
||||
"tls": {
|
||||
"certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
||||
// or: "certificate_path": "/path/to/client.crt",
|
||||
"key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----",
|
||||
// or: "key_path": "/path/to/client.key",
|
||||
"ca": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
||||
// or: "ca_path": "/path/to/ca.crt",
|
||||
"cipher_suites": [],
|
||||
"verify_x509_name": "",
|
||||
"verify_x509_name_mode": "", // name-prefix, name-suffix, exact (default)
|
||||
"fragment": false,
|
||||
"fragment_fallback_delay": "300ms",
|
||||
"record_fragment": false,
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false
|
||||
}
|
||||
// Dial Fields
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "openvpn-out",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
49
examples/openvpn/tls-crypt-v2.json
Normal file
49
examples/openvpn/tls-crypt-v2.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "info"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "openvpn",
|
||||
"tag": "openvpn-out",
|
||||
"servers": [
|
||||
{
|
||||
"server": "vpn.example.com",
|
||||
"server_port": 1194
|
||||
}
|
||||
],
|
||||
"proto": "udp", // udp, tcp
|
||||
"tls_crypt": "-----BEGIN OpenVPN tls-crypt-v2 client key-----\n...\n-----END OpenVPN tls-crypt-v2 client key-----",
|
||||
// or: "tls_crypt_path": "/path/to/tls-crypt-v2.key",
|
||||
"tls_crypt_v2": true,
|
||||
"tls": {
|
||||
"certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
||||
// or: "certificate_path": "/path/to/client.crt",
|
||||
"key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----",
|
||||
// or: "key_path": "/path/to/client.key",
|
||||
"ca": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
||||
// or: "ca_path": "/path/to/ca.crt",
|
||||
"cipher_suites": [],
|
||||
"verify_x509_name": "",
|
||||
"verify_x509_name_mode": "", // name-prefix, name-suffix, exact (default)
|
||||
"fragment": false,
|
||||
"fragment_fallback_delay": "300ms",
|
||||
"record_fragment": false,
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false
|
||||
}
|
||||
// Dial Fields
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "openvpn-out",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
52
examples/openvpn/tls-crypt.json
Normal file
52
examples/openvpn/tls-crypt.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "info"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "openvpn",
|
||||
"tag": "openvpn-out",
|
||||
"servers": [
|
||||
{
|
||||
"server": "vpn.example.com",
|
||||
"server_port": 1194
|
||||
}
|
||||
],
|
||||
"proto": "udp", // udp, tcp
|
||||
"cipher": "AES-256-GCM", // AES-128-GCM, AES-192-GCM, AES-256-GCM, AES-128-CBC, AES-192-CBC, AES-256-CBC, CHACHA20-POLY1305
|
||||
"auth": "SHA256", // SHA1, SHA256, SHA384, SHA512 (ignored for AEAD ciphers)
|
||||
"tls_crypt": "-----BEGIN OpenVPN Static key V1-----\n...\n-----END OpenVPN Static key V1-----",
|
||||
// or: "tls_crypt_path": "/path/to/ta.key",
|
||||
"ping_interval": "10s",
|
||||
"reconnect_delay": "30s",
|
||||
"tls": {
|
||||
"certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
||||
// or: "certificate_path": "/path/to/client.crt",
|
||||
"key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----",
|
||||
// or: "key_path": "/path/to/client.key",
|
||||
"ca": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
||||
// or: "ca_path": "/path/to/ca.crt",
|
||||
"cipher_suites": [],
|
||||
"verify_x509_name": "",
|
||||
"verify_x509_name_mode": "", // name-prefix, name-suffix, exact (default)
|
||||
"fragment": false,
|
||||
"fragment_fallback_delay": "300ms",
|
||||
"record_fragment": false,
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false
|
||||
}
|
||||
// Dial Fields
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "openvpn-out",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,8 @@
|
||||
// - SIP008 (shadowsocks)
|
||||
// - Raw shareable links (vless://, vmess://, ss://, trojan://, ...)
|
||||
"path": "subscriptions/my-sub.txt",
|
||||
// Remove emoji flags from proxy names.
|
||||
"remove_emojis": true,
|
||||
"health_check": {
|
||||
"enabled": true,
|
||||
"url": "https://www.gstatic.com/generate_204",
|
||||
|
||||
@@ -56,6 +56,8 @@
|
||||
// "exclude" wins over "include" when both match.
|
||||
"exclude": "(?i)expire|流量|官网",
|
||||
"include": "(?i)hk|jp|sg|us",
|
||||
// Remove emoji flags from proxy names.
|
||||
"remove_emojis": true,
|
||||
"health_check": {
|
||||
"enabled": true,
|
||||
"url": "https://www.gstatic.com/generate_204",
|
||||
|
||||
31
examples/sudoku/client.json
Normal file
31
examples/sudoku/client.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"listen": "127.0.0.1",
|
||||
"listen_port": 1080
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "sudoku",
|
||||
"server": "your-server.com",
|
||||
"server_port": 443,
|
||||
"key": "your-secret-key"
|
||||
// "aead_method": "chacha20-poly1305", // chacha20-poly1305 | aes-128-gcm | none
|
||||
// "table_type": "prefer_ascii", // prefer_ascii | prefer_entropy | up_ascii_down_entropy | up_entropy_down_ascii
|
||||
// "padding_min": 10, // 0-100
|
||||
// "padding_max": 30, // 0-100, >= padding_min
|
||||
// "enable_pure_downlink": true, // true | false
|
||||
// "custom_table": "xpxvvpvv", // 8 chars: 2x, 2p, 4v
|
||||
// "custom_tables": ["xpxvvpvv", "vxpvxvvp"],
|
||||
// "http_mask": {
|
||||
// "enabled": true, // true | false
|
||||
// "mode": "stream", // legacy | stream | poll | auto | ws
|
||||
// "host": "cdn.example.com", // optional, Host header / SNI override
|
||||
// "path_root": "secret", // optional, URL path prefix (single segment)
|
||||
// "multiplex": "auto" // off | auto | on
|
||||
// }
|
||||
}
|
||||
]
|
||||
}
|
||||
35
examples/sudoku/client_tls.json
Normal file
35
examples/sudoku/client_tls.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"listen": "127.0.0.1",
|
||||
"listen_port": 1080
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "sudoku",
|
||||
"server": "your-server.com",
|
||||
"server_port": 443,
|
||||
"key": "your-secret-key",
|
||||
// "aead_method": "chacha20-poly1305", // chacha20-poly1305 | aes-128-gcm | none
|
||||
// "table_type": "prefer_ascii", // prefer_ascii | prefer_entropy | up_ascii_down_entropy | up_entropy_down_ascii
|
||||
// "padding_min": 10, // 0-100
|
||||
// "padding_max": 30, // 0-100, >= padding_min
|
||||
// "enable_pure_downlink": true, // true | false
|
||||
// "custom_table": "xpxvvpvv", // 8 chars: 2x, 2p, 4v
|
||||
// "custom_tables": ["xpxvvpvv", "vxpvxvvp"],
|
||||
"http_mask": {
|
||||
"enabled": true, // true | false
|
||||
"mode": "stream", // legacy | stream | poll | auto | ws
|
||||
"host": "cdn.example.com", // optional, Host header / SNI override
|
||||
"path_root": "secret", // optional, URL path prefix (single segment)
|
||||
"multiplex": "auto", // off | auto | on
|
||||
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#outbound
|
||||
"enabled": true,
|
||||
"server_name": "cdn.example.com",
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
27
examples/sudoku/server.json
Normal file
27
examples/sudoku/server.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "sudoku",
|
||||
"listen": "::",
|
||||
"listen_port": 443,
|
||||
"key": "your-secret-key"
|
||||
// "aead_method": "chacha20-poly1305", // chacha20-poly1305 | aes-128-gcm | none
|
||||
// "table_type": "prefer_ascii", // prefer_ascii | prefer_entropy | up_ascii_down_entropy | up_entropy_down_ascii
|
||||
// "padding_min": 10, // 0-100
|
||||
// "padding_max": 30, // 0-100, >= padding_min
|
||||
// "enable_pure_downlink": true, // true | false
|
||||
// "handshake_timeout": 5, // seconds
|
||||
// "custom_table": "xpxvvpvv", // 8 chars: 2x, 2p, 4v
|
||||
// "custom_tables": ["xpxvvpvv", "vxpvxvvp"],
|
||||
// "disable_http_mask": false, // true | false
|
||||
// "http_mask_mode": "legacy", // legacy | stream | poll | auto | ws
|
||||
// "path_root": "secret", // optional, URL path prefix (single segment)
|
||||
// "fallback": "127.0.0.1:8080" // optional, fallback address for rejected connections
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct"
|
||||
}
|
||||
]
|
||||
}
|
||||
22
examples/sudoku/server_advanced.json
Normal file
22
examples/sudoku/server_advanced.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "sudoku",
|
||||
"listen": "::",
|
||||
"listen_port": 443,
|
||||
"key": "your-secret-key",
|
||||
"aead_method": "aes-128-gcm",
|
||||
"table_type": "prefer_entropy",
|
||||
"padding_min": 10,
|
||||
"padding_max": 50,
|
||||
"http_mask_mode": "stream",
|
||||
"path_root": "secret",
|
||||
"fallback": "127.0.0.1:8080"
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct"
|
||||
}
|
||||
]
|
||||
}
|
||||
78
examples/trusttunnel/client.json
Normal file
78
examples/trusttunnel/client.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "trusttunnel",
|
||||
"tag": "trusttunnel-h2",
|
||||
"server": "example.com",
|
||||
"server_port": 443,
|
||||
"username": "user1",
|
||||
"password": "password1",
|
||||
"network": ["tcp", "udp"],
|
||||
"health_check": true,
|
||||
"multiplex": {
|
||||
"enabled": true,
|
||||
"max_connections": 8,
|
||||
"min_streams": 5
|
||||
},
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": "example.com"
|
||||
}
|
||||
// Dial Fields
|
||||
},
|
||||
{
|
||||
"type": "trusttunnel",
|
||||
"tag": "trusttunnel-quic",
|
||||
"server": "example.com",
|
||||
"server_port": 443,
|
||||
"username": "user1",
|
||||
"password": "password1",
|
||||
"network": ["tcp", "udp"],
|
||||
"health_check": true,
|
||||
"quic": true,
|
||||
"congestion_controller": "bbr", // bbr, bbr_standard, bbr2, bbr2_variant, cubic, reno
|
||||
"bbr_profile": "standard", // standard, conservative, aggressive
|
||||
"cwnd": 32,
|
||||
"multiplex": {
|
||||
"enabled": true,
|
||||
"max_connections": 8,
|
||||
"min_streams": 5
|
||||
},
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": "example.com"
|
||||
}
|
||||
// Dial Fields
|
||||
},
|
||||
{
|
||||
"type": "selector",
|
||||
"tag": "trusttunnel-selector",
|
||||
"outbounds": ["trusttunnel-h2", "trusttunnel-quic"],
|
||||
"default": "trusttunnel-h2"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "trusttunnel-selector",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
32
examples/trusttunnel/server.json
Normal file
32
examples/trusttunnel/server.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "trusttunnel",
|
||||
"tag": "trusttunnel-in",
|
||||
"listen": "::",
|
||||
"listen_port": 443,
|
||||
"network": ["tcp", "udp"],
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"password": "password1"
|
||||
}
|
||||
],
|
||||
"congestion_controller": "bbr", // bbr, bbr_standard, bbr2, bbr2_variant, cubic, reno
|
||||
"bbr_profile": "standard", // standard, conservative, aggressive
|
||||
"cwnd": 32,
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"alpn": ["h2", "h3"],
|
||||
"certificate_path": "/path/to/cert.pem",
|
||||
"key_path": "/path/to/key.pem"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -23,8 +23,7 @@
|
||||
"address": "example.com",
|
||||
"port": 10001,
|
||||
"public_key": "3nk7jdnkcL95Fc/z+GCiH7jOovEKhFkLIGPT+U/uLEQ=",
|
||||
"allowed_ips": ["0.0.0.0/0"],
|
||||
"reserved": "AAAA"
|
||||
"allowed_ips": ["0.0.0.0/0"]
|
||||
}
|
||||
],
|
||||
"udp_timeout": "5m0s",
|
||||
|
||||
24
go.mod
24
go.mod
@@ -11,10 +11,11 @@ require (
|
||||
github.com/coder/websocket v1.8.14
|
||||
github.com/cretz/bine v0.2.0
|
||||
github.com/database64128/tfo-go/v2 v2.3.2
|
||||
github.com/enfein/mieru/v3 v3.17.1
|
||||
github.com/enfein/mieru/v3 v3.33.0
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/go-playground/validator/v10 v10.30.1
|
||||
github.com/gobwas/ws v1.4.0
|
||||
github.com/godbus/dbus/v5 v5.2.2
|
||||
github.com/gofrs/uuid/v5 v5.4.0
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||
@@ -53,7 +54,7 @@ require (
|
||||
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c
|
||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
||||
github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.0.2
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
|
||||
github.com/shtorm-7/workerpool v0.5.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/vishvananda/netns v0.0.5
|
||||
@@ -75,17 +76,24 @@ require (
|
||||
|
||||
require (
|
||||
github.com/OneOfOne/xxhash v1.2.8 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/docker/docker v28.5.2+incompatible // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/dunglas/httpsfv v1.1.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/panjf2000/ants/v2 v2.12.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/redis/go-redis/v9 v9.8.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b // indirect
|
||||
github.com/zeebo/assert v1.3.0 // indirect
|
||||
go.opentelemetry.io/otel v1.41.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20260408064518-65a410b0d584 // indirect
|
||||
modernc.org/libc v1.72.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
@@ -93,14 +101,14 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
filippo.io/edwards25519 v1.1.0
|
||||
github.com/AdguardTeam/golibs v0.32.7 // 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/ameshkov/dnscrypt/v2 v2.4.0
|
||||
github.com/ameshkov/dnsstamps v1.0.3 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/caddyserver/zerossl v0.1.5 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
|
||||
@@ -144,7 +152,7 @@ require (
|
||||
github.com/mdlayher/netlink v1.9.0 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.25 // indirect
|
||||
github.com/pires/go-proxyproto v0.11.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus-community/pro-bing v0.4.0 // indirect
|
||||
@@ -211,7 +219,7 @@ require (
|
||||
lukechampine.com/blake3 v1.4.1
|
||||
)
|
||||
|
||||
replace github.com/sagernet/wireguard-go => github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.4.2
|
||||
replace github.com/sagernet/wireguard-go => github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.4.3
|
||||
|
||||
replace github.com/sagernet/tailscale => github.com/shtorm-7/tailscale v1.92.4-sing-box-1.13-mod.7-extended-1.0.2
|
||||
|
||||
@@ -219,10 +227,12 @@ replace github.com/sagernet/sing-mux => github.com/shtorm-7/sing-mux v0.3.4-exte
|
||||
|
||||
replace github.com/ameshkov/dnscrypt/v2 => github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0
|
||||
|
||||
replace github.com/sagernet/sing-vmess => github.com/starifly/sing-vmess v0.2.7-mod.9
|
||||
replace github.com/sagernet/sing-vmess => github.com/shtorm-7/sing-vmess v0.2.7-extended-1.0.0
|
||||
|
||||
replace github.com/dolonet/mtg-multi => github.com/shtorm-7/mtg-multi v1.8.0-extended-1.0.1
|
||||
|
||||
replace github.com/Diniboy1123/connect-ip-go => github.com/shtorm-7/connect-ip-go v1.0.0-extended-1.0.0
|
||||
|
||||
replace github.com/shtorm-7/go-cache/v2 => github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.1.0
|
||||
|
||||
replace github.com/sagernet/sing => github.com/shtorm-7/sing v0.8.10-extended-1.0.0
|
||||
|
||||
68
go.sum
68
go.sum
@@ -20,14 +20,16 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
|
||||
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/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY=
|
||||
github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q=
|
||||
github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc=
|
||||
github.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0=
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
@@ -75,10 +77,10 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
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/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54=
|
||||
@@ -87,8 +89,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/enfein/mieru/v3 v3.17.1 h1:pIKbspsKRYNyUrORVI33t1/yz2syaaUkIanskAbGBHY=
|
||||
github.com/enfein/mieru/v3 v3.17.1/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/enfein/mieru/v3 v3.33.0 h1:hv2jK8nqYHwpSG86U2rpZR2I8Aff1/J3ifRmd9NBbFc=
|
||||
github.com/enfein/mieru/v3 v3.33.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE=
|
||||
@@ -129,12 +131,12 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
|
||||
github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/gogo/protobuf v1.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/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
@@ -243,16 +245,16 @@ github.com/openai/openai-go/v3 v3.26.0 h1:bRt6H/ozMNt/dDkN4gobnLqaEGrRGBzmbVs0xx
|
||||
github.com/openai/openai-go/v3 v3.26.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
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/panjf2000/ants/v2 v2.12.0 h1:u9JhESo83i/GkZnhfTNuFMMWcNt7mnV1bGJ6FT4wXH8=
|
||||
github.com/panjf2000/ants/v2 v2.12.0/go.mod h1:tSQuaNQ6r6NRhPt+IZVUevvDyFMTs+eS4ztZc52uJTY=
|
||||
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/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
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/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
|
||||
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -357,8 +359,6 @@ github.com/sagernet/nftables v0.3.0-mod.2 h1:ck2KMU02OxL1eDFgGaWYglMDpoOZ7OHzxje
|
||||
github.com/sagernet/nftables v0.3.0-mod.2/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ=
|
||||
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
|
||||
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
|
||||
github.com/sagernet/sing v0.8.10 h1:V5VZffy8rm4dtBVKIpKa8vibRR2SiJprtu/10DFUalU=
|
||||
github.com/sagernet/sing v0.8.10/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
|
||||
github.com/sagernet/sing-quic v0.6.1 h1:lx0tcm99wIA1RkyvILNzRSsMy1k7TTQYIhx71E/WBlw=
|
||||
github.com/sagernet/sing-quic v0.6.1/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
|
||||
@@ -381,21 +381,23 @@ github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.1.0 h1:PLZ/YHqnApPx13wt6MX3Itq
|
||||
github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.1.0/go.mod h1:Ek4yz5OK6stwhLKgLsRRYDI+FA+ZWvRJiWLjsi/vMM4=
|
||||
github.com/shtorm-7/mtg-multi v1.8.0-extended-1.0.1 h1:UeJkrCJJmIjTBywErVMx7fCSoBf4gh6QgT9bp9o1ajM=
|
||||
github.com/shtorm-7/mtg-multi v1.8.0-extended-1.0.1/go.mod h1:3rvdhwdPABkwKBdvgMt3VwMn9uSq8hpoHRezZ5jRJU0=
|
||||
github.com/shtorm-7/sing v0.8.10-extended-1.0.0 h1:mAkyycCQOzCttPOR5fcHkJaZvXMQXeu3mbEfr8D+7A8=
|
||||
github.com/shtorm-7/sing v0.8.10-extended-1.0.0/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
|
||||
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/sing-vmess v0.2.7-extended-1.0.0 h1:WVheKmQH5hSQbJU1ZTKthKSutkTLWSb2hp4JuQhJBow=
|
||||
github.com/shtorm-7/sing-vmess v0.2.7-extended-1.0.0/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
|
||||
github.com/shtorm-7/tailscale v1.92.4-sing-box-1.13-mod.7-extended-1.0.2 h1:hSMjh97OszszOd8HrzpaYUQH9dWRRBluJCbwQyz8ZOk=
|
||||
github.com/shtorm-7/tailscale v1.92.4-sing-box-1.13-mod.7-extended-1.0.2/go.mod h1:TYIIqO5sZpWq873rLIeO2usszSMUpR3h6WdqVVs65ug=
|
||||
github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.4.2 h1:oEvk13VPsypigqNK/rlcqC63gTj0ANJAnzUwlZkOia4=
|
||||
github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.4.2/go.mod h1:Me2JlCDYHxnd0mnuX7L5LXAeDHCltI7vSKq3eTE6SVE=
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
|
||||
github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.4.3 h1:jtOA73D4F5qRV70//ahOt20KBnWvQimAFjtIiOtt0ps=
|
||||
github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.4.3/go.mod h1:Me2JlCDYHxnd0mnuX7L5LXAeDHCltI7vSKq3eTE6SVE=
|
||||
github.com/shtorm-7/workerpool v0.5.0 h1:NPZuNgyH0EUm4aQsTL09xR1iV+7GCFw6jX9Z4aAVp2s=
|
||||
github.com/shtorm-7/workerpool v0.5.0/go.mod h1:NI0pUZgmGu0BdKO9j3mct1DNZmgXbyTS9foorljdH6E=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/starifly/sing-vmess v0.2.7-mod.9 h1:xobAmejSbBQ0A3f/EtJ9cJd3m6gK7dDPccPdeGz7tXY=
|
||||
github.com/starifly/sing-vmess v0.2.7-mod.9/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
@@ -443,12 +445,14 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
|
||||
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/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
|
||||
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/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=
|
||||
@@ -457,16 +461,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
|
||||
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
|
||||
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
|
||||
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
|
||||
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
@@ -521,8 +525,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
golang.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=
|
||||
|
||||
12
include/openvpn.go
Normal file
12
include/openvpn.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build with_openvpn
|
||||
|
||||
package include
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/protocol/openvpn"
|
||||
)
|
||||
|
||||
func registerOpenVPNOutbound(registry *outbound.Registry) {
|
||||
openvpn.RegisterOutbound(registry)
|
||||
}
|
||||
20
include/openvpn_stub.go
Normal file
20
include/openvpn_stub.go
Normal file
@@ -0,0 +1,20 @@
|
||||
//go:build !with_openvpn
|
||||
|
||||
package include
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func registerOpenVPNOutbound(registry *outbound.Registry) {
|
||||
outbound.Register[option.OpenVPNOutboundOptions](registry, C.TypeOpenVPN, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.OpenVPNOutboundOptions) (adapter.Outbound, error) {
|
||||
return nil, E.New(`OpenVPN outbound is not included in this build, rebuild with -tags with_openvpn`)
|
||||
})
|
||||
}
|
||||
@@ -80,13 +80,16 @@ func InboundRegistry() *inbound.Registry {
|
||||
shadowtls.RegisterInbound(registry)
|
||||
vless.RegisterInbound(registry)
|
||||
anytls.RegisterInbound(registry)
|
||||
mieru.RegisterInbound(registry)
|
||||
|
||||
bond.RegisterInbound(registry)
|
||||
failover.RegisterInbound(registry)
|
||||
registerTrustTunnelInbound(registry)
|
||||
|
||||
registerQUICInbounds(registry)
|
||||
registerStubForRemovedInbounds(registry)
|
||||
registerMTProxyInbound(registry)
|
||||
registerSudokuInbound(registry)
|
||||
|
||||
return registry
|
||||
}
|
||||
@@ -115,9 +118,11 @@ func OutboundRegistry() *outbound.Registry {
|
||||
mieru.RegisterOutbound(registry)
|
||||
anytls.RegisterOutbound(registry)
|
||||
registerMASQUEOutbound(registry)
|
||||
registerOpenVPNOutbound(registry)
|
||||
|
||||
bond.RegisterOutbound(registry)
|
||||
failover.RegisterOutbound(registry)
|
||||
registerTrustTunnelOutbound(registry)
|
||||
|
||||
bandwidth.RegisterOutbound(registry)
|
||||
connection.RegisterOutbound(registry)
|
||||
@@ -128,6 +133,7 @@ func OutboundRegistry() *outbound.Registry {
|
||||
|
||||
registerQUICOutbounds(registry)
|
||||
registerStubForRemovedOutbounds(registry)
|
||||
registerSudokuOutbound(registry)
|
||||
|
||||
return registry
|
||||
}
|
||||
|
||||
17
include/sudoku.go
Normal file
17
include/sudoku.go
Normal file
@@ -0,0 +1,17 @@
|
||||
//go:build with_sudoku
|
||||
|
||||
package include
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/protocol/sudoku"
|
||||
)
|
||||
|
||||
func registerSudokuInbound(registry *inbound.Registry) {
|
||||
sudoku.RegisterInbound(registry)
|
||||
}
|
||||
|
||||
func registerSudokuOutbound(registry *outbound.Registry) {
|
||||
sudoku.RegisterOutbound(registry)
|
||||
}
|
||||
27
include/sudoku_stub.go
Normal file
27
include/sudoku_stub.go
Normal file
@@ -0,0 +1,27 @@
|
||||
//go:build !with_sudoku
|
||||
|
||||
package include
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"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"
|
||||
)
|
||||
|
||||
func registerSudokuInbound(registry *inbound.Registry) {
|
||||
inbound.Register[option.SudokuInboundOptions](registry, C.TypeSudoku, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SudokuInboundOptions) (adapter.Inbound, error) {
|
||||
return nil, E.New(`Sudoku is not included in this build, rebuild with -tags with_sudoku`)
|
||||
})
|
||||
}
|
||||
|
||||
func registerSudokuOutbound(registry *outbound.Registry) {
|
||||
outbound.Register[option.SudokuOutboundOptions](registry, C.TypeSudoku, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SudokuOutboundOptions) (adapter.Outbound, error) {
|
||||
return nil, E.New(`Sudoku is not included in this build, rebuild with -tags with_sudoku`)
|
||||
})
|
||||
}
|
||||
17
include/trusttunnel.go
Normal file
17
include/trusttunnel.go
Normal file
@@ -0,0 +1,17 @@
|
||||
//go:build with_trusttunnel
|
||||
|
||||
package include
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/protocol/trusttunnel"
|
||||
)
|
||||
|
||||
func registerTrustTunnelInbound(registry *inbound.Registry) {
|
||||
trusttunnel.RegisterInbound(registry)
|
||||
}
|
||||
|
||||
func registerTrustTunnelOutbound(registry *outbound.Registry) {
|
||||
trusttunnel.RegisterOutbound(registry)
|
||||
}
|
||||
27
include/trusttunnel_stub.go
Normal file
27
include/trusttunnel_stub.go
Normal file
@@ -0,0 +1,27 @@
|
||||
//go:build !with_trusttunnel
|
||||
|
||||
package include
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"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"
|
||||
)
|
||||
|
||||
func registerTrustTunnelInbound(registry *inbound.Registry) {
|
||||
inbound.Register[option.TrustTunnelInboundOptions](registry, C.TypeTrustTunnel, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrustTunnelInboundOptions) (adapter.Inbound, error) {
|
||||
return nil, E.New(`TrustTunnel is not included in this build, rebuild with -tags with_trusttunnel`)
|
||||
})
|
||||
}
|
||||
|
||||
func registerTrustTunnelOutbound(registry *outbound.Registry) {
|
||||
outbound.Register[option.TrustTunnelOutboundOptions](registry, C.TypeTrustTunnel, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrustTunnelOutboundOptions) (adapter.Outbound, error) {
|
||||
return nil, E.New(`TrustTunnel is not included in this build, rebuild with -tags with_trusttunnel`)
|
||||
})
|
||||
}
|
||||
@@ -6,6 +6,6 @@ type ManagerServiceDatabase struct {
|
||||
}
|
||||
|
||||
type ManagerServiceOptions struct {
|
||||
Inbounds []string `json:"inbounds"`
|
||||
Inbounds []string `json:"inbounds"`
|
||||
Database ManagerServiceDatabase `json:"database"`
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
)
|
||||
|
||||
type MASQUEOutboundOptions struct {
|
||||
DialerOptions
|
||||
UseHTTP2 bool `json:"use_http2,omitempty"`
|
||||
UseIPv6 bool `json:"use_ipv6,omitempty"`
|
||||
Profile CloudflareProfile `json:"profile,omitempty"`
|
||||
@@ -12,8 +13,7 @@ type MASQUEOutboundOptions struct {
|
||||
UDPKeepalivePeriod badoption.Duration `json:"udp_keepalive_period,omitempty"`
|
||||
UDPInitialPacketSize uint16 `json:"udp_initial_packet_size,omitempty"`
|
||||
ReconnectDelay badoption.Duration `json:"reconnect_delay,omitempty"`
|
||||
MASQUEOutboundTLSOptions
|
||||
DialerOptions
|
||||
MASQUEOutboundTLSOptionsContainer
|
||||
}
|
||||
|
||||
type MASQUEOutboundTLSOptions struct {
|
||||
@@ -28,5 +28,5 @@ type MASQUEOutboundTLSOptions struct {
|
||||
}
|
||||
|
||||
type MASQUEOutboundTLSOptionsContainer struct {
|
||||
TLS *OutboundTLSOptions `json:"tls,omitempty"`
|
||||
TLS *MASQUEOutboundTLSOptions `json:"tls,omitempty"`
|
||||
}
|
||||
|
||||
@@ -10,4 +10,18 @@ type MieruOutboundOptions struct {
|
||||
UserName string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Multiplexing string `json:"multiplexing,omitempty"`
|
||||
TrafficPattern string `json:"traffic_pattern,omitempty"`
|
||||
}
|
||||
|
||||
type MieruInboundOptions struct {
|
||||
ListenOptions
|
||||
Users []MieruUser `json:"users,omitempty"`
|
||||
Transport string `json:"transport,omitempty"`
|
||||
TrafficPattern string `json:"traffic_pattern,omitempty"`
|
||||
UserHintIsMandatory bool `json:"user_hint_is_mandatory,omitempty"`
|
||||
}
|
||||
|
||||
type MieruUser struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package option
|
||||
|
||||
type NodeServiceOptions struct {
|
||||
UUID string
|
||||
Inbounds []string `json:"inbounds"`
|
||||
ConnectionLimiters []string `json:"connection_limiters"`
|
||||
BandwidthLimiters []string `json:"bandwidth_limiters"`
|
||||
TrafficLimiters []string `json:"traffic_limiters"`
|
||||
RateLimiters []string `json:"rate_limiters"`
|
||||
Manager string `json:"manager"`
|
||||
UUID string `json:"uuid"`
|
||||
Inbounds []string `json:"inbounds"`
|
||||
ConnectionLimiters []string `json:"connection_limiters"`
|
||||
BandwidthLimiters []string `json:"bandwidth_limiters"`
|
||||
TrafficLimiters []string `json:"traffic_limiters"`
|
||||
RateLimiters []string `json:"rate_limiters"`
|
||||
Manager string `json:"manager"`
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
)
|
||||
|
||||
type _NodeManagerAPIOptions struct {
|
||||
@@ -57,8 +58,10 @@ func (o *NodeManagerAPIOptions) UnmarshalJSON(bytes []byte) error {
|
||||
type NodeManagerAPIServerOptions struct {
|
||||
ListenOptions
|
||||
InboundTLSOptionsContainer
|
||||
Manager string `json:"manager"`
|
||||
APIKey string `json:"api_key"`
|
||||
Manager string `json:"manager"`
|
||||
APIKey string `json:"api_key"`
|
||||
KeepAlive badoption.Duration `json:"keep_alive,omitempty"`
|
||||
KeepAliveTimeout badoption.Duration `json:"keep_alive_timeout,omitempty"`
|
||||
}
|
||||
|
||||
type NodeManagerAPIClientOptions struct {
|
||||
|
||||
40
option/openvpn.go
Normal file
40
option/openvpn.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package option
|
||||
|
||||
import "github.com/sagernet/sing/common/json/badoption"
|
||||
|
||||
type OpenVPNOutboundOptions struct {
|
||||
DialerOptions
|
||||
Servers []ServerOptions `json:"servers"`
|
||||
Proto string `json:"proto,omitempty"`
|
||||
Cipher string `json:"cipher,omitempty"`
|
||||
Auth string `json:"auth,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
TLSCrypt string `json:"tls_crypt,omitempty"`
|
||||
TLSCryptPath string `json:"tls_crypt_path,omitempty"`
|
||||
TLSCryptV2 bool `json:"tls_crypt_v2,omitempty"`
|
||||
TLSAuth string `json:"tls_auth,omitempty"`
|
||||
TLSAuthPath string `json:"tls_auth_path,omitempty"`
|
||||
KeyDirection int `json:"key_direction,omitempty"`
|
||||
ReconnectDelay badoption.Duration `json:"reconnect_delay,omitempty"`
|
||||
PingInterval badoption.Duration `json:"ping_interval,omitempty"`
|
||||
OpenVPNOutboundTLSOptionsContainer
|
||||
}
|
||||
|
||||
type OpenVPNTLSOptions struct {
|
||||
Certificate string `json:"certificate,omitempty"`
|
||||
CertificatePath string `json:"certificate_path,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
KeyPath string `json:"key_path,omitempty"`
|
||||
CA string `json:"ca,omitempty"`
|
||||
CAPath string `json:"ca_path,omitempty"`
|
||||
CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"`
|
||||
VerifyX509Name string `json:"verify_x509_name,omitempty"`
|
||||
VerifyX509NameMode string `json:"verify_x509_name_mode,omitempty"`
|
||||
KernelTx bool `json:"kernel_tx,omitempty"`
|
||||
KernelRx bool `json:"kernel_rx,omitempty"`
|
||||
}
|
||||
|
||||
type OpenVPNOutboundTLSOptionsContainer struct {
|
||||
TLS *OpenVPNTLSOptions `json:"tls,omitempty"`
|
||||
}
|
||||
@@ -47,8 +47,9 @@ func (h *Provider) UnmarshalJSONContext(ctx context.Context, content []byte) err
|
||||
}
|
||||
|
||||
type ProviderLocalOptions struct {
|
||||
Path string `json:"path"`
|
||||
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
|
||||
Path string `json:"path"`
|
||||
RemoveEmojis bool `json:"remove_emojis,omitempty"`
|
||||
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
|
||||
}
|
||||
|
||||
type ProviderRemoteOptions struct {
|
||||
@@ -57,14 +58,16 @@ type ProviderRemoteOptions struct {
|
||||
DownloadDetour string `json:"download_detour,omitempty"`
|
||||
UpdateInterval badoption.Duration `json:"update_interval,omitempty"`
|
||||
|
||||
Exclude *badoption.Regexp `json:"exclude,omitempty"`
|
||||
Include *badoption.Regexp `json:"include,omitempty"`
|
||||
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
|
||||
Exclude *badoption.Regexp `json:"exclude,omitempty"`
|
||||
Include *badoption.Regexp `json:"include,omitempty"`
|
||||
RemoveEmojis bool `json:"remove_emojis,omitempty"`
|
||||
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
|
||||
}
|
||||
|
||||
type ProviderInlineOptions struct {
|
||||
Outbounds []Outbound `json:"outbounds,omitempty"`
|
||||
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
|
||||
Outbounds []Outbound `json:"outbounds,omitempty"`
|
||||
RemoveEmojis bool `json:"remove_emojis,omitempty"`
|
||||
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
|
||||
}
|
||||
|
||||
type ProviderHealthCheckOptions struct {
|
||||
|
||||
41
option/sudoku.go
Normal file
41
option/sudoku.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package option
|
||||
|
||||
type SudokuOutboundOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
Key string `json:"key"`
|
||||
AEADMethod string `json:"aead_method,omitempty"`
|
||||
PaddingMin *int `json:"padding_min,omitempty"`
|
||||
PaddingMax *int `json:"padding_max,omitempty"`
|
||||
TableType string `json:"table_type,omitempty"`
|
||||
EnablePureDownlink *bool `json:"enable_pure_downlink,omitempty"`
|
||||
CustomTable string `json:"custom_table,omitempty"`
|
||||
CustomTables []string `json:"custom_tables,omitempty"`
|
||||
HTTPMask *SudokuHTTPMask `json:"http_mask,omitempty"`
|
||||
}
|
||||
|
||||
type SudokuHTTPMask struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
PathRoot string `json:"path_root,omitempty"`
|
||||
Multiplex string `json:"multiplex,omitempty"`
|
||||
OutboundTLSOptionsContainer
|
||||
}
|
||||
|
||||
type SudokuInboundOptions struct {
|
||||
ListenOptions
|
||||
Key string `json:"key"`
|
||||
AEADMethod string `json:"aead_method,omitempty"`
|
||||
PaddingMin *int `json:"padding_min,omitempty"`
|
||||
PaddingMax *int `json:"padding_max,omitempty"`
|
||||
TableType string `json:"table_type,omitempty"`
|
||||
HandshakeTimeout *int `json:"handshake_timeout,omitempty"`
|
||||
EnablePureDownlink *bool `json:"enable_pure_downlink,omitempty"`
|
||||
CustomTable string `json:"custom_table,omitempty"`
|
||||
CustomTables []string `json:"custom_tables,omitempty"`
|
||||
DisableHTTPMask bool `json:"disable_http_mask,omitempty"`
|
||||
HTTPMaskMode string `json:"http_mask_mode,omitempty"`
|
||||
PathRoot string `json:"path_root,omitempty"`
|
||||
Fallback string `json:"fallback,omitempty"`
|
||||
}
|
||||
38
option/trusttunnel.go
Normal file
38
option/trusttunnel.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package option
|
||||
|
||||
type TrustTunnelInboundOptions struct {
|
||||
ListenOptions
|
||||
InboundTLSOptionsContainer
|
||||
Users []TrustTunnelUser `json:"users,omitempty"`
|
||||
Network NetworkList `json:"network,omitempty"`
|
||||
CongestionController string `json:"congestion_controller,omitempty"`
|
||||
BBRProfile string `json:"bbr_profile,omitempty"`
|
||||
CWND int `json:"cwnd,omitempty"`
|
||||
}
|
||||
|
||||
type TrustTunnelUser struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
type TrustTunnelMultiplexOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
MaxConnections int `json:"max_connections,omitempty"`
|
||||
MinStreams int `json:"min_streams,omitempty"`
|
||||
MaxStreams int `json:"max_streams,omitempty"`
|
||||
}
|
||||
|
||||
type TrustTunnelOutboundOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
OutboundTLSOptionsContainer
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Network NetworkList `json:"network,omitempty"`
|
||||
HealthCheck bool `json:"health_check,omitempty"`
|
||||
QUIC bool `json:"quic,omitempty"`
|
||||
CongestionController string `json:"congestion_controller,omitempty"`
|
||||
BBRProfile string `json:"bbr_profile,omitempty"`
|
||||
CWND int `json:"cwnd,omitempty"`
|
||||
Multiplex *TrustTunnelMultiplexOptions `json:"multiplex,omitempty"`
|
||||
}
|
||||
@@ -96,6 +96,12 @@ func (h *Inbound) Close() error {
|
||||
return common.Close(h.listener, h.tlsConfig)
|
||||
}
|
||||
|
||||
func (h *Inbound) UpdateUsers(users []option.AnyTLSUser) {
|
||||
h.service.UpdateUsers(common.Map(users, func(it option.AnyTLSUser) anytls.User {
|
||||
return anytls.User(it)
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
if h.tlsConfig != nil {
|
||||
tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig)
|
||||
|
||||
@@ -86,6 +86,10 @@ func (h *Inbound) Close() error {
|
||||
)
|
||||
}
|
||||
|
||||
func (h *Inbound) UpdateUsers(users []auth.User) {
|
||||
h.authenticator.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
if h.tlsConfig != nil {
|
||||
tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig)
|
||||
|
||||
@@ -3,15 +3,17 @@ package bandwidth
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/common/onclose"
|
||||
)
|
||||
|
||||
type connWithDownloadBandwidthLimiter struct {
|
||||
net.Conn
|
||||
ctx context.Context
|
||||
limiter Limiter
|
||||
limiter BandwidthLimiter
|
||||
}
|
||||
|
||||
func NewConnWithDownloadBandwidthLimiter(ctx context.Context, conn net.Conn, limiter Limiter) *connWithDownloadBandwidthLimiter {
|
||||
func NewConnWithDownloadBandwidthLimiter(ctx context.Context, conn net.Conn, limiter BandwidthLimiter) *connWithDownloadBandwidthLimiter {
|
||||
return &connWithDownloadBandwidthLimiter{conn, ctx, limiter}
|
||||
}
|
||||
|
||||
@@ -26,10 +28,10 @@ func (conn *connWithDownloadBandwidthLimiter) Write(p []byte) (n int, err error)
|
||||
type connWithUploadBandwidthLimiter struct {
|
||||
net.Conn
|
||||
ctx context.Context
|
||||
limiter Limiter
|
||||
limiter BandwidthLimiter
|
||||
}
|
||||
|
||||
func NewConnWithUploadBandwidthLimiter(ctx context.Context, conn net.Conn, limiter Limiter) *connWithUploadBandwidthLimiter {
|
||||
func NewConnWithUploadBandwidthLimiter(ctx context.Context, conn net.Conn, limiter BandwidthLimiter) *connWithUploadBandwidthLimiter {
|
||||
return &connWithUploadBandwidthLimiter{conn, ctx, limiter}
|
||||
}
|
||||
|
||||
@@ -47,10 +49,10 @@ func (conn *connWithUploadBandwidthLimiter) Read(p []byte) (n int, err error) {
|
||||
|
||||
type connWithCloseHandler struct {
|
||||
net.Conn
|
||||
onClose CloseHandlerFunc
|
||||
onClose onclose.CloseHandlerFunc
|
||||
}
|
||||
|
||||
func NewConnWithCloseHandler(conn net.Conn, onClose CloseHandlerFunc) *connWithCloseHandler {
|
||||
func NewConnWithCloseHandler(conn net.Conn, onClose onclose.CloseHandlerFunc) *connWithCloseHandler {
|
||||
return &connWithCloseHandler{conn, onClose}
|
||||
}
|
||||
|
||||
@@ -62,10 +64,10 @@ func (conn *connWithCloseHandler) Close() error {
|
||||
type packetConnWithDownloadBandwidthLimiter struct {
|
||||
net.PacketConn
|
||||
ctx context.Context
|
||||
limiter Limiter
|
||||
limiter BandwidthLimiter
|
||||
}
|
||||
|
||||
func NewPacketConnWithDownloadBandwidthLimiter(ctx context.Context, conn net.PacketConn, limiter Limiter) *packetConnWithDownloadBandwidthLimiter {
|
||||
func NewPacketConnWithDownloadBandwidthLimiter(ctx context.Context, conn net.PacketConn, limiter BandwidthLimiter) *packetConnWithDownloadBandwidthLimiter {
|
||||
return &packetConnWithDownloadBandwidthLimiter{conn, ctx, limiter}
|
||||
}
|
||||
|
||||
@@ -80,10 +82,10 @@ func (conn *packetConnWithDownloadBandwidthLimiter) WriteTo(p []byte, addr net.A
|
||||
type packetConnWithUploadBandwidthLimiter struct {
|
||||
net.PacketConn
|
||||
ctx context.Context
|
||||
limiter Limiter
|
||||
limiter BandwidthLimiter
|
||||
}
|
||||
|
||||
func NewPacketConnWithUploadBandwidthLimiter(ctx context.Context, conn net.PacketConn, limiter Limiter) *packetConnWithUploadBandwidthLimiter {
|
||||
func NewPacketConnWithUploadBandwidthLimiter(ctx context.Context, conn net.PacketConn, limiter BandwidthLimiter) *packetConnWithUploadBandwidthLimiter {
|
||||
return &packetConnWithUploadBandwidthLimiter{conn, ctx, limiter}
|
||||
}
|
||||
|
||||
@@ -101,10 +103,10 @@ func (conn *packetConnWithUploadBandwidthLimiter) ReadFrom(p []byte) (n int, add
|
||||
|
||||
type packetConnWithCloseHandler struct {
|
||||
net.PacketConn
|
||||
onClose CloseHandlerFunc
|
||||
onClose onclose.CloseHandlerFunc
|
||||
}
|
||||
|
||||
func NewPacketConnWithCloseHandler(conn net.PacketConn, onClose CloseHandlerFunc) *packetConnWithCloseHandler {
|
||||
func NewPacketConnWithCloseHandler(conn net.PacketConn, onClose onclose.CloseHandlerFunc) *packetConnWithCloseHandler {
|
||||
return &packetConnWithCloseHandler{conn, onClose}
|
||||
}
|
||||
|
||||
@@ -113,38 +115,38 @@ func (conn *packetConnWithCloseHandler) Close() error {
|
||||
return conn.PacketConn.Close()
|
||||
}
|
||||
|
||||
func connWithDownloadBandwidthWrapper(ctx context.Context, conn net.Conn, limiter Limiter, reverse bool) net.Conn {
|
||||
func connWithDownloadBandwidthWrapper(ctx context.Context, conn net.Conn, limiter BandwidthLimiter, 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 Limiter, reverse bool) net.Conn {
|
||||
func connWithUploadBandwidthWrapper(ctx context.Context, conn net.Conn, limiter BandwidthLimiter, reverse bool) net.Conn {
|
||||
if reverse {
|
||||
return NewConnWithDownloadBandwidthLimiter(ctx, conn, limiter)
|
||||
}
|
||||
return NewConnWithUploadBandwidthLimiter(ctx, conn, limiter)
|
||||
}
|
||||
|
||||
func connWithBidirectionalBandwidthWrapper(ctx context.Context, conn net.Conn, limiter Limiter, reverse bool) net.Conn {
|
||||
func connWithBidirectionalBandwidthWrapper(ctx context.Context, conn net.Conn, limiter BandwidthLimiter, reverse bool) net.Conn {
|
||||
return NewConnWithUploadBandwidthLimiter(ctx, NewConnWithDownloadBandwidthLimiter(ctx, conn, limiter), limiter)
|
||||
}
|
||||
|
||||
func packetConnWithDownloadBandwidthWrapper(ctx context.Context, conn net.PacketConn, limiter Limiter, reverse bool) net.PacketConn {
|
||||
func packetConnWithDownloadBandwidthWrapper(ctx context.Context, conn net.PacketConn, limiter BandwidthLimiter, 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 Limiter, reverse bool) net.PacketConn {
|
||||
func packetConnWithUploadBandwidthWrapper(ctx context.Context, conn net.PacketConn, limiter BandwidthLimiter, reverse bool) net.PacketConn {
|
||||
if reverse {
|
||||
return NewPacketConnWithDownloadBandwidthLimiter(ctx, conn, limiter)
|
||||
}
|
||||
return NewPacketConnWithUploadBandwidthLimiter(ctx, conn, limiter)
|
||||
}
|
||||
|
||||
func packetConnWithBidirectionalBandwidthWrapper(ctx context.Context, conn net.PacketConn, limiter Limiter, reverse bool) net.PacketConn {
|
||||
func packetConnWithBidirectionalBandwidthWrapper(ctx context.Context, conn net.PacketConn, limiter BandwidthLimiter, reverse bool) net.PacketConn {
|
||||
return NewPacketConnWithUploadBandwidthLimiter(ctx, NewPacketConnWithDownloadBandwidthLimiter(ctx, conn, limiter), limiter)
|
||||
}
|
||||
|
||||
@@ -9,12 +9,13 @@ import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
)
|
||||
|
||||
type Limiter interface {
|
||||
type BandwidthLimiter interface {
|
||||
WaitN(ctx context.Context, n int) (err error)
|
||||
SetSpeed(speed uint64)
|
||||
}
|
||||
|
||||
type FlowKeysLimiter struct {
|
||||
limiter Limiter
|
||||
limiter BandwidthLimiter
|
||||
connIDGetter ConnIDGetter
|
||||
|
||||
waits map[string][]*wait
|
||||
@@ -25,7 +26,7 @@ type FlowKeysLimiter struct {
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewFlowKeysLimiter(connIDGetter ConnIDGetter, limiter Limiter) *FlowKeysLimiter {
|
||||
func NewFlowKeysLimiter(connIDGetter ConnIDGetter, limiter BandwidthLimiter) *FlowKeysLimiter {
|
||||
return &FlowKeysLimiter{
|
||||
limiter: limiter,
|
||||
connIDGetter: connIDGetter,
|
||||
@@ -36,6 +37,10 @@ func NewFlowKeysLimiter(connIDGetter ConnIDGetter, limiter Limiter) *FlowKeysLim
|
||||
}
|
||||
}
|
||||
|
||||
func (l *FlowKeysLimiter) SetSpeed(speed uint64) {
|
||||
l.limiter.SetSpeed(speed)
|
||||
}
|
||||
|
||||
func (l *FlowKeysLimiter) WaitN(ctx context.Context, n int) error {
|
||||
id, _ := l.connIDGetter(ctx, adapter.ContextFrom(ctx))
|
||||
mainWait := &wait{ctx, make(chan struct{}), n}
|
||||
|
||||
@@ -7,16 +7,16 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/onclose"
|
||||
"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 Limiter, reverse bool) net.Conn
|
||||
PacketConnWrapper = func(ctx context.Context, conn net.PacketConn, limiter Limiter, reverse bool) net.PacketConn
|
||||
ConnWrapper = func(ctx context.Context, conn net.Conn, limiter BandwidthLimiter, reverse bool) net.Conn
|
||||
PacketConnWrapper = func(ctx context.Context, conn net.PacketConn, limiter BandwidthLimiter, reverse bool) net.PacketConn
|
||||
)
|
||||
|
||||
type BandwidthStrategy interface {
|
||||
@@ -24,8 +24,12 @@ type BandwidthStrategy interface {
|
||||
wrapPacketConn(ctx context.Context, conn net.PacketConn, metadata *adapter.InboundContext, reverse bool) (net.PacketConn, error)
|
||||
}
|
||||
|
||||
type SpeedUpdater interface {
|
||||
SetSpeed(speed uint64)
|
||||
}
|
||||
|
||||
type BandwidthLimiterStrategy interface {
|
||||
getLimiter(ctx context.Context, metadata *adapter.InboundContext) (Limiter, CloseHandlerFunc, error)
|
||||
getLimiter(ctx context.Context, metadata *adapter.InboundContext) (BandwidthLimiter, onclose.CloseHandlerFunc, error)
|
||||
}
|
||||
|
||||
type DefaultWrapStrategy struct {
|
||||
@@ -54,8 +58,14 @@ func (s *DefaultWrapStrategy) wrapPacketConn(ctx context.Context, conn net.Packe
|
||||
return NewPacketConnWithCloseHandler(s.packetConnWrapper(ctx, conn, limiter, reverse), onClose), nil
|
||||
}
|
||||
|
||||
func (s *DefaultWrapStrategy) SetSpeed(speed uint64) {
|
||||
if updater, ok := s.limiterStrategy.(SpeedUpdater); ok {
|
||||
updater.SetSpeed(speed)
|
||||
}
|
||||
}
|
||||
|
||||
type GlobalBandwidthStrategy struct {
|
||||
limiter Limiter
|
||||
limiter BandwidthLimiter
|
||||
}
|
||||
|
||||
func NewGlobalBandwidthStrategy(speed uint64, flowKeys []string) (*GlobalBandwidthStrategy, error) {
|
||||
@@ -68,12 +78,16 @@ func NewGlobalBandwidthStrategy(speed uint64, flowKeys []string) (*GlobalBandwid
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *GlobalBandwidthStrategy) getLimiter(ctx context.Context, metadata *adapter.InboundContext) (Limiter, CloseHandlerFunc, error) {
|
||||
func (s *GlobalBandwidthStrategy) getLimiter(ctx context.Context, metadata *adapter.InboundContext) (BandwidthLimiter, onclose.CloseHandlerFunc, error) {
|
||||
return s.limiter, func() {}, nil
|
||||
}
|
||||
|
||||
func (s *GlobalBandwidthStrategy) SetSpeed(speed uint64) {
|
||||
s.limiter.SetSpeed(speed)
|
||||
}
|
||||
|
||||
type idBandwidthLimiter struct {
|
||||
limiter Limiter
|
||||
limiter BandwidthLimiter
|
||||
handles uint32
|
||||
}
|
||||
|
||||
@@ -94,7 +108,7 @@ func NewConnectionBandwidthStrategy(connIDGetter ConnIDGetter, speed uint64, flo
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ConnectionBandwidthStrategy) getLimiter(ctx context.Context, metadata *adapter.InboundContext) (Limiter, CloseHandlerFunc, error) {
|
||||
func (s *ConnectionBandwidthStrategy) getLimiter(ctx context.Context, metadata *adapter.InboundContext) (BandwidthLimiter, onclose.CloseHandlerFunc, error) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
id, ok := s.connIDGetter(ctx, metadata)
|
||||
@@ -126,6 +140,15 @@ func (s *ConnectionBandwidthStrategy) getLimiter(ctx context.Context, metadata *
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ConnectionBandwidthStrategy) SetSpeed(speed uint64) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
s.speed = speed
|
||||
for _, limiter := range s.limiters {
|
||||
limiter.limiter.SetSpeed(speed)
|
||||
}
|
||||
}
|
||||
|
||||
type UsersBandwidthStrategy struct {
|
||||
strategies map[string]BandwidthStrategy
|
||||
mtx sync.Mutex
|
||||
@@ -167,20 +190,86 @@ func (s *UsersBandwidthStrategy) getStrategy(ctx context.Context, metadata *adap
|
||||
return nil, E.New("user strategy not found: ", user)
|
||||
}
|
||||
|
||||
type bwConnEntry struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
|
||||
|
||||
type ManagerBandwidthStrategy struct {
|
||||
*UsersBandwidthStrategy
|
||||
strategies map[string]BandwidthStrategy
|
||||
conns map[string][]*bwConnEntry
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewManagerBandwidthStrategy() *ManagerBandwidthStrategy {
|
||||
return &ManagerBandwidthStrategy{
|
||||
UsersBandwidthStrategy: NewUsersBandwidthStrategy(map[string]BandwidthStrategy{}),
|
||||
strategies: make(map[string]BandwidthStrategy),
|
||||
conns: make(map[string][]*bwConnEntry),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ManagerBandwidthStrategy) wrapConn(ctx context.Context, conn net.Conn, metadata *adapter.InboundContext, reverse bool) (net.Conn, error) {
|
||||
s.mtx.Lock()
|
||||
var user string
|
||||
if metadata != nil {
|
||||
user = metadata.User
|
||||
}
|
||||
strategy, ok := s.strategies[user]
|
||||
s.mtx.Unlock()
|
||||
if !ok {
|
||||
return nil, E.New("user strategy not found: ", user)
|
||||
}
|
||||
wrapped, err := strategy.wrapConn(ctx, conn, metadata, reverse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry := &bwConnEntry{conn: conn}
|
||||
s.mtx.Lock()
|
||||
s.conns[user] = append(s.conns[user], entry)
|
||||
s.mtx.Unlock()
|
||||
return onclose.NewConn(wrapped, func() {
|
||||
s.mtx.Lock()
|
||||
entries := s.conns[user]
|
||||
for i, e := range entries {
|
||||
if e == entry {
|
||||
s.conns[user] = append(entries[:i], entries[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mtx.Unlock()
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (s *ManagerBandwidthStrategy) wrapPacketConn(ctx context.Context, conn net.PacketConn, metadata *adapter.InboundContext, reverse bool) (net.PacketConn, error) {
|
||||
s.mtx.Lock()
|
||||
var user string
|
||||
if metadata != nil {
|
||||
user = metadata.User
|
||||
}
|
||||
strategy, ok := s.strategies[user]
|
||||
s.mtx.Unlock()
|
||||
if !ok {
|
||||
return nil, E.New("user strategy not found: ", user)
|
||||
}
|
||||
return strategy.wrapPacketConn(ctx, conn, metadata, reverse)
|
||||
}
|
||||
|
||||
func (s *ManagerBandwidthStrategy) UpdateStrategies(strategies map[string]BandwidthStrategy) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
var closedEntries []*bwConnEntry
|
||||
for user, entries := range s.conns {
|
||||
if _, exists := strategies[user]; !exists {
|
||||
closedEntries = append(closedEntries, entries...)
|
||||
delete(s.conns, user)
|
||||
}
|
||||
}
|
||||
s.strategies = strategies
|
||||
s.mtx.Unlock()
|
||||
for _, entry := range closedEntries {
|
||||
entry.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type BypassBandwidthStrategy struct{}
|
||||
@@ -263,8 +352,8 @@ func CreateStrategy(strategy string, mode string, connectionType string, speed u
|
||||
return NewDefaultWrapStrategy(limiterStrategy, connWrapper, packetConnWrapper), nil
|
||||
}
|
||||
|
||||
func createSpeedLimiter(speed uint64, flowKeys []string) (Limiter, error) {
|
||||
var limiter Limiter = rate.NewLimiter(rate.Limit(float64(speed)), 65536)
|
||||
func createSpeedLimiter(speed uint64, flowKeys []string) (BandwidthLimiter, error) {
|
||||
var limiter BandwidthLimiter = &speedLimiter{limiter: rate.NewLimiter(rate.Limit(float64(speed)), 65536)}
|
||||
for i := len(flowKeys) - 1; i >= 0; i-- {
|
||||
getter, err := flowKeysConnIDGetter(flowKeys[i])
|
||||
if err != nil {
|
||||
@@ -275,16 +364,24 @@ func createSpeedLimiter(speed uint64, flowKeys []string) (Limiter, error) {
|
||||
return limiter, nil
|
||||
}
|
||||
|
||||
type speedLimiter struct {
|
||||
limiter *rate.Limiter
|
||||
}
|
||||
|
||||
func (r *speedLimiter) WaitN(ctx context.Context, n int) error {
|
||||
return r.limiter.WaitN(ctx, n)
|
||||
}
|
||||
|
||||
func (r *speedLimiter) SetSpeed(speed uint64) {
|
||||
r.limiter.SetLimit(rate.Limit(float64(speed)))
|
||||
}
|
||||
|
||||
func flowKeysConnIDGetter(name string) (ConnIDGetter, error) {
|
||||
switch name {
|
||||
case "user":
|
||||
return func(ctx context.Context, metadata *adapter.InboundContext) (string, bool) {
|
||||
return metadata.User, true
|
||||
}, nil
|
||||
case "destination":
|
||||
return func(ctx context.Context, metadata *adapter.InboundContext) (string, bool) {
|
||||
return metadata.Destination.String(), true
|
||||
}, nil
|
||||
case "source_ip":
|
||||
return func(ctx context.Context, metadata *adapter.InboundContext) (string, bool) {
|
||||
return metadata.Source.IPAddr().String(), true
|
||||
@@ -302,6 +399,14 @@ func flowKeysConnIDGetter(name string) (ConnIDGetter, error) {
|
||||
}
|
||||
return strconv.FormatUint(uint64(id.ID), 10), ok
|
||||
}, nil
|
||||
case "protocol":
|
||||
return func(ctx context.Context, metadata *adapter.InboundContext) (string, bool) {
|
||||
return metadata.Protocol, metadata.Protocol != ""
|
||||
}, nil
|
||||
case "destination":
|
||||
return func(ctx context.Context, metadata *adapter.InboundContext) (string, bool) {
|
||||
return metadata.Destination.String(), true
|
||||
}, nil
|
||||
default:
|
||||
return nil, E.New("flow key not found: ", name)
|
||||
}
|
||||
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/common/onclose"
|
||||
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) {
|
||||
return func(id string) (onclose.CloseHandlerFunc, context.Context, error) {
|
||||
mtx.Lock()
|
||||
defer mtx.Unlock()
|
||||
handles, ok := locks[id]
|
||||
@@ -22,16 +23,13 @@ func NewDefaultLock(max uint32) LockIDGetter {
|
||||
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)
|
||||
}
|
||||
})
|
||||
mtx.Lock()
|
||||
defer mtx.Unlock()
|
||||
*handles--
|
||||
if *handles == 0 {
|
||||
delete(locks, id)
|
||||
}
|
||||
}, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"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/common/onclose"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/route"
|
||||
@@ -110,7 +111,7 @@ func (h *Outbound) DialContext(ctx context.Context, network string, destination
|
||||
onClose()
|
||||
return nil, err
|
||||
}
|
||||
conn = newConnWithCloseHandlerFunc(conn, onClose)
|
||||
conn = onclose.NewConn(conn, onClose)
|
||||
if lockCtx != nil {
|
||||
go connChecker(lockCtx, conn.Close)
|
||||
}
|
||||
@@ -127,7 +128,7 @@ func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
|
||||
onClose()
|
||||
return nil, err
|
||||
}
|
||||
conn = newPacketConnWithCloseHandlerFunc(conn, onClose)
|
||||
conn = onclose.NewPacketConn(conn, onClose)
|
||||
if lockCtx != nil {
|
||||
go connChecker(lockCtx, conn.Close)
|
||||
}
|
||||
@@ -141,7 +142,7 @@ func (h *Outbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
return
|
||||
}
|
||||
conn = newConnWithCloseHandlerFunc(conn, limiterOnClose)
|
||||
conn = onclose.NewConn(conn, limiterOnClose)
|
||||
if lockCtx != nil {
|
||||
go connChecker(lockCtx, conn.Close)
|
||||
}
|
||||
@@ -158,7 +159,7 @@ func (h *Outbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
return
|
||||
}
|
||||
conn = bufio.NewPacketConn(newPacketConnWithCloseHandlerFunc(bufio.NewNetPacketConn(conn), limiterOnClose))
|
||||
conn = bufio.NewPacketConn(onclose.NewPacketConn(bufio.NewNetPacketConn(conn), limiterOnClose))
|
||||
if lockCtx != nil {
|
||||
go connChecker(lockCtx, conn.Close)
|
||||
}
|
||||
@@ -172,33 +173,7 @@ 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()
|
||||
|
||||
@@ -6,18 +6,17 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/onclose"
|
||||
"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)
|
||||
LockIDGetter = func(string) (onclose.CloseHandlerFunc, context.Context, error)
|
||||
|
||||
ConnectionStrategy interface {
|
||||
request(ctx context.Context, metadata *adapter.InboundContext) (onClose CloseHandlerFunc, lockCtx context.Context, err error)
|
||||
request(ctx context.Context, metadata *adapter.InboundContext) (onClose onclose.CloseHandlerFunc, lockCtx context.Context, err error)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -36,7 +35,7 @@ func NewDefaultConnectionStrategy(connIDGetter ConnIDGetter, lockIDGetter LockID
|
||||
return outbound
|
||||
}
|
||||
|
||||
func (s *DefaultConnectionStrategy) request(ctx context.Context, metadata *adapter.InboundContext) (CloseHandlerFunc, context.Context, error) {
|
||||
func (s *DefaultConnectionStrategy) request(ctx context.Context, metadata *adapter.InboundContext) (onclose.CloseHandlerFunc, context.Context, error) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
id, ok := s.connIDGetter(ctx, metadata)
|
||||
@@ -57,7 +56,7 @@ func NewUsersConnectionStrategy(strategies map[string]ConnectionStrategy) *Users
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UsersConnectionStrategy) request(ctx context.Context, metadata *adapter.InboundContext) (CloseHandlerFunc, context.Context, error) {
|
||||
func (s *UsersConnectionStrategy) request(ctx context.Context, metadata *adapter.InboundContext) (onclose.CloseHandlerFunc, context.Context, error) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
var user string
|
||||
@@ -71,20 +70,78 @@ func (s *UsersConnectionStrategy) request(ctx context.Context, metadata *adapter
|
||||
return nil, nil, E.New("user strategy not found: ", user)
|
||||
}
|
||||
|
||||
type cancelEntry struct {
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type ManagerConnectionStrategy struct {
|
||||
*UsersConnectionStrategy
|
||||
strategies map[string]ConnectionStrategy
|
||||
cancels map[string][]*cancelEntry
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewManagerConnectionStrategy() *ManagerConnectionStrategy {
|
||||
return &ManagerConnectionStrategy{
|
||||
UsersConnectionStrategy: NewUsersConnectionStrategy(map[string]ConnectionStrategy{}),
|
||||
strategies: make(map[string]ConnectionStrategy),
|
||||
cancels: make(map[string][]*cancelEntry),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ManagerConnectionStrategy) request(ctx context.Context, metadata *adapter.InboundContext) (onclose.CloseHandlerFunc, context.Context, error) {
|
||||
s.mtx.Lock()
|
||||
var user string
|
||||
if metadata != nil {
|
||||
user = metadata.User
|
||||
}
|
||||
strategy, ok := s.strategies[user]
|
||||
if !ok {
|
||||
s.mtx.Unlock()
|
||||
return nil, nil, E.New("user strategy not found: ", user)
|
||||
}
|
||||
s.mtx.Unlock()
|
||||
onClose, _, err := strategy.request(ctx, metadata)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
cancelCtx, cancel := context.WithCancel(context.Background())
|
||||
entry := &cancelEntry{cancel: cancel}
|
||||
s.mtx.Lock()
|
||||
s.cancels[user] = append(s.cancels[user], entry)
|
||||
s.mtx.Unlock()
|
||||
originalOnClose := onClose
|
||||
wrappedOnClose := func() {
|
||||
s.mtx.Lock()
|
||||
entries := s.cancels[user]
|
||||
for i, e := range entries {
|
||||
if e == entry {
|
||||
s.cancels[user] = append(entries[:i], entries[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mtx.Unlock()
|
||||
cancel()
|
||||
if originalOnClose != nil {
|
||||
originalOnClose()
|
||||
}
|
||||
}
|
||||
return wrappedOnClose, cancelCtx, nil
|
||||
}
|
||||
|
||||
func (s *ManagerConnectionStrategy) UpdateStrategies(strategies map[string]ConnectionStrategy) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
var entries []*cancelEntry
|
||||
for user, cancels := range s.cancels {
|
||||
if _, exists := strategies[user]; !exists {
|
||||
entries = append(entries, cancels...)
|
||||
delete(s.cancels, user)
|
||||
}
|
||||
}
|
||||
s.strategies = strategies
|
||||
s.mtx.Unlock()
|
||||
for _, entry := range entries {
|
||||
entry.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
type BypassConnectionStrategy struct{}
|
||||
@@ -93,7 +150,7 @@ func NewBypassConnectionStrategy() *BypassConnectionStrategy {
|
||||
return &BypassConnectionStrategy{}
|
||||
}
|
||||
|
||||
func (s *BypassConnectionStrategy) request(ctx context.Context, metadata *adapter.InboundContext) (CloseHandlerFunc, context.Context, error) {
|
||||
func (s *BypassConnectionStrategy) request(ctx context.Context, metadata *adapter.InboundContext) (onclose.CloseHandlerFunc, context.Context, error) {
|
||||
return func() {}, nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/onclose"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type (
|
||||
CloseHandlerFunc = func()
|
||||
ConnWrapper = func(ctx context.Context, conn net.Conn, limiter TrafficLimiter, reverse bool) net.Conn
|
||||
PacketConnWrapper = func(ctx context.Context, conn net.PacketConn, limiter TrafficLimiter, reverse bool) net.PacketConn
|
||||
)
|
||||
@@ -72,32 +72,60 @@ func (s *GlobalTrafficStrategy) getLimiter(ctx context.Context, metadata *adapte
|
||||
return s.limiter, nil
|
||||
}
|
||||
|
||||
type connEntry struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
|
||||
|
||||
type ManagerTrafficStrategy struct {
|
||||
strategies map[string]TrafficStrategy
|
||||
mtx sync.Mutex
|
||||
conns map[string][]*connEntry
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewManagerTrafficStrategy() *ManagerTrafficStrategy {
|
||||
return &ManagerTrafficStrategy{}
|
||||
return &ManagerTrafficStrategy{
|
||||
conns: make(map[string][]*connEntry),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ManagerTrafficStrategy) wrapConn(ctx context.Context, conn net.Conn, metadata *adapter.InboundContext, reverse bool) (net.Conn, error) {
|
||||
strategy, err := s.getStrategy(ctx, metadata)
|
||||
strategy, user, err := s.getStrategy(ctx, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return strategy.wrapConn(ctx, conn, metadata, reverse)
|
||||
wrapped, err := strategy.wrapConn(ctx, conn, metadata, reverse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry := &connEntry{conn: conn}
|
||||
s.mtx.Lock()
|
||||
s.conns[user] = append(s.conns[user], entry)
|
||||
s.mtx.Unlock()
|
||||
return onclose.NewConn(wrapped, func() {
|
||||
s.mtx.Lock()
|
||||
entries := s.conns[user]
|
||||
for i, e := range entries {
|
||||
if e == entry {
|
||||
s.conns[user] = append(entries[:i], entries[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mtx.Unlock()
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (s *ManagerTrafficStrategy) wrapPacketConn(ctx context.Context, conn net.PacketConn, metadata *adapter.InboundContext, reverse bool) (net.PacketConn, error) {
|
||||
strategy, err := s.getStrategy(ctx, metadata)
|
||||
strategy, _, err := s.getStrategy(ctx, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return strategy.wrapPacketConn(ctx, conn, metadata, reverse)
|
||||
}
|
||||
|
||||
func (s *ManagerTrafficStrategy) getStrategy(ctx context.Context, metadata *adapter.InboundContext) (TrafficStrategy, error) {
|
||||
func (s *ManagerTrafficStrategy) getStrategy(ctx context.Context, metadata *adapter.InboundContext) (TrafficStrategy, string, error) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
var user string
|
||||
@@ -106,15 +134,25 @@ func (s *ManagerTrafficStrategy) getStrategy(ctx context.Context, metadata *adap
|
||||
}
|
||||
strategy, ok := s.strategies[user]
|
||||
if ok {
|
||||
return strategy, nil
|
||||
return strategy, user, nil
|
||||
}
|
||||
return nil, E.New("user strategy not found: ", user)
|
||||
return nil, user, E.New("user strategy not found: ", user)
|
||||
}
|
||||
|
||||
func (s *ManagerTrafficStrategy) UpdateStrategies(strategies map[string]TrafficStrategy) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
var closedEntries []*connEntry
|
||||
for user, entries := range s.conns {
|
||||
if _, exists := strategies[user]; !exists {
|
||||
closedEntries = append(closedEntries, entries...)
|
||||
delete(s.conns, user)
|
||||
}
|
||||
}
|
||||
s.strategies = strategies
|
||||
s.mtx.Unlock()
|
||||
for _, entry := range closedEntries {
|
||||
entry.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type BypassTrafficStrategy struct{}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/common/cloudflare"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
@@ -99,7 +100,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
logger.ErrorContext(ctx, E.New("failed to generate cert: ", err))
|
||||
return
|
||||
}
|
||||
tlsConfig, err := tls.NewMASQUEClient(ctx, logger, "consumer-masque.cloudflareclient.com", cert, privKey, peerPubKey, options.MASQUEOutboundTLSOptions)
|
||||
tlsConfig, err := tls.NewMASQUEClient(ctx, logger, "consumer-masque.cloudflareclient.com", cert, privKey, peerPubKey, common.PtrValueOrDefault(options.TLS))
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, E.New("failed to prepare TLS config: ", err))
|
||||
return
|
||||
|
||||
340
protocol/mieru/inbound.go
Normal file
340
protocol/mieru/inbound.go
Normal file
@@ -0,0 +1,340 @@
|
||||
package mieru
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
"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"
|
||||
"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"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
mierucommon "github.com/enfein/mieru/v3/apis/common"
|
||||
mieruconstant "github.com/enfein/mieru/v3/apis/constant"
|
||||
mierumodel "github.com/enfein/mieru/v3/apis/model"
|
||||
mieruserver "github.com/enfein/mieru/v3/apis/server"
|
||||
mierutp "github.com/enfein/mieru/v3/apis/trafficpattern"
|
||||
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func RegisterInbound(registry *inbound.Registry) {
|
||||
inbound.Register[option.MieruInboundOptions](registry, C.TypeMieru, NewInbound)
|
||||
}
|
||||
|
||||
type Inbound struct {
|
||||
inbound.Adapter
|
||||
ctx context.Context
|
||||
router adapter.ConnectionRouterEx
|
||||
logger log.ContextLogger
|
||||
listener *listener.Listener
|
||||
server mieruserver.Server
|
||||
userNames []string
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.MieruInboundOptions) (adapter.Inbound, error) {
|
||||
config, userNames, err := buildMieruServerConfig(ctx, options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build mieru server config: %w", err)
|
||||
}
|
||||
|
||||
s := mieruserver.NewServer()
|
||||
if err := s.Store(config); err != nil {
|
||||
return nil, fmt.Errorf("failed to store mieru server config: %w", err)
|
||||
}
|
||||
|
||||
inboundInstance := &Inbound{
|
||||
Adapter: inbound.NewAdapter(C.TypeMieru, tag),
|
||||
ctx: ctx,
|
||||
router: uot.NewRouter(router, logger),
|
||||
logger: logger,
|
||||
server: s,
|
||||
userNames: userNames,
|
||||
}
|
||||
inboundInstance.listener = listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Network: []string{N.NetworkTCP, N.NetworkUDP},
|
||||
Listen: options.ListenOptions,
|
||||
})
|
||||
|
||||
return inboundInstance, nil
|
||||
}
|
||||
|
||||
func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if err := h.server.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start mieru server: %w", err)
|
||||
}
|
||||
|
||||
h.logger.Info("mieru server is started")
|
||||
go h.acceptLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Inbound) Close() error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if h.server.IsRunning() {
|
||||
return h.server.Stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Inbound) acceptLoop() {
|
||||
for {
|
||||
conn, request, err := h.server.Accept()
|
||||
if err != nil {
|
||||
if !h.server.IsRunning() {
|
||||
return
|
||||
}
|
||||
h.logger.Debug("failed to accept mieru connection: ", err)
|
||||
continue
|
||||
}
|
||||
go h.handleConnection(conn, request)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Inbound) handleConnection(conn net.Conn, request *mierumodel.Request) {
|
||||
ctx := log.ContextWithNewID(h.ctx)
|
||||
|
||||
// Send fake SOCKS5 response back to proxy client.
|
||||
resp := &mierumodel.Response{
|
||||
Reply: mieruconstant.Socks5ReplySuccess,
|
||||
BindAddr: mierumodel.AddrSpec{
|
||||
IP: net.IPv4zero,
|
||||
Port: 0,
|
||||
},
|
||||
}
|
||||
if err := resp.WriteToSocks5(conn); err != nil {
|
||||
conn.Close()
|
||||
h.logger.DebugContext(ctx, "failed to write mieru response: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Build metadata.
|
||||
var metadata adapter.InboundContext
|
||||
metadata.Inbound = h.Tag()
|
||||
metadata.InboundType = h.Type()
|
||||
//nolint:staticcheck
|
||||
metadata.InboundDetour = h.listener.ListenOptions().Detour
|
||||
metadata.UDPDisableDomainUnmapping = h.listener.ListenOptions().UDPDisableDomainUnmapping
|
||||
|
||||
// Parse source address.
|
||||
if remoteAddr := conn.RemoteAddr(); remoteAddr != nil {
|
||||
metadata.Source = M.SocksaddrFromNet(remoteAddr)
|
||||
}
|
||||
|
||||
// Parse destination from request.
|
||||
if request.DstAddr.FQDN != "" {
|
||||
metadata.Destination = M.Socksaddr{
|
||||
Fqdn: request.DstAddr.FQDN,
|
||||
Port: uint16(request.DstAddr.Port),
|
||||
}
|
||||
} else if request.DstAddr.IP != nil {
|
||||
addr, _ := netip.AddrFromSlice(request.DstAddr.IP)
|
||||
metadata.Destination = M.Socksaddr{
|
||||
Addr: addr.Unmap(),
|
||||
Port: uint16(request.DstAddr.Port),
|
||||
}
|
||||
}
|
||||
|
||||
// Get username from connection.
|
||||
if userCtx, ok := conn.(mierucommon.UserContext); ok {
|
||||
metadata.User = userCtx.UserName()
|
||||
}
|
||||
|
||||
// Handle request.
|
||||
switch request.Command {
|
||||
case mieruconstant.Socks5ConnectCmd:
|
||||
h.logger.InfoContext(ctx, "inbound TCP connection from ", metadata.Source, " to ", metadata.Destination)
|
||||
if metadata.User != "" {
|
||||
h.logger.InfoContext(ctx, "[", metadata.User, "] inbound TCP connection")
|
||||
}
|
||||
h.router.RouteConnectionEx(ctx, conn, metadata, nil)
|
||||
case mieruconstant.Socks5UDPAssociateCmd:
|
||||
h.logger.InfoContext(ctx, "inbound UDP connection from ", metadata.Source, " to ", metadata.Destination)
|
||||
if metadata.User != "" {
|
||||
h.logger.InfoContext(ctx, "[", metadata.User, "] inbound UDP connection")
|
||||
}
|
||||
h.handleUDP(ctx, conn, metadata)
|
||||
default:
|
||||
conn.Close()
|
||||
h.logger.WarnContext(ctx, "unsupported mieru command: ", request.Command)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Inbound) handleUDP(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) {
|
||||
pc := mierucommon.NewPacketOverStreamTunnel(conn)
|
||||
packetConn := &mieruPacketConn{
|
||||
PacketConn: pc,
|
||||
destination: metadata.Destination,
|
||||
}
|
||||
h.router.RoutePacketConnectionEx(ctx, packetConn, metadata, nil)
|
||||
}
|
||||
|
||||
// mieruPacketConn wraps mieru's PacketConn to implement N.PacketConn
|
||||
type mieruPacketConn struct {
|
||||
net.PacketConn
|
||||
destination M.Socksaddr
|
||||
}
|
||||
|
||||
var _ N.PacketConn = (*mieruPacketConn)(nil)
|
||||
|
||||
// ReadPacket parses the SOCKS5 UDP header and returns the destination address.
|
||||
func (c *mieruPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||
n, _, err := c.PacketConn.ReadFrom(buffer.FreeBytes())
|
||||
if err != nil {
|
||||
return M.Socksaddr{}, err
|
||||
}
|
||||
buffer.Truncate(n)
|
||||
if buffer.Len() < 3 {
|
||||
return M.Socksaddr{}, io.ErrShortBuffer
|
||||
}
|
||||
|
||||
// Skip RSV (2 bytes) and FRAG (1 byte).
|
||||
buffer.Advance(3)
|
||||
|
||||
var addr mierumodel.AddrSpec
|
||||
if err := addr.ReadFromSocks5(buffer); err != nil {
|
||||
return M.Socksaddr{}, err
|
||||
}
|
||||
if addr.FQDN != "" {
|
||||
destination = M.Socksaddr{
|
||||
Fqdn: addr.FQDN,
|
||||
Port: uint16(addr.Port),
|
||||
}
|
||||
} else if addr.IP != nil {
|
||||
netAddr, _ := netip.AddrFromSlice(addr.IP)
|
||||
destination = M.Socksaddr{
|
||||
Addr: netAddr.Unmap(),
|
||||
Port: uint16(addr.Port),
|
||||
}
|
||||
}
|
||||
return destination, nil
|
||||
}
|
||||
|
||||
// WritePacket writes the SOCKS5 UDP header and the payload.
|
||||
func (c *mieruPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||
header := buf.NewSize(3 + M.MaxSocksaddrLength)
|
||||
defer header.Release()
|
||||
|
||||
// RSV (2 bytes) + FRAG (1 byte)
|
||||
common.Must(header.WriteZeroN(3))
|
||||
|
||||
var addr mierumodel.AddrSpec
|
||||
if destination.IsFqdn() {
|
||||
addr.FQDN = destination.Fqdn
|
||||
} else {
|
||||
addr.IP = destination.Addr.AsSlice()
|
||||
}
|
||||
addr.Port = int(destination.Port)
|
||||
if err := addr.WriteToSocks5(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
packet := buf.NewSize(header.Len() + buffer.Len())
|
||||
defer packet.Release()
|
||||
common.Must1(packet.Write(header.Bytes()))
|
||||
common.Must1(packet.Write(buffer.Bytes()))
|
||||
_, err := c.PacketConn.WriteTo(packet.Bytes(), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func buildMieruServerConfig(_ context.Context, options option.MieruInboundOptions) (*mieruserver.ServerConfig, []string, error) {
|
||||
if err := validateMieruInboundOptions(options); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to validate mieru options: %w", err)
|
||||
}
|
||||
|
||||
var transportProtocol *mierupb.TransportProtocol
|
||||
switch options.Transport {
|
||||
case "TCP":
|
||||
transportProtocol = mierupb.TransportProtocol_TCP.Enum()
|
||||
case "UDP":
|
||||
transportProtocol = mierupb.TransportProtocol_UDP.Enum()
|
||||
}
|
||||
|
||||
if options.ListenOptions.ListenPort == 0 {
|
||||
return nil, nil, E.New("listen_port must be set")
|
||||
}
|
||||
portBindings := []*mierupb.PortBinding{
|
||||
{
|
||||
Port: proto.Int32(int32(options.ListenOptions.ListenPort)),
|
||||
Protocol: transportProtocol,
|
||||
},
|
||||
}
|
||||
|
||||
var users []*mierupb.User
|
||||
var userNames []string
|
||||
for _, user := range options.Users {
|
||||
users = append(users, &mierupb.User{
|
||||
Name: proto.String(user.Name),
|
||||
Password: proto.String(user.Password),
|
||||
})
|
||||
userNames = append(userNames, user.Name)
|
||||
}
|
||||
var trafficPattern *mierupb.TrafficPattern
|
||||
trafficPattern, _ = mierutp.Decode(options.TrafficPattern)
|
||||
var advancedSettings *mierupb.ServerAdvancedSettings
|
||||
if options.UserHintIsMandatory {
|
||||
advancedSettings = &mierupb.ServerAdvancedSettings{
|
||||
UserHintIsMandatory: proto.Bool(true),
|
||||
}
|
||||
}
|
||||
return &mieruserver.ServerConfig{
|
||||
Config: &mierupb.ServerConfig{
|
||||
PortBindings: portBindings,
|
||||
Users: users,
|
||||
TrafficPattern: trafficPattern,
|
||||
AdvancedSettings: advancedSettings,
|
||||
},
|
||||
}, userNames, nil
|
||||
}
|
||||
|
||||
func validateMieruInboundOptions(options option.MieruInboundOptions) error {
|
||||
if options.Transport != "TCP" && options.Transport != "UDP" {
|
||||
return E.New("transport must be TCP or UDP")
|
||||
}
|
||||
if len(options.Users) == 0 {
|
||||
return E.New("users is empty")
|
||||
}
|
||||
for _, user := range options.Users {
|
||||
if user.Name == "" {
|
||||
return E.New("username is empty")
|
||||
}
|
||||
if user.Password == "" {
|
||||
return E.New("password is empty")
|
||||
}
|
||||
}
|
||||
if options.TrafficPattern != "" {
|
||||
trafficPattern, err := mierutp.Decode(options.TrafficPattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode traffic pattern %q: %w", options.TrafficPattern, err)
|
||||
}
|
||||
if err := mierutp.Validate(trafficPattern); err != nil {
|
||||
return fmt.Errorf("invalid traffic pattern %q: %w", options.TrafficPattern, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
mieruclient "github.com/enfein/mieru/v3/apis/client"
|
||||
mierucommon "github.com/enfein/mieru/v3/apis/common"
|
||||
mierumodel "github.com/enfein/mieru/v3/apis/model"
|
||||
mierutp "github.com/enfein/mieru/v3/apis/trafficpattern"
|
||||
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
@@ -36,7 +37,7 @@ func RegisterOutbound(registry *outbound.Registry) {
|
||||
}
|
||||
|
||||
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.MieruOutboundOptions) (adapter.Outbound, error) {
|
||||
outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
|
||||
outboundDialer, err := dialer.New(ctx, options.DialerOptions, M.IsDomainName(options.Server))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -123,7 +124,15 @@ func (md mieruDialer) DialContext(ctx context.Context, network, address string)
|
||||
return md.dialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
|
||||
var _ mierucommon.Dialer = (*mieruDialer)(nil)
|
||||
func (md mieruDialer) ListenPacket(ctx context.Context, network, laddr, raddr string) (net.PacketConn, error) {
|
||||
addr := M.ParseSocksaddr(raddr)
|
||||
return md.dialer.ListenPacket(ctx, addr)
|
||||
}
|
||||
|
||||
var (
|
||||
_ mierucommon.Dialer = (*mieruDialer)(nil)
|
||||
_ mierucommon.PacketDialer = (*mieruDialer)(nil)
|
||||
)
|
||||
|
||||
// streamer converts a net.PacketConn to a net.Conn.
|
||||
type streamer struct {
|
||||
@@ -161,7 +170,13 @@ func buildMieruClientConfig(options option.MieruOutboundOptions, dialer mieruDia
|
||||
return nil, fmt.Errorf("failed to validate mieru options: %w", err)
|
||||
}
|
||||
|
||||
transportProtocol := mierupb.TransportProtocol_TCP.Enum()
|
||||
var transportProtocol *mierupb.TransportProtocol
|
||||
switch options.Transport {
|
||||
case "TCP":
|
||||
transportProtocol = mierupb.TransportProtocol_TCP.Enum()
|
||||
case "UDP":
|
||||
transportProtocol = mierupb.TransportProtocol_UDP.Enum()
|
||||
}
|
||||
server := &mierupb.ServerEndpoint{}
|
||||
if options.ServerPort != 0 {
|
||||
server.PortBindings = append(server.PortBindings, &mierupb.PortBinding{
|
||||
@@ -189,13 +204,21 @@ func buildMieruClientConfig(options option.MieruOutboundOptions, dialer mieruDia
|
||||
},
|
||||
Servers: []*mierupb.ServerEndpoint{server},
|
||||
},
|
||||
Dialer: dialer,
|
||||
Dialer: dialer,
|
||||
PacketDialer: dialer,
|
||||
DNSConfig: &mierucommon.ClientDNSConfig{
|
||||
BypassDialerDNS: true,
|
||||
},
|
||||
}
|
||||
if multiplexing, ok := mierupb.MultiplexingLevel_value[options.Multiplexing]; ok {
|
||||
config.Profile.Multiplexing = &mierupb.MultiplexingConfig{
|
||||
Level: mierupb.MultiplexingLevel(multiplexing).Enum(),
|
||||
}
|
||||
}
|
||||
if options.TrafficPattern != "" {
|
||||
trafficPattern, _ := mierutp.Decode(options.TrafficPattern)
|
||||
config.Profile.TrafficPattern = trafficPattern
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
@@ -221,8 +244,8 @@ func validateMieruOptions(options option.MieruOutboundOptions) error {
|
||||
return fmt.Errorf("begin port must be less than or equal to end port")
|
||||
}
|
||||
}
|
||||
if options.Transport != "TCP" {
|
||||
return fmt.Errorf("transport must be TCP")
|
||||
if options.Transport != "TCP" && options.Transport != "UDP" {
|
||||
return fmt.Errorf("transport must be TCP or UDP")
|
||||
}
|
||||
if options.UserName == "" {
|
||||
return fmt.Errorf("username is empty")
|
||||
@@ -235,6 +258,15 @@ func validateMieruOptions(options option.MieruOutboundOptions) error {
|
||||
return fmt.Errorf("invalid multiplexing level: %s", options.Multiplexing)
|
||||
}
|
||||
}
|
||||
if options.TrafficPattern != "" {
|
||||
trafficPattern, err := mierutp.Decode(options.TrafficPattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode traffic pattern %q: %w", options.TrafficPattern, err)
|
||||
}
|
||||
if err := mierutp.Validate(trafficPattern); err != nil {
|
||||
return fmt.Errorf("invalid traffic pattern %q: %w", options.TrafficPattern, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,10 @@ func (h *Inbound) Close() error {
|
||||
)
|
||||
}
|
||||
|
||||
func (h *Inbound) UpdateUsers(users []auth.User) {
|
||||
h.authenticator.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
err := h.newConnection(ctx, conn, metadata, onClose)
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
|
||||
@@ -147,6 +147,10 @@ func (n *Inbound) Close() error {
|
||||
)
|
||||
}
|
||||
|
||||
func (n *Inbound) UpdateUsers(users []auth.User) {
|
||||
n.authenticator.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func (n *Inbound) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
ctx := log.ContextWithNewID(request.Context())
|
||||
if request.Method != "CONNECT" {
|
||||
|
||||
158
protocol/openvpn/outbound.go
Normal file
158
protocol/openvpn/outbound.go
Normal file
@@ -0,0 +1,158 @@
|
||||
//go:build with_openvpn
|
||||
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
ovpn "github.com/sagernet/sing-box/transport/openvpn"
|
||||
"github.com/sagernet/sing/common"
|
||||
"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.OpenVPNOutboundOptions](registry, C.TypeOpenVPN, NewOutbound)
|
||||
}
|
||||
|
||||
type Outbound struct {
|
||||
outbound.Adapter
|
||||
ctx context.Context
|
||||
dnsRouter adapter.DNSRouter
|
||||
logger logger.ContextLogger
|
||||
tunnel *ovpn.Tunnel
|
||||
}
|
||||
|
||||
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.OpenVPNOutboundOptions) (adapter.Outbound, error) {
|
||||
tlsConfig, err := tls.NewOpenVPNClient(ctx, logger, common.PtrValueOrDefault(options.TLS))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var tlsKey []byte
|
||||
keyDirection := -1
|
||||
if options.TLSAuth != "" || options.TLSAuthPath != "" {
|
||||
tlsAuth := options.TLSAuth
|
||||
if tlsAuth == "" {
|
||||
data, err := os.ReadFile(options.TLSAuthPath)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read tls_auth_path")
|
||||
}
|
||||
tlsAuth = string(data)
|
||||
}
|
||||
tlsKey = []byte(tlsAuth)
|
||||
keyDirection = options.KeyDirection
|
||||
} else {
|
||||
tlsCrypt := options.TLSCrypt
|
||||
if tlsCrypt == "" && options.TLSCryptPath != "" {
|
||||
data, err := os.ReadFile(options.TLSCryptPath)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read tls_crypt_path")
|
||||
}
|
||||
tlsCrypt = string(data)
|
||||
}
|
||||
tlsKey = []byte(tlsCrypt)
|
||||
}
|
||||
clientConfig := &ovpn.ClientConfig{
|
||||
Proto: options.Proto,
|
||||
Cipher: options.Cipher,
|
||||
Auth: options.Auth,
|
||||
Username: options.Username,
|
||||
Password: options.Password,
|
||||
KeyDirection: keyDirection,
|
||||
TLSCrypt: tlsKey,
|
||||
TLSCryptV2: options.TLSCryptV2,
|
||||
}
|
||||
if err := clientConfig.Prepare(); err != nil {
|
||||
return nil, E.Cause(err, "invalid openvpn config")
|
||||
}
|
||||
outboundDialer, err := dialer.New(ctx, options.DialerOptions, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o := &Outbound{
|
||||
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeOpenVPN, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
|
||||
ctx: ctx,
|
||||
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
|
||||
logger: logger,
|
||||
}
|
||||
tunnel, err := ovpn.NewTunnel(ctx, logger, ovpn.TunnelOptions{
|
||||
Dialer: outboundDialer,
|
||||
Servers: options.Servers,
|
||||
TLSConfig: tlsConfig,
|
||||
Config: clientConfig,
|
||||
ReconnectDelay: time.Duration(options.ReconnectDelay),
|
||||
PingInterval: time.Duration(options.PingInterval),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.tunnel = tunnel
|
||||
return o, nil
|
||||
}
|
||||
|
||||
func (o *Outbound) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStatePostStart {
|
||||
return nil
|
||||
}
|
||||
return o.tunnel.Start()
|
||||
}
|
||||
|
||||
func (o *Outbound) Close() error {
|
||||
if o.tunnel != nil {
|
||||
return o.tunnel.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
switch network {
|
||||
case N.NetworkTCP:
|
||||
o.logger.InfoContext(ctx, "outbound connection to ", destination)
|
||||
case N.NetworkUDP:
|
||||
o.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||||
}
|
||||
if destination.IsDomain() {
|
||||
destinationAddresses, err := o.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return N.DialSerial(ctx, o.tunnel, network, destination, destinationAddresses)
|
||||
}
|
||||
if !destination.Addr.IsValid() {
|
||||
return nil, E.New("invalid destination: ", destination)
|
||||
}
|
||||
return o.tunnel.DialContext(ctx, network, destination)
|
||||
}
|
||||
|
||||
func (o *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
o.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||||
if destination.IsDomain() {
|
||||
destinationAddresses, err := o.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
packetConn, destinationAddress, err := N.ListenSerial(ctx, o.tunnel, destination, destinationAddresses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if destinationAddress.IsValid() && destination != M.SocksaddrFrom(destinationAddress, destination.Port) {
|
||||
return bufio.NewNATPacketConn(bufio.NewPacketConn(packetConn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
||||
}
|
||||
return packetConn, nil
|
||||
}
|
||||
return o.tunnel.ListenPacket(ctx, destination)
|
||||
}
|
||||
@@ -70,6 +70,10 @@ func (h *Inbound) Close() error {
|
||||
return h.listener.Close()
|
||||
}
|
||||
|
||||
func (h *Inbound) UpdateUsers(users []auth.User) {
|
||||
h.authenticator.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
err := socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), h.listener, h.udpTimeout, metadata.Source, onClose)
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
|
||||
179
protocol/sudoku/inbound.go
Normal file
179
protocol/sudoku/inbound.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/transport/sudoku"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func RegisterInbound(registry *inbound.Registry) {
|
||||
inbound.Register[option.SudokuInboundOptions](registry, C.TypeSudoku, NewInbound)
|
||||
}
|
||||
|
||||
type Inbound struct {
|
||||
inbound.Adapter
|
||||
router adapter.ConnectionRouterEx
|
||||
logger logger.ContextLogger
|
||||
listener *listener.Listener
|
||||
protoConf sudoku.ProtocolConfig
|
||||
tunnelSrv *sudoku.HTTPMaskTunnelServer
|
||||
fallback string
|
||||
}
|
||||
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SudokuInboundOptions) (adapter.Inbound, error) {
|
||||
defaultConf := sudoku.DefaultConfig()
|
||||
tableType, err := sudoku.NormalizeTableType(options.TableType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paddingMin, paddingMax := sudoku.ResolvePadding(options.PaddingMin, options.PaddingMax, defaultConf.PaddingMin, defaultConf.PaddingMax)
|
||||
enablePureDownlink := sudoku.DerefBool(options.EnablePureDownlink, defaultConf.EnablePureDownlink)
|
||||
handshakeTimeout := sudoku.DerefInt(options.HandshakeTimeout, defaultConf.HandshakeTimeoutSeconds)
|
||||
|
||||
tables, err := sudoku.NewServerTablesWithCustomPatterns(sudoku.ServerAEADSeed(options.Key), tableType, options.CustomTable, options.CustomTables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
protoConf := sudoku.ProtocolConfig{
|
||||
Key: options.Key,
|
||||
AEADMethod: defaultConf.AEADMethod,
|
||||
PaddingMin: paddingMin,
|
||||
PaddingMax: paddingMax,
|
||||
EnablePureDownlink: enablePureDownlink,
|
||||
HandshakeTimeoutSeconds: handshakeTimeout,
|
||||
DisableHTTPMask: options.DisableHTTPMask,
|
||||
HTTPMaskMode: options.HTTPMaskMode,
|
||||
HTTPMaskPathRoot: strings.TrimSpace(options.PathRoot),
|
||||
}
|
||||
if len(tables) == 1 {
|
||||
protoConf.Table = tables[0]
|
||||
} else {
|
||||
protoConf.Tables = tables
|
||||
}
|
||||
if options.AEADMethod != "" {
|
||||
protoConf.AEADMethod = options.AEADMethod
|
||||
}
|
||||
|
||||
inbound := &Inbound{
|
||||
Adapter: inbound.NewAdapter(C.TypeSudoku, tag),
|
||||
router: router,
|
||||
logger: logger,
|
||||
protoConf: protoConf,
|
||||
fallback: strings.TrimSpace(options.Fallback),
|
||||
}
|
||||
if inbound.fallback != "" {
|
||||
inbound.tunnelSrv = sudoku.NewHTTPMaskTunnelServerWithFallback(&inbound.protoConf)
|
||||
} else {
|
||||
inbound.tunnelSrv = sudoku.NewHTTPMaskTunnelServer(&inbound.protoConf)
|
||||
}
|
||||
|
||||
inbound.listener = listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Network: []string{N.NetworkTCP},
|
||||
Listen: options.ListenOptions,
|
||||
ConnectionHandler: inbound,
|
||||
})
|
||||
return inbound, nil
|
||||
}
|
||||
|
||||
func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
return h.listener.Start()
|
||||
}
|
||||
|
||||
func (h *Inbound) Close() error {
|
||||
return h.listener.Close()
|
||||
}
|
||||
|
||||
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
h.handleConn(ctx, conn, metadata, onClose)
|
||||
}
|
||||
|
||||
func (h *Inbound) handleConn(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
handshakeConn := conn
|
||||
handshakeCfg := &h.protoConf
|
||||
|
||||
if h.tunnelSrv != nil {
|
||||
c, cfg, done, err := h.tunnelSrv.WrapConn(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
if done {
|
||||
return
|
||||
}
|
||||
if c != nil {
|
||||
handshakeConn = c
|
||||
}
|
||||
if cfg != nil {
|
||||
handshakeCfg = cfg
|
||||
}
|
||||
}
|
||||
|
||||
cConn, meta, err := sudoku.ServerHandshake(handshakeConn, handshakeCfg)
|
||||
if err != nil {
|
||||
h.logger.DebugContext(ctx, "handshake failed: ", err)
|
||||
conn.Close()
|
||||
if handshakeConn != conn {
|
||||
handshakeConn.Close()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
session, err := sudoku.ReadServerSession(cConn, meta)
|
||||
if err != nil {
|
||||
h.logger.WarnContext(ctx, "read session failed: ", err)
|
||||
cConn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
switch session.Type {
|
||||
case sudoku.SessionTypeUoT:
|
||||
h.handleUoT(ctx, session.Conn, metadata, onClose)
|
||||
case sudoku.SessionTypeMultiplex:
|
||||
mux, err := sudoku.AcceptMultiplexServer(session.Conn)
|
||||
if err != nil {
|
||||
session.Conn.Close()
|
||||
return
|
||||
}
|
||||
defer mux.Close()
|
||||
for {
|
||||
stream, target, err := mux.AcceptTCP()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go h.routeTCP(ctx, stream, target, metadata)
|
||||
}
|
||||
default:
|
||||
h.routeTCP(ctx, session.Conn, session.Target, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Inbound) routeTCP(ctx context.Context, conn net.Conn, target string, metadata adapter.InboundContext) {
|
||||
destination := M.ParseSocksaddr(target)
|
||||
metadata.Destination = destination
|
||||
h.logger.InfoContext(ctx, "inbound connection to ", destination)
|
||||
h.router.RouteConnectionEx(ctx, conn, metadata, nil)
|
||||
}
|
||||
|
||||
func (h *Inbound) handleUoT(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
h.logger.InfoContext(ctx, "inbound packet connection")
|
||||
packetConn := sudoku.NewUoTPacketConn(conn)
|
||||
h.router.RoutePacketConnectionEx(ctx, bufio.NewPacketConn(packetConn), metadata, onClose)
|
||||
}
|
||||
156
protocol/sudoku/outbound.go
Normal file
156
protocol/sudoku/outbound.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/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/transport/sudoku"
|
||||
"github.com/sagernet/sing/common"
|
||||
"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"
|
||||
)
|
||||
|
||||
func RegisterOutbound(registry *outbound.Registry) {
|
||||
outbound.Register[option.SudokuOutboundOptions](registry, C.TypeSudoku, NewOutbound)
|
||||
}
|
||||
|
||||
type Outbound struct {
|
||||
outbound.Adapter
|
||||
logger logger.ContextLogger
|
||||
client *sudoku.Client
|
||||
tlsConfig tls.Config
|
||||
}
|
||||
|
||||
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SudokuOutboundOptions) (adapter.Outbound, error) {
|
||||
outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defaultConf := sudoku.DefaultConfig()
|
||||
tableType, err := sudoku.NormalizeTableType(options.TableType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paddingMin, paddingMax := sudoku.ResolvePadding(options.PaddingMin, options.PaddingMax, defaultConf.PaddingMin, defaultConf.PaddingMax)
|
||||
enablePureDownlink := sudoku.DerefBool(options.EnablePureDownlink, defaultConf.EnablePureDownlink)
|
||||
|
||||
serverAddr := options.ServerOptions.Build()
|
||||
|
||||
disableHTTPMask := defaultConf.DisableHTTPMask
|
||||
httpMaskMode := defaultConf.HTTPMaskMode
|
||||
var httpMaskHost string
|
||||
var pathRoot string
|
||||
httpMaskMultiplex := defaultConf.HTTPMaskMultiplex
|
||||
|
||||
if hm := options.HTTPMask; hm != nil {
|
||||
disableHTTPMask = !hm.Enabled
|
||||
if hm.Mode != "" {
|
||||
httpMaskMode = hm.Mode
|
||||
}
|
||||
httpMaskHost = hm.Host
|
||||
pathRoot = strings.TrimSpace(hm.PathRoot)
|
||||
if hm.Multiplex != "" {
|
||||
httpMaskMultiplex = hm.Multiplex
|
||||
}
|
||||
}
|
||||
|
||||
baseConf := sudoku.ProtocolConfig{
|
||||
ServerAddress: serverAddr.String(),
|
||||
Key: options.Key,
|
||||
AEADMethod: defaultConf.AEADMethod,
|
||||
PaddingMin: paddingMin,
|
||||
PaddingMax: paddingMax,
|
||||
EnablePureDownlink: enablePureDownlink,
|
||||
HandshakeTimeoutSeconds: defaultConf.HandshakeTimeoutSeconds,
|
||||
DisableHTTPMask: disableHTTPMask,
|
||||
HTTPMaskMode: httpMaskMode,
|
||||
HTTPMaskHost: httpMaskHost,
|
||||
HTTPMaskPathRoot: pathRoot,
|
||||
HTTPMaskMultiplex: httpMaskMultiplex,
|
||||
}
|
||||
if options.AEADMethod != "" {
|
||||
baseConf.AEADMethod = options.AEADMethod
|
||||
}
|
||||
|
||||
tables, err := sudoku.NewClientTablesWithCustomPatterns(sudoku.ClientAEADSeed(options.Key), tableType, options.CustomTable, options.CustomTables)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "build table(s)")
|
||||
}
|
||||
if len(tables) == 1 {
|
||||
baseConf.Table = tables[0]
|
||||
} else {
|
||||
baseConf.Tables = tables
|
||||
}
|
||||
|
||||
var tlsConfig tls.Config
|
||||
if hm := options.HTTPMask; !disableHTTPMask && hm != nil && hm.TLS != nil && hm.TLS.Enabled {
|
||||
tlsConfig, err = tls.NewClientWithOptions(tls.ClientOptions{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
ServerAddress: options.Server,
|
||||
Options: *hm.TLS,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
client := sudoku.NewClient(sudoku.ClientOptions{
|
||||
Dialer: outboundDialer,
|
||||
TLSConfig: tlsConfig,
|
||||
Config: baseConf,
|
||||
})
|
||||
|
||||
return &Outbound{
|
||||
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSudoku, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
|
||||
logger: logger,
|
||||
client: client,
|
||||
tlsConfig: tlsConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
switch N.NetworkName(network) {
|
||||
case N.NetworkTCP:
|
||||
h.logger.InfoContext(ctx, "outbound connection to ", destination)
|
||||
case N.NetworkUDP:
|
||||
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||||
}
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
metadata.Outbound = h.Tag()
|
||||
metadata.Destination = destination
|
||||
return h.client.DialContext(ctx, network, destination)
|
||||
}
|
||||
|
||||
func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
metadata.Outbound = h.Tag()
|
||||
metadata.Destination = destination
|
||||
conn, err := h.client.DialContext(ctx, N.NetworkUDP, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bufio.NewBindPacketConn(sudoku.NewUoTPacketConn(conn), destination), nil
|
||||
}
|
||||
|
||||
func (h *Outbound) Close() error {
|
||||
h.client.Close()
|
||||
return common.Close(h.tlsConfig)
|
||||
}
|
||||
|
||||
func (h *Outbound) InterfaceUpdated() {
|
||||
h.client.Close()
|
||||
}
|
||||
224
protocol/trusttunnel/inbound.go
Normal file
224
protocol/trusttunnel/inbound.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package trusttunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"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/transport/trusttunnel"
|
||||
"github.com/sagernet/sing-quic"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
"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/ntp"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
)
|
||||
|
||||
func RegisterInbound(registry *inbound.Registry) {
|
||||
inbound.Register[option.TrustTunnelInboundOptions](registry, C.TypeTrustTunnel, NewInbound)
|
||||
}
|
||||
|
||||
type Inbound struct {
|
||||
inbound.Adapter
|
||||
ctx context.Context
|
||||
router adapter.Router
|
||||
logger logger.ContextLogger
|
||||
options option.TrustTunnelInboundOptions
|
||||
listener *listener.Listener
|
||||
service *trusttunnel.Service
|
||||
httpServer *http.Server
|
||||
http3Server *http3.Server
|
||||
httpTLSConfig tls.ServerConfig
|
||||
http3TLSConfig tls.ServerConfig
|
||||
network []string
|
||||
}
|
||||
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrustTunnelInboundOptions) (adapter.Inbound, error) {
|
||||
if options.TLS == nil || !options.TLS.Enabled {
|
||||
return nil, C.ErrTLSRequired
|
||||
}
|
||||
networkList := options.Network.Build()
|
||||
if len(networkList) == 0 {
|
||||
networkList = []string{N.NetworkTCP}
|
||||
}
|
||||
inbound := &Inbound{
|
||||
Adapter: inbound.NewAdapter(C.TypeTrustTunnel, tag),
|
||||
ctx: ctx,
|
||||
router: router,
|
||||
logger: logger,
|
||||
options: options,
|
||||
network: networkList,
|
||||
listener: listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Listen: options.ListenOptions,
|
||||
}),
|
||||
}
|
||||
service := trusttunnel.NewService(trusttunnel.ServiceOptions{
|
||||
Ctx: ctx,
|
||||
Logger: logger,
|
||||
Handler: (*inboundHandler)(inbound),
|
||||
})
|
||||
userMap := make(map[string]string, len(options.Users))
|
||||
for _, u := range options.Users {
|
||||
userMap[u.Name] = u.Password
|
||||
}
|
||||
service.UpdateUsers(userMap)
|
||||
inbound.service = service
|
||||
return inbound, nil
|
||||
}
|
||||
|
||||
func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
if common.Contains(h.network, N.NetworkTCP) {
|
||||
listener, err := h.listener.ListenTCP()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.httpServer = &http.Server{
|
||||
Handler: h2c.NewHandler(h.service, &http2.Server{}),
|
||||
BaseContext: func(net.Listener) context.Context {
|
||||
return h.ctx
|
||||
},
|
||||
}
|
||||
h.httpTLSConfig, err = tls.NewServer(h.ctx, h.logger, common.PtrValueOrDefault(h.options.TLS))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(h.httpTLSConfig.NextProtos()) == 0 {
|
||||
h.httpTLSConfig.SetNextProtos([]string{http2.NextProtoTLS})
|
||||
} else if !common.Contains(h.httpTLSConfig.NextProtos(), http2.NextProtoTLS) {
|
||||
h.httpTLSConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, h.httpTLSConfig.NextProtos()...))
|
||||
}
|
||||
err = h.httpTLSConfig.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listener = aTLS.NewListener(listener, h.httpTLSConfig)
|
||||
go func() {
|
||||
sErr := h.httpServer.Serve(listener)
|
||||
if sErr != nil && !errors.Is(sErr, http.ErrServerClosed) {
|
||||
h.logger.Error("HTTP server error: ", sErr)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if common.Contains(h.network, N.NetworkUDP) {
|
||||
h.http3TLSConfig, err = tls.NewServer(h.ctx, h.logger, common.PtrValueOrDefault(h.options.TLS))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := qtls.ConfigureHTTP3(h.http3TLSConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
err = h.http3TLSConfig.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
udpConn, err := h.listener.ListenUDP()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
congestionControlFactory, err := trusttunnel.NewCongestionControl(
|
||||
h.options.CongestionController,
|
||||
h.options.CWND,
|
||||
h.options.BBRProfile,
|
||||
ntp.TimeFuncFromContext(h.ctx),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.http3Server = &http3.Server{
|
||||
Handler: h.service,
|
||||
ConnContext: func(ctx context.Context, conn *quic.Conn) context.Context {
|
||||
conn.SetCongestionControl(congestionControlFactory(conn))
|
||||
return ctx
|
||||
},
|
||||
}
|
||||
quicListener, err := qtls.ListenEarly(udpConn, h.http3TLSConfig, &quic.Config{
|
||||
MaxIdleTimeout: trusttunnel.DefaultSessionTimeout * 2,
|
||||
MaxIncomingStreams: 1 << 60,
|
||||
Allow0RTT: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
_ = h.http3Server.ServeListener(quicListener)
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Inbound) Close() error {
|
||||
return common.Close(
|
||||
h.listener,
|
||||
common.PtrOrNil(h.httpServer),
|
||||
common.PtrOrNil(h.http3Server),
|
||||
h.httpTLSConfig,
|
||||
h.http3TLSConfig,
|
||||
)
|
||||
}
|
||||
|
||||
func (h *Inbound) UpdateUsers(users []option.TrustTunnelUser) {
|
||||
userMap := make(map[string]string, len(users))
|
||||
for _, u := range users {
|
||||
userMap[u.Name] = u.Password
|
||||
}
|
||||
h.service.UpdateUsers(userMap)
|
||||
}
|
||||
|
||||
type inboundHandler Inbound
|
||||
|
||||
func (h *inboundHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
var inboundCtx adapter.InboundContext
|
||||
inboundCtx.Inbound = h.Tag()
|
||||
inboundCtx.InboundType = h.Type()
|
||||
//nolint:staticcheck
|
||||
inboundCtx.InboundDetour = h.listener.ListenOptions().Detour
|
||||
inboundCtx.Source = metadata.Source
|
||||
inboundCtx.Destination = metadata.Destination
|
||||
if userName, _ := auth.UserFromContext[string](ctx); userName != "" {
|
||||
inboundCtx.User = userName
|
||||
h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", inboundCtx.Destination)
|
||||
} else {
|
||||
h.logger.InfoContext(ctx, "inbound connection to ", inboundCtx.Destination)
|
||||
}
|
||||
h.router.RouteConnectionEx(ctx, conn, inboundCtx, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *inboundHandler) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
var inboundCtx adapter.InboundContext
|
||||
inboundCtx.Inbound = h.Tag()
|
||||
inboundCtx.InboundType = h.Type()
|
||||
//nolint:staticcheck
|
||||
inboundCtx.InboundDetour = h.listener.ListenOptions().Detour
|
||||
inboundCtx.Source = metadata.Source
|
||||
inboundCtx.Destination = metadata.Destination
|
||||
if userName, _ := auth.UserFromContext[string](ctx); userName != "" {
|
||||
inboundCtx.User = userName
|
||||
h.logger.InfoContext(ctx, "[", userName, "] inbound packet connection to ", inboundCtx.Destination)
|
||||
} else {
|
||||
h.logger.InfoContext(ctx, "inbound packet connection to ", inboundCtx.Destination)
|
||||
}
|
||||
h.router.RoutePacketConnectionEx(ctx, conn, inboundCtx, nil)
|
||||
return nil
|
||||
}
|
||||
102
protocol/trusttunnel/outbound.go
Normal file
102
protocol/trusttunnel/outbound.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package trusttunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/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/transport/trusttunnel"
|
||||
"github.com/sagernet/sing/common"
|
||||
"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"
|
||||
)
|
||||
|
||||
func RegisterOutbound(registry *outbound.Registry) {
|
||||
outbound.Register[option.TrustTunnelOutboundOptions](registry, C.TypeTrustTunnel, NewOutbound)
|
||||
}
|
||||
|
||||
type Outbound struct {
|
||||
outbound.Adapter
|
||||
logger logger.ContextLogger
|
||||
client trusttunnel.Dialer
|
||||
}
|
||||
|
||||
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrustTunnelOutboundOptions) (adapter.Outbound, error) {
|
||||
if options.TLS == nil || !options.TLS.Enabled {
|
||||
return nil, C.ErrTLSRequired
|
||||
}
|
||||
outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverAddr := options.ServerOptions.Build()
|
||||
networkList := options.Network.Build()
|
||||
tlsConfig, err := tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clientOpts := trusttunnel.ClientOptions{
|
||||
Dialer: outboundDialer,
|
||||
TLSConfig: tlsConfig,
|
||||
Server: serverAddr,
|
||||
Username: options.Username,
|
||||
Password: options.Password,
|
||||
QUIC: options.QUIC,
|
||||
CongestionControl: options.CongestionController,
|
||||
CWND: options.CWND,
|
||||
BBRProfile: options.BBRProfile,
|
||||
HealthCheck: options.HealthCheck,
|
||||
}
|
||||
var client trusttunnel.Dialer
|
||||
if options.Multiplex != nil && options.Multiplex.Enabled {
|
||||
clientOpts.MaxConnections = options.Multiplex.MaxConnections
|
||||
clientOpts.MinStreams = options.Multiplex.MinStreams
|
||||
clientOpts.MaxStreams = options.Multiplex.MaxStreams
|
||||
client, err = trusttunnel.NewMultiplexClient(ctx, clientOpts)
|
||||
} else {
|
||||
client, err = trusttunnel.NewClient(ctx, clientOpts)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Outbound{
|
||||
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeTrustTunnel, tag, networkList, options.DialerOptions),
|
||||
logger: logger,
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
switch N.NetworkName(network) {
|
||||
case N.NetworkTCP:
|
||||
h.logger.InfoContext(ctx, "outbound connection to ", destination)
|
||||
return h.client.Dial(ctx, destination.String())
|
||||
case N.NetworkUDP:
|
||||
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||||
conn, err := h.client.ListenPacket(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bufio.NewBindPacketConn(conn, destination), nil
|
||||
default:
|
||||
return nil, E.New("unsupported network: ", network)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||||
return h.client.ListenPacket(ctx)
|
||||
}
|
||||
|
||||
func (h *Outbound) Close() error {
|
||||
return h.client.Close()
|
||||
}
|
||||
@@ -46,13 +46,14 @@ func NewProviderInline(ctx context.Context, router adapter.Router, logFactory lo
|
||||
outbound = service.FromContext[adapter.OutboundManager](ctx)
|
||||
logger = logFactory.NewLogger(F.ToString("provider/inline", "[", tag, "]"))
|
||||
)
|
||||
provider := &ProviderLocal{
|
||||
p := &ProviderLocal{
|
||||
Adapter: provider.NewAdapter(ctx, router, outbound, logFactory, logger, tag, C.ProviderTypeInline, options.HealthCheck),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
}
|
||||
provider.UpdateOutbounds(nil, options.Outbounds)
|
||||
return provider, nil
|
||||
p.SetRemoveEmojis(options.RemoveEmojis)
|
||||
p.UpdateOutbounds(nil, options.Outbounds)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func NewProviderLocal(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, options option.ProviderLocalOptions) (adapter.Provider, error) {
|
||||
@@ -69,6 +70,7 @@ func NewProviderLocal(ctx context.Context, router adapter.Router, logFactory log
|
||||
logger: logger,
|
||||
provider: service.FromContext[adapter.ProviderManager](ctx),
|
||||
}
|
||||
provider.SetRemoveEmojis(options.RemoveEmojis)
|
||||
filePath := filemanager.BasePath(ctx, options.Path)
|
||||
provider.path, _ = filepath.Abs(filePath)
|
||||
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
||||
|
||||
@@ -83,7 +83,7 @@ func NewProviderRemote(ctx context.Context, router adapter.Router, logFactory lo
|
||||
logger := logFactory.NewLogger(F.ToString("provider/remote", "[", tag, "]"))
|
||||
updateChan := make(chan struct{})
|
||||
close(updateChan)
|
||||
return &ProviderRemote{
|
||||
p := &ProviderRemote{
|
||||
Adapter: provider.NewAdapter(ctx, router, outbound, logFactory, logger, tag, C.ProviderTypeRemote, options.HealthCheck),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
@@ -97,7 +97,9 @@ func NewProviderRemote(ctx context.Context, router adapter.Router, logFactory lo
|
||||
updateInterval: updateInterval,
|
||||
exclude: (*regexp.Regexp)(options.Exclude),
|
||||
include: (*regexp.Regexp)(options.Include),
|
||||
}, nil
|
||||
}
|
||||
p.SetRemoveEmojis(options.RemoveEmojis)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (s *ProviderRemote) Start() error {
|
||||
|
||||
@@ -1 +1 @@
|
||||
with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_naive_outbound,badlinkname,tfogo_checklinkname0
|
||||
with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_masque,with_mtproxy,with_ccm,with_ocm,with_openvpn,with_trusttunnel,with_sudoku,with_naive_outbound,badlinkname,tfogo_checklinkname0
|
||||
@@ -1 +1 @@
|
||||
with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_manager,with_admin_panel,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0
|
||||
with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_manager,with_admin_panel,with_masque,with_mtproxy,with_ccm,with_ocm,with_openvpn,with_trusttunnel,with_sudoku,badlinkname,tfogo_checklinkname0
|
||||
|
||||
@@ -1 +1 @@
|
||||
with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_naive_outbound,with_purego,badlinkname,tfogo_checklinkname0
|
||||
with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_masque,with_mtproxy,with_ccm,with_ocm,with_openvpn,with_trusttunnel,with_sudoku,with_naive_outbound,with_purego,badlinkname,tfogo_checklinkname0
|
||||
@@ -279,7 +279,7 @@ func (m *ConnectionManager) connectionCopy(ctx context.Context, source net.Conn,
|
||||
if !direction {
|
||||
if err == nil {
|
||||
m.logger.DebugContext(ctx, "connection upload finished")
|
||||
} else if !E.IsClosedOrCanceled(err) && !strings.Contains(err.Error(), "NO_ERROR") {
|
||||
} else if !E.IsClosedOrCanceled(err) && !strings.Contains(err.Error(), "NO_ERROR") && !strings.Contains(err.Error(), "CANCEL") && !strings.Contains(err.Error(), "body closed") {
|
||||
m.logger.ErrorContext(ctx, "connection upload closed: ", err)
|
||||
} else {
|
||||
m.logger.TraceContext(ctx, "connection upload closed")
|
||||
|
||||
@@ -34,10 +34,16 @@ export interface NodeUpdate {
|
||||
export type NodeStatus = "online" | "offline";
|
||||
|
||||
export type UserType =
|
||||
| "anytls"
|
||||
| "http"
|
||||
| "hysteria"
|
||||
| "hysteria2"
|
||||
| "mixed"
|
||||
| "mtproxy"
|
||||
| "naive"
|
||||
| "socks"
|
||||
| "trojan"
|
||||
| "trusttunnel"
|
||||
| "tuic"
|
||||
| "vless"
|
||||
| "vmess";
|
||||
|
||||
@@ -44,10 +44,11 @@ const CONN_TYPES: { value: ConnectionType; label: string }[] = [
|
||||
];
|
||||
const FLOW_KEYS: { value: string; label: string }[] = [
|
||||
{ value: "user", label: "User" },
|
||||
{ value: "destination", label: "Destination" },
|
||||
{ value: "source_ip", label: "Source IP" },
|
||||
{ value: "hwid", label: "HWID" },
|
||||
{ value: "mux", label: "Mux" },
|
||||
{ value: "source_ip", label: "Source IP" },
|
||||
{ value: "protocol", label: "Protocol" },
|
||||
{ value: "destination", label: "Destination" },
|
||||
];
|
||||
|
||||
export function BandwidthLimitersPage() {
|
||||
|
||||
@@ -17,10 +17,16 @@ import {
|
||||
|
||||
// Display labels mirror service/admin_panel/tables/user.go.
|
||||
const USER_TYPES: { value: UserType; label: string }[] = [
|
||||
{ value: "anytls", label: "AnyTLS" },
|
||||
{ value: "http", label: "HTTP" },
|
||||
{ value: "hysteria", label: "Hysteria" },
|
||||
{ value: "hysteria2", label: "Hysteria2" },
|
||||
{ value: "mixed", label: "Mixed" },
|
||||
{ value: "mtproxy", label: "MTProxy" },
|
||||
{ value: "naive", label: "Naive" },
|
||||
{ value: "socks", label: "SOCKS" },
|
||||
{ value: "trojan", label: "Trojan" },
|
||||
{ value: "trusttunnel", label: "TrustTunnel" },
|
||||
{ value: "tuic", label: "TUIC" },
|
||||
{ value: "vless", label: "VLESS" },
|
||||
{ value: "vmess", label: "VMess" },
|
||||
@@ -38,7 +44,7 @@ const FLOW_OPTIONS: { value: string; label: string }[] = [
|
||||
// same rule up-front (required fields invisible for the current type
|
||||
// are filtered out before validateRequired runs).
|
||||
const SHOW_UUID = new Set<UserType>(["vless", "vmess", "tuic"]);
|
||||
const SHOW_PASSWORD = new Set<UserType>(["hysteria", "hysteria2", "trojan", "tuic"]);
|
||||
const SHOW_PASSWORD = new Set<UserType>(["anytls", "http", "hysteria", "hysteria2", "mixed", "naive", "socks", "trojan", "trusttunnel", "tuic"]);
|
||||
const SHOW_SECRET = new Set<UserType>(["mtproxy"]);
|
||||
const SHOW_FLOW = new Set<UserType>(["vless"]);
|
||||
const SHOW_ALTER_ID = new Set<UserType>(["vmess"]);
|
||||
|
||||
@@ -67,7 +67,7 @@ type UserCreate struct {
|
||||
SquadIDs []int `json:"squad_ids" validate:"required,min=1"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Inbound string `json:"inbound" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=hysteria hysteria2 mtproxy trojan tuic vless vmess"`
|
||||
Type string `json:"type" validate:"required,oneof=anytls http hysteria hysteria2 mixed mtproxy naive socks trojan trusttunnel tuic vless vmess"`
|
||||
UUID string `json:"uuid" validate:"omitempty,uuid4"`
|
||||
Password string `json:"password" validate:"omitempty"`
|
||||
Secret string `json:"secret" validate:"omitempty"`
|
||||
@@ -140,7 +140,7 @@ type BandwidthLimiter struct {
|
||||
Strategy string `json:"strategy" validate:"required"`
|
||||
ConnectionType string `json:"connection_type" validate:"omitempty"`
|
||||
Mode string `json:"mode" validate:"required"`
|
||||
FlowKeys []string `json:"flow_keys" validate:"omitempty,dive,oneof=user destination ip hwid mux"`
|
||||
FlowKeys []string `json:"flow_keys" validate:"omitempty,dive,oneof=user source_ip hwid mux protocol destination"`
|
||||
Speed string `json:"speed" validate:"required"`
|
||||
RawSpeed uint64 `json:"raw_speed" validate:"required"`
|
||||
CreatedAt time.Time `json:"created_at" validate:"required"`
|
||||
@@ -154,7 +154,7 @@ type BandwidthLimiterCreate struct {
|
||||
Strategy string `json:"strategy" validate:"required,oneof=global connection bypass"`
|
||||
ConnectionType string `json:"connection_type" validate:"excluded_if=Strategy bypass,omitempty"`
|
||||
Mode string `json:"mode" validate:"excluded_if=Strategy bypass,required_unless=Strategy bypass"`
|
||||
FlowKeys []string `json:"flow_keys" validate:"excluded_if=Strategy bypass,omitempty,dive,oneof=user destination ip hwid mux"`
|
||||
FlowKeys []string `json:"flow_keys" validate:"excluded_if=Strategy bypass,omitempty,dive,oneof=user source_ip hwid mux protocol destination"`
|
||||
Speed string `json:"speed" validate:"excluded_if=Strategy bypass,required_unless=Strategy bypass"`
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ type BandwidthLimiterUpdate struct {
|
||||
Strategy string `json:"strategy" validate:"required,oneof=global connection bypass"`
|
||||
ConnectionType string `json:"connection_type" validate:"excluded_if=Strategy bypass,omitempty"`
|
||||
Mode string `json:"mode" validate:"excluded_if=Strategy bypass,required_unless=Strategy bypass"`
|
||||
FlowKeys []string `json:"flow_keys" validate:"excluded_if=Strategy bypass,omitempty,dive,oneof=user destination ip hwid mux"`
|
||||
FlowKeys []string `json:"flow_keys" validate:"excluded_if=Strategy bypass,omitempty,dive,oneof=user source_ip hwid mux protocol destination"`
|
||||
Speed string `json:"speed" validate:"excluded_if=Strategy bypass,required_unless=Strategy bypass"`
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ type BaseBandwidthLimiter struct {
|
||||
Strategy string `json:"strategy" validate:"required,oneof=global connection bypass"`
|
||||
ConnectionType string `json:"connection_type" validate:"excluded_if=Strategy bypass,omitempty"`
|
||||
Mode string `json:"mode" validate:"excluded_if=Strategy bypass,required_unless=Strategy bypass"`
|
||||
FlowKeys []string `json:"flow_keys" validate:"excluded_if=Strategy bypass,omitempty,dive,oneof=user destination ip hwid mux"`
|
||||
FlowKeys []string `json:"flow_keys" validate:"excluded_if=Strategy bypass,omitempty,dive,oneof=user source_ip hwid mux protocol destination"`
|
||||
Speed string `json:"speed" validate:"excluded_if=Strategy bypass,required_unless=Strategy bypass"`
|
||||
RawSpeed uint64 `json:"raw_speed" validate:"excluded_if=Strategy bypass,required_unless=Strategy bypass"`
|
||||
}
|
||||
@@ -261,3 +261,5 @@ type BaseRateLimiter struct {
|
||||
Count uint32 `json:"count" validate:"excluded_if=Strategy bypass,required_unless=Strategy bypass"`
|
||||
Interval string `json:"interval" validate:"excluded_if=Strategy bypass,required_unless=Strategy bypass"`
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -51,3 +51,5 @@ type Repository interface {
|
||||
UpdateRateLimiter(id int, limiter RateLimiterUpdate) (RateLimiter, error)
|
||||
DeleteRateLimiter(id int) (RateLimiter, error)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ package postgresql
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"github.com/sagernet/sing-box/common/byteformats"
|
||||
"github.com/sagernet/sing/common/byteformats"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type Filter func(sb *sqlbuilder.SelectBuilder, value []string) error
|
||||
@@ -84,12 +86,13 @@ func SpeedLessEqualThanFilter(field string) Filter {
|
||||
}
|
||||
}
|
||||
|
||||
func ExistsAndWhereInFilter(subquery *sqlbuilder.SelectBuilder, field string) Filter {
|
||||
func ExistsAndWhereInFilter(subqueryFactory func() *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 := subqueryFactory()
|
||||
subquery.Where(subquery.In(field, values...))
|
||||
sb.Where(sb.Exists(subquery))
|
||||
return nil
|
||||
@@ -110,38 +113,54 @@ func InFilter(field string) Filter {
|
||||
}
|
||||
}
|
||||
|
||||
func SortAscFilter() Filter {
|
||||
func SortAscFilter(columns []string) 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])
|
||||
column, err := isValidSortColumn(value[0], columns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sb.OrderByAsc(column)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func ReplacedSortDescFilter(replace map[string]string) Filter {
|
||||
func SortDescFilter(columns []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])
|
||||
column, err := isValidSortColumn(value[0], columns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sb.OrderByDesc(column)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func ReplacedSortAscFilter(replace map[string]string, columns []string) Filter {
|
||||
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
|
||||
column, ok := replace[value[0]]
|
||||
if !ok {
|
||||
column = value[0]
|
||||
}
|
||||
column, err := isValidSortColumn(column, columns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sb.OrderByAsc(column)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func ReplacedSortDescFilter(replace map[string]string, columns []string) Filter {
|
||||
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
|
||||
column, ok := replace[value[0]]
|
||||
if !ok {
|
||||
column = value[0]
|
||||
}
|
||||
column, err := isValidSortColumn(column, columns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sb.OrderByDesc(column)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -167,3 +186,10 @@ func OffsetFilter() Filter {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func isValidSortColumn(column string, columns []string) (string, error) {
|
||||
if slices.Contains(columns, column) {
|
||||
return column, nil
|
||||
}
|
||||
return "", E.New("invalid sort column \"", column, "\"")
|
||||
}
|
||||
|
||||
@@ -4,14 +4,15 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/sagernet/sing-box/common/byteformats"
|
||||
"github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing/common/byteformats"
|
||||
)
|
||||
|
||||
var squadFilters, nodeFilters, userFilters, bandwidthLimiterFilters, connectionLimiterFilters, trafficLimiterFilters, rateLimiterFilters map[string]Filter
|
||||
@@ -37,6 +38,13 @@ func NewPostgreSQLRepository(ctx context.Context, dsn string) (*PostgreSQLReposi
|
||||
return &PostgreSQLRepository{db: pool, ctx: ctx}, nil
|
||||
}
|
||||
|
||||
func notFoundErr(err error) error {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return constant.ErrNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *PostgreSQLRepository) CreateSquad(squad constant.SquadCreate) (constant.Squad, error) {
|
||||
var s constant.Squad
|
||||
now := time.Now()
|
||||
@@ -138,7 +146,7 @@ func (r *PostgreSQLRepository) GetSquad(id int) (constant.Squad, error) {
|
||||
&s.CreatedAt,
|
||||
&s.UpdatedAt,
|
||||
)
|
||||
return s, err
|
||||
return s, notFoundErr(err)
|
||||
}
|
||||
|
||||
func (r *PostgreSQLRepository) UpdateSquad(id int, squad constant.SquadUpdate) (constant.Squad, error) {
|
||||
@@ -450,10 +458,7 @@ func (r *PostgreSQLRepository) GetNode(uuid string) (constant.Node, error) {
|
||||
&n.CreatedAt,
|
||||
&n.UpdatedAt,
|
||||
)
|
||||
if err != nil && err.Error() == "no rows in result set" {
|
||||
return n, constant.ErrNotFound
|
||||
}
|
||||
return n, err
|
||||
return n, notFoundErr(err)
|
||||
}
|
||||
|
||||
func (r *PostgreSQLRepository) UpdateNode(uuid string, node constant.NodeUpdate) (constant.Node, error) {
|
||||
@@ -696,7 +701,7 @@ func (r *PostgreSQLRepository) GetUser(id int) (constant.User, error) {
|
||||
&u.CreatedAt,
|
||||
&u.UpdatedAt,
|
||||
)
|
||||
return u, err
|
||||
return u, notFoundErr(err)
|
||||
}
|
||||
|
||||
func (r *PostgreSQLRepository) UpdateUser(id int, user constant.UserUpdate) (constant.User, error) {
|
||||
@@ -974,7 +979,7 @@ func (r *PostgreSQLRepository) GetConnectionLimiter(id int) (constant.Connection
|
||||
&cl.CreatedAt,
|
||||
&cl.UpdatedAt,
|
||||
)
|
||||
return cl, err
|
||||
return cl, notFoundErr(err)
|
||||
}
|
||||
|
||||
func (r *PostgreSQLRepository) UpdateConnectionLimiter(id int, limiter constant.ConnectionLimiterUpdate) (constant.ConnectionLimiter, error) {
|
||||
@@ -1275,7 +1280,7 @@ func (r *PostgreSQLRepository) GetBandwidthLimiter(id int) (constant.BandwidthLi
|
||||
&bl.UpdatedAt,
|
||||
)
|
||||
bl.FlowKeys = []string(flowKeys)
|
||||
return bl, err
|
||||
return bl, notFoundErr(err)
|
||||
}
|
||||
|
||||
func (r *PostgreSQLRepository) UpdateBandwidthLimiter(id int, limiter constant.BandwidthLimiterUpdate) (constant.BandwidthLimiter, error) {
|
||||
@@ -1594,7 +1599,7 @@ func (r *PostgreSQLRepository) GetTrafficLimiter(id int) (constant.TrafficLimite
|
||||
&tl.CreatedAt,
|
||||
&tl.UpdatedAt,
|
||||
)
|
||||
return tl, err
|
||||
return tl, notFoundErr(err)
|
||||
}
|
||||
|
||||
func (r *PostgreSQLRepository) UpdateTrafficLimiter(id int, limiter constant.TrafficLimiterUpdate) (constant.TrafficLimiter, error) {
|
||||
@@ -1928,7 +1933,7 @@ func (r *PostgreSQLRepository) GetRateLimiter(id int) (constant.RateLimiter, err
|
||||
&rl.CreatedAt,
|
||||
&rl.UpdatedAt,
|
||||
)
|
||||
return rl, err
|
||||
return rl, notFoundErr(err)
|
||||
}
|
||||
|
||||
func (r *PostgreSQLRepository) UpdateRateLimiter(id int, limiter constant.RateLimiterUpdate) (constant.RateLimiter, error) {
|
||||
@@ -2029,8 +2034,8 @@ func init() {
|
||||
"created_at_end": LessThanFilter("created_at"),
|
||||
"updated_at_start": GreaterThanFilter("updated_at"),
|
||||
"updated_at_end": LessThanFilter("updated_at"),
|
||||
"sort_asc": SortAscFilter(),
|
||||
"sort_desc": SortDescFilter(),
|
||||
"sort_asc": SortAscFilter([]string{"id", "name", "created_at", "updated_at"}),
|
||||
"sort_desc": SortDescFilter([]string{"id", "name", "created_at", "updated_at"}),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
}
|
||||
@@ -2038,8 +2043,8 @@ func init() {
|
||||
"uuid": EqualFilter("uuid"),
|
||||
"pk": EqualFilter("uuid"),
|
||||
"name": EqualFilter("name"),
|
||||
"squad_id_in": ExistsAndWhereInFilter(
|
||||
sqlbuilder.PostgreSQL.NewSelectBuilder().
|
||||
"squad_id_in": ExistsAndWhereInFilter(func() *sqlbuilder.SelectBuilder {
|
||||
return sqlbuilder.PostgreSQL.NewSelectBuilder().
|
||||
Select(
|
||||
"squad_id",
|
||||
).
|
||||
@@ -2048,23 +2053,22 @@ func init() {
|
||||
).
|
||||
From(
|
||||
"node_to_squad",
|
||||
),
|
||||
"node_to_squad.squad_id",
|
||||
),
|
||||
)
|
||||
}, "node_to_squad.squad_id"),
|
||||
"created_at_start": GreaterThanFilter("created_at"),
|
||||
"created_at_end": LessThanFilter("created_at"),
|
||||
"updated_at_start": GreaterThanFilter("updated_at"),
|
||||
"updated_at_end": LessThanFilter("updated_at"),
|
||||
"sort_asc": SortAscFilter(),
|
||||
"sort_desc": SortDescFilter(),
|
||||
"sort_asc": SortAscFilter([]string{"uuid", "name", "created_at", "updated_at"}),
|
||||
"sort_desc": SortDescFilter([]string{"uuid", "name", "created_at", "updated_at"}),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
}
|
||||
userFilters = map[string]Filter{
|
||||
"id": EqualFilter("id"),
|
||||
"pk": EqualFilter("id"),
|
||||
"squad_id_in": ExistsAndWhereInFilter(
|
||||
sqlbuilder.PostgreSQL.NewSelectBuilder().
|
||||
"squad_id_in": ExistsAndWhereInFilter(func() *sqlbuilder.SelectBuilder {
|
||||
return sqlbuilder.PostgreSQL.NewSelectBuilder().
|
||||
Select(
|
||||
"squad_id",
|
||||
).
|
||||
@@ -2073,26 +2077,24 @@ func init() {
|
||||
).
|
||||
From(
|
||||
"user_to_squad",
|
||||
),
|
||||
"user_to_squad.squad_id",
|
||||
),
|
||||
)
|
||||
}, "user_to_squad.squad_id"),
|
||||
"username": EqualFilter("username"),
|
||||
"inbound": EqualFilter("inbound"),
|
||||
"type": EqualFilter("type"),
|
||||
"created_at_start": GreaterThanFilter("created_at"),
|
||||
"created_at_end": LessThanFilter("created_at"),
|
||||
"updated_at_start": GreaterThanFilter("updated_at"),
|
||||
"updated_at_end": LessThanFilter("updated_at"),
|
||||
"sort_asc": SortAscFilter(),
|
||||
"sort_desc": SortDescFilter(),
|
||||
"sort_asc": SortAscFilter([]string{"id", "username", "inbound", "type", "created_at", "updated_at"}),
|
||||
"sort_desc": SortDescFilter([]string{"id", "username", "inbound", "type", "created_at", "updated_at"}),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
}
|
||||
connectionLimiterFilters = map[string]Filter{
|
||||
"id": EqualFilter("id"),
|
||||
"pk": EqualFilter("id"),
|
||||
"squad_id_in": ExistsAndWhereInFilter(
|
||||
sqlbuilder.PostgreSQL.NewSelectBuilder().
|
||||
"squad_id_in": ExistsAndWhereInFilter(func() *sqlbuilder.SelectBuilder {
|
||||
return sqlbuilder.PostgreSQL.NewSelectBuilder().
|
||||
Select(
|
||||
"squad_id",
|
||||
).
|
||||
@@ -2101,9 +2103,8 @@ func init() {
|
||||
).
|
||||
From(
|
||||
"connection_limiter_to_squad",
|
||||
),
|
||||
"connection_limiter_to_squad.squad_id",
|
||||
),
|
||||
)
|
||||
}, "connection_limiter_to_squad.squad_id"),
|
||||
"strategy": EqualFilter("strategy"),
|
||||
"username": EqualFilter("username"),
|
||||
"outbound": EqualFilter("outbound"),
|
||||
@@ -2113,16 +2114,16 @@ func init() {
|
||||
"created_at_end": LessThanFilter("created_at"),
|
||||
"updated_at_start": GreaterThanFilter("updated_at"),
|
||||
"updated_at_end": LessThanFilter("updated_at"),
|
||||
"sort_asc": SortAscFilter(),
|
||||
"sort_desc": SortDescFilter(),
|
||||
"sort_asc": SortAscFilter([]string{"id", "username", "outbound", "strategy", "connection_type", "lock_type", "count", "created_at", "updated_at"}),
|
||||
"sort_desc": SortDescFilter([]string{"id", "username", "outbound", "strategy", "connection_type", "lock_type", "count", "created_at", "updated_at"}),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
}
|
||||
bandwidthLimiterFilters = map[string]Filter{
|
||||
"id": EqualFilter("id"),
|
||||
"pk": EqualFilter("id"),
|
||||
"squad_id_in": ExistsAndWhereInFilter(
|
||||
sqlbuilder.PostgreSQL.NewSelectBuilder().
|
||||
"squad_id_in": ExistsAndWhereInFilter(func() *sqlbuilder.SelectBuilder {
|
||||
return sqlbuilder.PostgreSQL.NewSelectBuilder().
|
||||
Select(
|
||||
"squad_id",
|
||||
).
|
||||
@@ -2131,31 +2132,31 @@ func init() {
|
||||
).
|
||||
From(
|
||||
"bandwidth_limiter_to_squad",
|
||||
),
|
||||
"bandwidth_limiter_to_squad.squad_id",
|
||||
),
|
||||
)
|
||||
}, "bandwidth_limiter_to_squad.squad_id"),
|
||||
"strategy": EqualFilter("strategy"),
|
||||
"mode": EqualFilter("mode"),
|
||||
"type": EqualFilter("type"),
|
||||
"username": EqualFilter("username"),
|
||||
"down_start": SpeedGreaterEqualThanFilter("raw_down"),
|
||||
"down_end": SpeedLessEqualThanFilter("raw_down"),
|
||||
"up_start": SpeedGreaterEqualThanFilter("raw_up"),
|
||||
"up_end": SpeedLessEqualThanFilter("raw_up"),
|
||||
"created_at_start": GreaterThanFilter("created_at"),
|
||||
"created_at_end": LessThanFilter("created_at"),
|
||||
"updated_at_start": GreaterThanFilter("updated_at"),
|
||||
"updated_at_end": LessThanFilter("updated_at"),
|
||||
"sort_asc": ReplacedSortAscFilter(map[string]string{"down": "raw_down", "up": "raw_up"}),
|
||||
"sort_desc": ReplacedSortDescFilter(map[string]string{"down": "raw_down", "up": "raw_up"}),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
"sort_asc": ReplacedSortAscFilter(
|
||||
map[string]string{"speed": "raw_speed"},
|
||||
[]string{"id", "username", "outbound", "strategy", "mode", "raw_speed", "created_at", "updated_at"},
|
||||
),
|
||||
"sort_desc": ReplacedSortDescFilter(
|
||||
map[string]string{"speed": "raw_speed"},
|
||||
[]string{"id", "username", "outbound", "strategy", "mode", "raw_speed", "created_at", "updated_at"},
|
||||
),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
}
|
||||
trafficLimiterFilters = map[string]Filter{
|
||||
"id": EqualFilter("id"),
|
||||
"pk": EqualFilter("id"),
|
||||
"squad_id_in": ExistsAndWhereInFilter(
|
||||
sqlbuilder.PostgreSQL.NewSelectBuilder().
|
||||
"squad_id_in": ExistsAndWhereInFilter(func() *sqlbuilder.SelectBuilder {
|
||||
return sqlbuilder.PostgreSQL.NewSelectBuilder().
|
||||
Select(
|
||||
"squad_id",
|
||||
).
|
||||
@@ -2164,31 +2165,36 @@ func init() {
|
||||
).
|
||||
From(
|
||||
"traffic_limiter_to_squad",
|
||||
),
|
||||
"traffic_limiter_to_squad.squad_id",
|
||||
),
|
||||
)
|
||||
}, "traffic_limiter_to_squad.squad_id"),
|
||||
"username": EqualFilter("username"),
|
||||
"outbound": EqualFilter("outbound"),
|
||||
"strategy": EqualFilter("strategy"),
|
||||
"mode": EqualFilter("mode"),
|
||||
"used_start": SpeedGreaterEqualThanFilter("raw_used"),
|
||||
"used_end": SpeedLessEqualThanFilter("raw_used"),
|
||||
"used_start": SpeedGreaterEqualThanFilter("raw_used"),
|
||||
"used_end": SpeedLessEqualThanFilter("raw_used"),
|
||||
"quota_start": SpeedGreaterEqualThanFilter("raw_quota"),
|
||||
"quota_end": SpeedLessEqualThanFilter("raw_quota"),
|
||||
"created_at_start": GreaterThanFilter("created_at"),
|
||||
"created_at_end": LessThanFilter("created_at"),
|
||||
"updated_at_start": GreaterThanFilter("updated_at"),
|
||||
"updated_at_end": LessThanFilter("updated_at"),
|
||||
"sort_asc": ReplacedSortAscFilter(map[string]string{"used": "raw_used", "quota": "raw_quota"}),
|
||||
"sort_desc": ReplacedSortDescFilter(map[string]string{"used": "raw_used", "quota": "raw_quota"}),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
"sort_asc": ReplacedSortAscFilter(
|
||||
map[string]string{"used": "raw_used", "quota": "raw_quota"},
|
||||
[]string{"id", "username", "outbound", "strategy", "mode", "raw_used", "raw_quota", "created_at", "updated_at"},
|
||||
),
|
||||
"sort_desc": ReplacedSortDescFilter(
|
||||
map[string]string{"used": "raw_used", "quota": "raw_quota"},
|
||||
[]string{"id", "username", "outbound", "strategy", "mode", "raw_used", "raw_quota", "created_at", "updated_at"},
|
||||
),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
}
|
||||
rateLimiterFilters = map[string]Filter{
|
||||
"id": EqualFilter("id"),
|
||||
"pk": EqualFilter("id"),
|
||||
"squad_id_in": ExistsAndWhereInFilter(
|
||||
sqlbuilder.PostgreSQL.NewSelectBuilder().
|
||||
"squad_id_in": ExistsAndWhereInFilter(func() *sqlbuilder.SelectBuilder {
|
||||
return sqlbuilder.PostgreSQL.NewSelectBuilder().
|
||||
Select(
|
||||
"squad_id",
|
||||
).
|
||||
@@ -2197,9 +2203,8 @@ func init() {
|
||||
).
|
||||
From(
|
||||
"rate_limiter_to_squad",
|
||||
),
|
||||
"rate_limiter_to_squad.squad_id",
|
||||
),
|
||||
)
|
||||
}, "rate_limiter_to_squad.squad_id"),
|
||||
"strategy": EqualFilter("strategy"),
|
||||
"username": EqualFilter("username"),
|
||||
"outbound": EqualFilter("outbound"),
|
||||
@@ -2211,8 +2216,8 @@ func init() {
|
||||
"created_at_end": LessThanFilter("created_at"),
|
||||
"updated_at_start": GreaterThanFilter("updated_at"),
|
||||
"updated_at_end": LessThanFilter("updated_at"),
|
||||
"sort_asc": SortAscFilter(),
|
||||
"sort_desc": SortDescFilter(),
|
||||
"sort_asc": SortAscFilter([]string{"id", "username", "outbound", "strategy", "connection_type", "count", "interval", "created_at", "updated_at"}),
|
||||
"sort_desc": SortDescFilter([]string{"id", "username", "outbound", "strategy", "connection_type", "count", "interval", "created_at", "updated_at"}),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package sqlite
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"github.com/sagernet/sing-box/common/byteformats"
|
||||
"github.com/sagernet/sing/common/byteformats"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type Filter func(sb *sqlbuilder.SelectBuilder, value []string) error
|
||||
@@ -84,12 +86,13 @@ func SpeedLessEqualThanFilter(field string) Filter {
|
||||
}
|
||||
}
|
||||
|
||||
func ExistsAndWhereInFilter(subquery *sqlbuilder.SelectBuilder, field string) Filter {
|
||||
func ExistsAndWhereInFilter(subqueryFactory func() *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 := subqueryFactory()
|
||||
subquery.Where(subquery.In(field, values...))
|
||||
sb.Where(sb.Exists(subquery))
|
||||
return nil
|
||||
@@ -110,38 +113,54 @@ func InFilter(field string) Filter {
|
||||
}
|
||||
}
|
||||
|
||||
func SortAscFilter() Filter {
|
||||
func SortAscFilter(columns []string) 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])
|
||||
column, err := isValidSortColumn(value[0], columns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sb.OrderByAsc(column)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func ReplacedSortDescFilter(replace map[string]string) Filter {
|
||||
func SortDescFilter(columns []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])
|
||||
column, err := isValidSortColumn(value[0], columns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sb.OrderByDesc(column)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func ReplacedSortAscFilter(replace map[string]string, columns []string) Filter {
|
||||
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
|
||||
column, ok := replace[value[0]]
|
||||
if !ok {
|
||||
column = value[0]
|
||||
}
|
||||
column, err := isValidSortColumn(column, columns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sb.OrderByAsc(column)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func ReplacedSortDescFilter(replace map[string]string, columns []string) Filter {
|
||||
return func(sb *sqlbuilder.SelectBuilder, value []string) error {
|
||||
column, ok := replace[value[0]]
|
||||
if !ok {
|
||||
column = value[0]
|
||||
}
|
||||
column, err := isValidSortColumn(column, columns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sb.OrderByDesc(column)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -167,3 +186,10 @@ func OffsetFilter() Filter {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func isValidSortColumn(column string, columns []string) (string, error) {
|
||||
if slices.Contains(columns, column) {
|
||||
return column, nil
|
||||
}
|
||||
return "", E.New("invalid sort column \"", column, "\"")
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"github.com/sagernet/sing-box/common/byteformats"
|
||||
"github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing/common/byteformats"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
@@ -33,6 +33,13 @@ func NewSQLiteRepository(ctx context.Context, dsn string) (*SQLiteRepository, er
|
||||
return &SQLiteRepository{db: db, ctx: ctx}, nil
|
||||
}
|
||||
|
||||
func notFoundErr(err error) error {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return constant.ErrNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *SQLiteRepository) CreateSquad(squad constant.SquadCreate) (constant.Squad, error) {
|
||||
var s constant.Squad
|
||||
now := time.Now()
|
||||
@@ -134,7 +141,7 @@ func (r *SQLiteRepository) GetSquad(id int) (constant.Squad, error) {
|
||||
&s.CreatedAt,
|
||||
&s.UpdatedAt,
|
||||
)
|
||||
return s, err
|
||||
return s, notFoundErr(err)
|
||||
}
|
||||
|
||||
func (r *SQLiteRepository) UpdateSquad(id int, squad constant.SquadUpdate) (constant.Squad, error) {
|
||||
@@ -446,13 +453,9 @@ func (r *SQLiteRepository) GetNode(uuid string) (constant.Node, error) {
|
||||
&n.CreatedAt,
|
||||
&n.UpdatedAt,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return n, constant.ErrNotFound
|
||||
}
|
||||
n.SquadIDs = []int(squadIDs)
|
||||
return n, err
|
||||
return n, notFoundErr(err)
|
||||
}
|
||||
|
||||
func (r *SQLiteRepository) UpdateNode(uuid string, node constant.NodeUpdate) (constant.Node, error) {
|
||||
var n constant.Node
|
||||
err := r.db.QueryRowContext(
|
||||
@@ -693,7 +696,7 @@ func (r *SQLiteRepository) GetUser(id int) (constant.User, error) {
|
||||
&u.UpdatedAt,
|
||||
)
|
||||
u.SquadIDs = []int(squadIDs)
|
||||
return u, err
|
||||
return u, notFoundErr(err)
|
||||
}
|
||||
|
||||
func (r *SQLiteRepository) UpdateUser(id int, user constant.UserUpdate) (constant.User, error) {
|
||||
@@ -975,7 +978,7 @@ func (r *SQLiteRepository) GetConnectionLimiter(id int) (constant.ConnectionLimi
|
||||
&cl.UpdatedAt,
|
||||
)
|
||||
cl.SquadIDs = []int(squadIDs)
|
||||
return cl, err
|
||||
return cl, notFoundErr(err)
|
||||
}
|
||||
|
||||
func (r *SQLiteRepository) UpdateConnectionLimiter(id int, limiter constant.ConnectionLimiterUpdate) (constant.ConnectionLimiter, error) {
|
||||
@@ -1280,7 +1283,7 @@ func (r *SQLiteRepository) GetBandwidthLimiter(id int) (constant.BandwidthLimite
|
||||
)
|
||||
bl.SquadIDs = []int(squadIDs)
|
||||
bl.FlowKeys = []string(flowKeys)
|
||||
return bl, err
|
||||
return bl, notFoundErr(err)
|
||||
}
|
||||
|
||||
func (r *SQLiteRepository) UpdateBandwidthLimiter(id int, limiter constant.BandwidthLimiterUpdate) (constant.BandwidthLimiter, error) {
|
||||
@@ -1603,7 +1606,7 @@ func (r *SQLiteRepository) GetTrafficLimiter(id int) (constant.TrafficLimiter, e
|
||||
&tl.UpdatedAt,
|
||||
)
|
||||
tl.SquadIDs = []int(squadIDs)
|
||||
return tl, err
|
||||
return tl, notFoundErr(err)
|
||||
}
|
||||
|
||||
func (r *SQLiteRepository) UpdateTrafficLimiter(id int, limiter constant.TrafficLimiterUpdate) (constant.TrafficLimiter, error) {
|
||||
@@ -1943,7 +1946,7 @@ func (r *SQLiteRepository) GetRateLimiter(id int) (constant.RateLimiter, error)
|
||||
&rl.UpdatedAt,
|
||||
)
|
||||
rl.SquadIDs = []int(squadIDs)
|
||||
return rl, err
|
||||
return rl, notFoundErr(err)
|
||||
}
|
||||
|
||||
func (r *SQLiteRepository) UpdateRateLimiter(id int, limiter constant.RateLimiterUpdate) (constant.RateLimiter, error) {
|
||||
@@ -2048,8 +2051,8 @@ func init() {
|
||||
"created_at_end": LessThanFilter("created_at"),
|
||||
"updated_at_start": GreaterThanFilter("updated_at"),
|
||||
"updated_at_end": LessThanFilter("updated_at"),
|
||||
"sort_asc": SortAscFilter(),
|
||||
"sort_desc": SortDescFilter(),
|
||||
"sort_asc": SortAscFilter([]string{"id", "name", "created_at", "updated_at"}),
|
||||
"sort_desc": SortDescFilter([]string{"id", "name", "created_at", "updated_at"}),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
}
|
||||
@@ -2057,8 +2060,8 @@ func init() {
|
||||
"uuid": EqualFilter("uuid"),
|
||||
"pk": EqualFilter("uuid"),
|
||||
"name": EqualFilter("name"),
|
||||
"squad_id_in": ExistsAndWhereInFilter(
|
||||
sqlbuilder.SQLite.NewSelectBuilder().
|
||||
"squad_id_in": ExistsAndWhereInFilter(func() *sqlbuilder.SelectBuilder {
|
||||
return sqlbuilder.SQLite.NewSelectBuilder().
|
||||
Select(
|
||||
"squad_id",
|
||||
).
|
||||
@@ -2067,23 +2070,22 @@ func init() {
|
||||
).
|
||||
From(
|
||||
"node_to_squad",
|
||||
),
|
||||
"node_to_squad.squad_id",
|
||||
),
|
||||
)
|
||||
}, "node_to_squad.squad_id"),
|
||||
"created_at_start": GreaterThanFilter("created_at"),
|
||||
"created_at_end": LessThanFilter("created_at"),
|
||||
"updated_at_start": GreaterThanFilter("updated_at"),
|
||||
"updated_at_end": LessThanFilter("updated_at"),
|
||||
"sort_asc": SortAscFilter(),
|
||||
"sort_desc": SortDescFilter(),
|
||||
"sort_asc": SortAscFilter([]string{"uuid", "name", "created_at", "updated_at"}),
|
||||
"sort_desc": SortDescFilter([]string{"uuid", "name", "created_at", "updated_at"}),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
}
|
||||
userFilters = map[string]Filter{
|
||||
"id": EqualFilter("id"),
|
||||
"pk": EqualFilter("id"),
|
||||
"squad_id_in": ExistsAndWhereInFilter(
|
||||
sqlbuilder.SQLite.NewSelectBuilder().
|
||||
"squad_id_in": ExistsAndWhereInFilter(func() *sqlbuilder.SelectBuilder {
|
||||
return sqlbuilder.SQLite.NewSelectBuilder().
|
||||
Select(
|
||||
"squad_id",
|
||||
).
|
||||
@@ -2092,26 +2094,24 @@ func init() {
|
||||
).
|
||||
From(
|
||||
"user_to_squad",
|
||||
),
|
||||
"user_to_squad.squad_id",
|
||||
),
|
||||
)
|
||||
}, "user_to_squad.squad_id"),
|
||||
"username": EqualFilter("username"),
|
||||
"inbound": EqualFilter("inbound"),
|
||||
"type": EqualFilter("type"),
|
||||
"created_at_start": GreaterThanFilter("created_at"),
|
||||
"created_at_end": LessThanFilter("created_at"),
|
||||
"updated_at_start": GreaterThanFilter("updated_at"),
|
||||
"updated_at_end": LessThanFilter("updated_at"),
|
||||
"sort_asc": SortAscFilter(),
|
||||
"sort_desc": SortDescFilter(),
|
||||
"sort_asc": SortAscFilter([]string{"id", "username", "inbound", "type", "created_at", "updated_at"}),
|
||||
"sort_desc": SortDescFilter([]string{"id", "username", "inbound", "type", "created_at", "updated_at"}),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
}
|
||||
connectionLimiterFilters = map[string]Filter{
|
||||
"id": EqualFilter("id"),
|
||||
"pk": EqualFilter("id"),
|
||||
"squad_id_in": ExistsAndWhereInFilter(
|
||||
sqlbuilder.SQLite.NewSelectBuilder().
|
||||
"squad_id_in": ExistsAndWhereInFilter(func() *sqlbuilder.SelectBuilder {
|
||||
return sqlbuilder.SQLite.NewSelectBuilder().
|
||||
Select(
|
||||
"squad_id",
|
||||
).
|
||||
@@ -2120,9 +2120,8 @@ func init() {
|
||||
).
|
||||
From(
|
||||
"connection_limiter_to_squad",
|
||||
),
|
||||
"connection_limiter_to_squad.squad_id",
|
||||
),
|
||||
)
|
||||
}, "connection_limiter_to_squad.squad_id"),
|
||||
"strategy": EqualFilter("strategy"),
|
||||
"username": EqualFilter("username"),
|
||||
"outbound": EqualFilter("outbound"),
|
||||
@@ -2132,16 +2131,16 @@ func init() {
|
||||
"created_at_end": LessThanFilter("created_at"),
|
||||
"updated_at_start": GreaterThanFilter("updated_at"),
|
||||
"updated_at_end": LessThanFilter("updated_at"),
|
||||
"sort_asc": SortAscFilter(),
|
||||
"sort_desc": SortDescFilter(),
|
||||
"sort_asc": SortAscFilter([]string{"id", "username", "outbound", "strategy", "connection_type", "lock_type", "count", "created_at", "updated_at"}),
|
||||
"sort_desc": SortDescFilter([]string{"id", "username", "outbound", "strategy", "connection_type", "lock_type", "count", "created_at", "updated_at"}),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
}
|
||||
bandwidthLimiterFilters = map[string]Filter{
|
||||
"id": EqualFilter("id"),
|
||||
"pk": EqualFilter("id"),
|
||||
"squad_id_in": ExistsAndWhereInFilter(
|
||||
sqlbuilder.SQLite.NewSelectBuilder().
|
||||
"squad_id_in": ExistsAndWhereInFilter(func() *sqlbuilder.SelectBuilder {
|
||||
return sqlbuilder.SQLite.NewSelectBuilder().
|
||||
Select(
|
||||
"squad_id",
|
||||
).
|
||||
@@ -2150,31 +2149,31 @@ func init() {
|
||||
).
|
||||
From(
|
||||
"bandwidth_limiter_to_squad",
|
||||
),
|
||||
"bandwidth_limiter_to_squad.squad_id",
|
||||
),
|
||||
)
|
||||
}, "bandwidth_limiter_to_squad.squad_id"),
|
||||
"strategy": EqualFilter("strategy"),
|
||||
"mode": EqualFilter("mode"),
|
||||
"type": EqualFilter("type"),
|
||||
"username": EqualFilter("username"),
|
||||
"down_start": SpeedGreaterEqualThanFilter("raw_down"),
|
||||
"down_end": SpeedLessEqualThanFilter("raw_down"),
|
||||
"up_start": SpeedGreaterEqualThanFilter("raw_up"),
|
||||
"up_end": SpeedLessEqualThanFilter("raw_up"),
|
||||
"created_at_start": GreaterThanFilter("created_at"),
|
||||
"created_at_end": LessThanFilter("created_at"),
|
||||
"updated_at_start": GreaterThanFilter("updated_at"),
|
||||
"updated_at_end": LessThanFilter("updated_at"),
|
||||
"sort_asc": ReplacedSortAscFilter(map[string]string{"down": "raw_down", "up": "raw_up"}),
|
||||
"sort_desc": ReplacedSortDescFilter(map[string]string{"down": "raw_down", "up": "raw_up"}),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
"sort_asc": ReplacedSortAscFilter(
|
||||
map[string]string{"speed": "raw_speed"},
|
||||
[]string{"id", "username", "outbound", "strategy", "mode", "raw_speed", "created_at", "updated_at"},
|
||||
),
|
||||
"sort_desc": ReplacedSortDescFilter(
|
||||
map[string]string{"speed": "raw_speed"},
|
||||
[]string{"id", "username", "outbound", "strategy", "mode", "raw_speed", "created_at", "updated_at"},
|
||||
),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
}
|
||||
trafficLimiterFilters = map[string]Filter{
|
||||
"id": EqualFilter("id"),
|
||||
"pk": EqualFilter("id"),
|
||||
"squad_id_in": ExistsAndWhereInFilter(
|
||||
sqlbuilder.SQLite.NewSelectBuilder().
|
||||
"squad_id_in": ExistsAndWhereInFilter(func() *sqlbuilder.SelectBuilder {
|
||||
return sqlbuilder.SQLite.NewSelectBuilder().
|
||||
Select(
|
||||
"squad_id",
|
||||
).
|
||||
@@ -2183,31 +2182,36 @@ func init() {
|
||||
).
|
||||
From(
|
||||
"traffic_limiter_to_squad",
|
||||
),
|
||||
"traffic_limiter_to_squad.squad_id",
|
||||
),
|
||||
)
|
||||
}, "traffic_limiter_to_squad.squad_id"),
|
||||
"username": EqualFilter("username"),
|
||||
"outbound": EqualFilter("outbound"),
|
||||
"strategy": EqualFilter("strategy"),
|
||||
"mode": EqualFilter("mode"),
|
||||
"used_start": SpeedGreaterEqualThanFilter("raw_used"),
|
||||
"used_end": SpeedLessEqualThanFilter("raw_used"),
|
||||
"used_start": SpeedGreaterEqualThanFilter("raw_used"),
|
||||
"used_end": SpeedLessEqualThanFilter("raw_used"),
|
||||
"quota_start": SpeedGreaterEqualThanFilter("raw_quota"),
|
||||
"quota_end": SpeedLessEqualThanFilter("raw_quota"),
|
||||
"created_at_start": GreaterThanFilter("created_at"),
|
||||
"created_at_end": LessThanFilter("created_at"),
|
||||
"updated_at_start": GreaterThanFilter("updated_at"),
|
||||
"updated_at_end": LessThanFilter("updated_at"),
|
||||
"sort_asc": ReplacedSortAscFilter(map[string]string{"used": "raw_used", "quota": "raw_quota"}),
|
||||
"sort_desc": ReplacedSortDescFilter(map[string]string{"used": "raw_used", "quota": "raw_quota"}),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
"sort_asc": ReplacedSortAscFilter(
|
||||
map[string]string{"used": "raw_used", "quota": "raw_quota"},
|
||||
[]string{"id", "username", "outbound", "strategy", "mode", "raw_used", "raw_quota", "created_at", "updated_at"},
|
||||
),
|
||||
"sort_desc": ReplacedSortDescFilter(
|
||||
map[string]string{"used": "raw_used", "quota": "raw_quota"},
|
||||
[]string{"id", "username", "outbound", "strategy", "mode", "raw_used", "raw_quota", "created_at", "updated_at"},
|
||||
),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
}
|
||||
rateLimiterFilters = map[string]Filter{
|
||||
"id": EqualFilter("id"),
|
||||
"pk": EqualFilter("id"),
|
||||
"squad_id_in": ExistsAndWhereInFilter(
|
||||
sqlbuilder.SQLite.NewSelectBuilder().
|
||||
"squad_id_in": ExistsAndWhereInFilter(func() *sqlbuilder.SelectBuilder {
|
||||
return sqlbuilder.SQLite.NewSelectBuilder().
|
||||
Select(
|
||||
"squad_id",
|
||||
).
|
||||
@@ -2216,9 +2220,8 @@ func init() {
|
||||
).
|
||||
From(
|
||||
"rate_limiter_to_squad",
|
||||
),
|
||||
"rate_limiter_to_squad.squad_id",
|
||||
),
|
||||
)
|
||||
}, "rate_limiter_to_squad.squad_id"),
|
||||
"strategy": EqualFilter("strategy"),
|
||||
"username": EqualFilter("username"),
|
||||
"outbound": EqualFilter("outbound"),
|
||||
@@ -2230,8 +2233,8 @@ func init() {
|
||||
"created_at_end": LessThanFilter("created_at"),
|
||||
"updated_at_start": GreaterThanFilter("updated_at"),
|
||||
"updated_at_end": LessThanFilter("updated_at"),
|
||||
"sort_asc": SortAscFilter(),
|
||||
"sort_desc": SortDescFilter(),
|
||||
"sort_asc": SortAscFilter([]string{"id", "username", "outbound", "strategy", "connection_type", "count", "interval", "created_at", "updated_at"}),
|
||||
"sort_desc": SortDescFilter([]string{"id", "username", "outbound", "strategy", "connection_type", "count", "interval", "created_at", "updated_at"}),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ import (
|
||||
"github.com/sagernet/sing-box/service/manager/repository/sqlite"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/shtorm-7/go-cache/v2"
|
||||
wpconstant "github.com/shtorm-7/workerpool/constant"
|
||||
"github.com/shtorm-7/workerpool/pool"
|
||||
"github.com/shtorm-7/workerpool/tools"
|
||||
"github.com/shtorm-7/workerpool/worker"
|
||||
)
|
||||
|
||||
func RegisterService(registry *boxService.Registry) {
|
||||
@@ -28,16 +32,19 @@ func RegisterService(registry *boxService.Registry) {
|
||||
|
||||
type Service struct {
|
||||
boxService.Adapter
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
repository constant.Repository
|
||||
nodes map[string]constant.ConnectedNode
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
repository constant.Repository
|
||||
nodes map[string]constant.ConnectedNode
|
||||
|
||||
limiterLocks map[int]map[string]*cache.Cache[string, struct{}]
|
||||
trafficUsage map[int]*TrafficUsage
|
||||
|
||||
defaultValidator *validator.Validate
|
||||
|
||||
broadcastQueue wpconstant.Queue
|
||||
broadcastPool wpconstant.Pool
|
||||
|
||||
mtx sync.RWMutex
|
||||
connLockMtx sync.Mutex
|
||||
trafficMtx sync.Mutex
|
||||
@@ -106,6 +113,13 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
|
||||
defaultValidator.RegisterStructValidation(func(sl validator.StructLevel) {
|
||||
validateRateLimiterInterval(sl, sl.Current().Interface().(constant.RateLimiterUpdate).Interval)
|
||||
}, constant.RateLimiterUpdate{})
|
||||
broadcastQueue := make(wpconstant.Queue)
|
||||
broadcastWorkers := make([]wpconstant.WorkerFactory, 16)
|
||||
for i := range broadcastWorkers {
|
||||
broadcastWorkers[i] = worker.NewWorkerFactory(broadcastQueue)
|
||||
}
|
||||
broadcastPool := pool.NewPool(broadcastWorkers)
|
||||
broadcastPool.Start()
|
||||
service := &Service{
|
||||
Adapter: boxService.NewAdapter(C.TypeManager, tag),
|
||||
ctx: ctx,
|
||||
@@ -115,6 +129,8 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
|
||||
limiterLocks: make(map[int]map[string]*cache.Cache[string, struct{}]),
|
||||
trafficUsage: make(map[int]*TrafficUsage),
|
||||
defaultValidator: defaultValidator,
|
||||
broadcastQueue: broadcastQueue,
|
||||
broadcastPool: broadcastPool,
|
||||
}
|
||||
limiters, err := service.repository.GetTrafficLimiters(map[string][]string{})
|
||||
if err != nil {
|
||||
@@ -320,11 +336,9 @@ func (s *Service) CreateUser(user constant.UserCreate) (constant.User, error) {
|
||||
s.closeAllNodes()
|
||||
return createdUser, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node, ok := s.nodes[node.UUID]; ok {
|
||||
node.UpdateUser(createdUser)
|
||||
}
|
||||
}
|
||||
s.dispatchToNodes(nodes, func(node constant.ConnectedNode) {
|
||||
node.UpdateUser(createdUser)
|
||||
})
|
||||
return createdUser, nil
|
||||
}
|
||||
|
||||
@@ -343,6 +357,10 @@ func (s *Service) GetUser(id int) (constant.User, error) {
|
||||
func (s *Service) UpdateUser(id int, user constant.UserUpdate) (constant.User, error) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
err := s.defaultValidator.Struct(user)
|
||||
if err != nil {
|
||||
return constant.User{}, err
|
||||
}
|
||||
updatedUser, err := s.repository.UpdateUser(id, user)
|
||||
if err != nil {
|
||||
return updatedUser, err
|
||||
@@ -354,11 +372,9 @@ func (s *Service) UpdateUser(id int, user constant.UserUpdate) (constant.User, e
|
||||
s.closeAllNodes()
|
||||
return updatedUser, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node, ok := s.nodes[node.UUID]; ok {
|
||||
node.UpdateUser(updatedUser)
|
||||
}
|
||||
}
|
||||
s.dispatchToNodes(nodes, func(node constant.ConnectedNode) {
|
||||
node.UpdateUser(updatedUser)
|
||||
})
|
||||
return updatedUser, nil
|
||||
}
|
||||
|
||||
@@ -376,11 +392,9 @@ func (s *Service) DeleteUser(id int) (constant.User, error) {
|
||||
s.closeAllNodes()
|
||||
return deletedUser, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node, ok := s.nodes[node.UUID]; ok {
|
||||
node.DeleteUser(deletedUser)
|
||||
}
|
||||
}
|
||||
s.dispatchToNodes(nodes, func(node constant.ConnectedNode) {
|
||||
node.DeleteUser(deletedUser)
|
||||
})
|
||||
return deletedUser, nil
|
||||
}
|
||||
|
||||
@@ -402,11 +416,9 @@ func (s *Service) CreateBandwidthLimiter(limiter constant.BandwidthLimiterCreate
|
||||
s.closeAllNodes()
|
||||
return createdLimiter, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node, ok := s.nodes[node.UUID]; ok {
|
||||
node.UpdateBandwidthLimiter(createdLimiter)
|
||||
}
|
||||
}
|
||||
s.dispatchToNodes(nodes, func(node constant.ConnectedNode) {
|
||||
node.UpdateBandwidthLimiter(createdLimiter)
|
||||
})
|
||||
return createdLimiter, nil
|
||||
}
|
||||
|
||||
@@ -440,11 +452,9 @@ func (s *Service) UpdateBandwidthLimiter(id int, limiter constant.BandwidthLimit
|
||||
s.closeAllNodes()
|
||||
return updatedLimiter, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node, ok := s.nodes[node.UUID]; ok {
|
||||
node.UpdateBandwidthLimiter(updatedLimiter)
|
||||
}
|
||||
}
|
||||
s.dispatchToNodes(nodes, func(node constant.ConnectedNode) {
|
||||
node.UpdateBandwidthLimiter(updatedLimiter)
|
||||
})
|
||||
return updatedLimiter, nil
|
||||
}
|
||||
|
||||
@@ -462,11 +472,9 @@ func (s *Service) DeleteBandwidthLimiter(id int) (constant.BandwidthLimiter, err
|
||||
s.closeAllNodes()
|
||||
return deletedLimiter, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node, ok := s.nodes[node.UUID]; ok {
|
||||
node.DeleteBandwidthLimiter(deletedLimiter)
|
||||
}
|
||||
}
|
||||
s.dispatchToNodes(nodes, func(node constant.ConnectedNode) {
|
||||
node.DeleteBandwidthLimiter(deletedLimiter)
|
||||
})
|
||||
return deletedLimiter, nil
|
||||
}
|
||||
|
||||
@@ -494,11 +502,9 @@ func (s *Service) CreateTrafficLimiter(limiter constant.TrafficLimiterCreate) (c
|
||||
s.closeAllNodes()
|
||||
return createdLimiter, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node, ok := s.nodes[node.UUID]; ok {
|
||||
node.UpdateTrafficLimiter(createdLimiter)
|
||||
}
|
||||
}
|
||||
s.dispatchToNodes(nodes, func(node constant.ConnectedNode) {
|
||||
node.UpdateTrafficLimiter(createdLimiter)
|
||||
})
|
||||
return createdLimiter, nil
|
||||
}
|
||||
|
||||
@@ -538,11 +544,9 @@ func (s *Service) UpdateTrafficLimiter(id int, limiter constant.TrafficLimiterUp
|
||||
s.closeAllNodes()
|
||||
return updatedLimiter, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node, ok := s.nodes[node.UUID]; ok {
|
||||
node.UpdateTrafficLimiter(updatedLimiter)
|
||||
}
|
||||
}
|
||||
s.dispatchToNodes(nodes, func(node constant.ConnectedNode) {
|
||||
node.UpdateTrafficLimiter(updatedLimiter)
|
||||
})
|
||||
return updatedLimiter, nil
|
||||
}
|
||||
|
||||
@@ -566,11 +570,9 @@ func (s *Service) UpdateTrafficLimiterUsed(id int, used uint64) (constant.Traffi
|
||||
s.closeAllNodes()
|
||||
return updatedLimiter, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node, ok := s.nodes[node.UUID]; ok {
|
||||
node.UpdateTrafficLimiter(updatedLimiter)
|
||||
}
|
||||
}
|
||||
s.dispatchToNodes(nodes, func(node constant.ConnectedNode) {
|
||||
node.UpdateTrafficLimiter(updatedLimiter)
|
||||
})
|
||||
return updatedLimiter, nil
|
||||
}
|
||||
|
||||
@@ -591,11 +593,9 @@ func (s *Service) DeleteTrafficLimiter(id int) (constant.TrafficLimiter, error)
|
||||
s.closeAllNodes()
|
||||
return deletedLimiter, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node, ok := s.nodes[node.UUID]; ok {
|
||||
node.DeleteTrafficLimiter(deletedLimiter)
|
||||
}
|
||||
}
|
||||
s.dispatchToNodes(nodes, func(node constant.ConnectedNode) {
|
||||
node.DeleteTrafficLimiter(deletedLimiter)
|
||||
})
|
||||
return deletedLimiter, nil
|
||||
}
|
||||
|
||||
@@ -617,11 +617,9 @@ func (s *Service) CreateConnectionLimiter(limiter constant.ConnectionLimiterCrea
|
||||
s.closeAllNodes()
|
||||
return createdLimiter, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node, ok := s.nodes[node.UUID]; ok {
|
||||
node.UpdateConnectionLimiter(createdLimiter)
|
||||
}
|
||||
}
|
||||
s.dispatchToNodes(nodes, func(node constant.ConnectedNode) {
|
||||
node.UpdateConnectionLimiter(createdLimiter)
|
||||
})
|
||||
return createdLimiter, nil
|
||||
}
|
||||
|
||||
@@ -655,11 +653,9 @@ func (s *Service) UpdateConnectionLimiter(id int, limiter constant.ConnectionLim
|
||||
s.closeAllNodes()
|
||||
return updatedLimiter, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node, ok := s.nodes[node.UUID]; ok {
|
||||
node.UpdateConnectionLimiter(updatedLimiter)
|
||||
}
|
||||
}
|
||||
s.dispatchToNodes(nodes, func(node constant.ConnectedNode) {
|
||||
node.UpdateConnectionLimiter(updatedLimiter)
|
||||
})
|
||||
if limiter.LockType != "manager" {
|
||||
s.connLockMtx.Lock()
|
||||
defer s.connLockMtx.Unlock()
|
||||
@@ -682,11 +678,9 @@ func (s *Service) DeleteConnectionLimiter(id int) (constant.ConnectionLimiter, e
|
||||
s.closeAllNodes()
|
||||
return deletedLimiter, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node, ok := s.nodes[node.UUID]; ok {
|
||||
node.DeleteConnectionLimiter(deletedLimiter)
|
||||
}
|
||||
}
|
||||
s.dispatchToNodes(nodes, func(node constant.ConnectedNode) {
|
||||
node.DeleteConnectionLimiter(deletedLimiter)
|
||||
})
|
||||
if deletedLimiter.LockType == "manager" {
|
||||
s.connLockMtx.Lock()
|
||||
defer s.connLockMtx.Unlock()
|
||||
@@ -713,11 +707,9 @@ func (s *Service) CreateRateLimiter(limiter constant.RateLimiterCreate) (constan
|
||||
s.closeAllNodes()
|
||||
return createdLimiter, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node, ok := s.nodes[node.UUID]; ok {
|
||||
node.UpdateRateLimiter(createdLimiter)
|
||||
}
|
||||
}
|
||||
s.dispatchToNodes(nodes, func(node constant.ConnectedNode) {
|
||||
node.UpdateRateLimiter(createdLimiter)
|
||||
})
|
||||
return createdLimiter, nil
|
||||
}
|
||||
|
||||
@@ -751,11 +743,9 @@ func (s *Service) UpdateRateLimiter(id int, limiter constant.RateLimiterUpdate)
|
||||
s.closeAllNodes()
|
||||
return updatedLimiter, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node, ok := s.nodes[node.UUID]; ok {
|
||||
node.UpdateRateLimiter(updatedLimiter)
|
||||
}
|
||||
}
|
||||
s.dispatchToNodes(nodes, func(node constant.ConnectedNode) {
|
||||
node.UpdateRateLimiter(updatedLimiter)
|
||||
})
|
||||
return updatedLimiter, nil
|
||||
}
|
||||
|
||||
@@ -773,11 +763,9 @@ func (s *Service) DeleteRateLimiter(id int) (constant.RateLimiter, error) {
|
||||
s.closeAllNodes()
|
||||
return deletedLimiter, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node, ok := s.nodes[node.UUID]; ok {
|
||||
node.DeleteRateLimiter(deletedLimiter)
|
||||
}
|
||||
}
|
||||
s.dispatchToNodes(nodes, func(node constant.ConnectedNode) {
|
||||
node.DeleteRateLimiter(deletedLimiter)
|
||||
})
|
||||
return deletedLimiter, nil
|
||||
}
|
||||
|
||||
@@ -922,6 +910,7 @@ func (s *Service) Start(stage adapter.StartStage) error {
|
||||
}
|
||||
|
||||
func (s *Service) Close() error {
|
||||
s.broadcastPool.Stop()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -936,6 +925,22 @@ func (s *Service) closeAllNodes() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) dispatchToNodes(nodes []constant.Node, fn func(node constant.ConnectedNode)) {
|
||||
awaits := make([]<-chan struct{}, 0, len(nodes))
|
||||
for _, node := range nodes {
|
||||
connectedNode, ok := s.nodes[node.UUID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
awaits = append(awaits, tools.Await(s.broadcastQueue, func() {
|
||||
fn(connectedNode)
|
||||
}))
|
||||
}
|
||||
for _, await := range awaits {
|
||||
<-await
|
||||
}
|
||||
}
|
||||
|
||||
func convertIntSliceToStringSlice(values []int) []string {
|
||||
result := make([]string, len(values))
|
||||
for i, v := range values {
|
||||
|
||||
89
service/node/inbound/anytls.go
Normal file
89
service/node/inbound/anytls.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/protocol/anytls"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing-box/service/node/constant"
|
||||
)
|
||||
|
||||
type AnyTLSManager struct {
|
||||
inbounds map[string]*AnyTLSUserManager
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewAnyTLSManager() *AnyTLSManager {
|
||||
return &AnyTLSManager{
|
||||
inbounds: make(map[string]*AnyTLSUserManager),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *AnyTLSManager) AddUserManager(inbound adapter.Inbound) error {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
m.inbounds[inbound.Tag()] = &AnyTLSUserManager{
|
||||
inbound: inbound.(*anytls.Inbound),
|
||||
usersMap: make(map[string]option.AnyTLSUser),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *AnyTLSManager) GetUserManager(tag string) (constant.UserManager, bool) {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
inbound, ok := m.inbounds[tag]
|
||||
return inbound, ok
|
||||
}
|
||||
|
||||
func (m *AnyTLSManager) GetUserManagerTags() []string {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
tags := make([]string, 0, len(m.inbounds))
|
||||
for tag := range m.inbounds {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
type AnyTLSUserManager struct {
|
||||
inbound *anytls.Inbound
|
||||
usersMap map[string]option.AnyTLSUser
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func (i *AnyTLSUserManager) postUpdate() {
|
||||
users := make([]option.AnyTLSUser, 0, len(i.usersMap))
|
||||
for _, user := range i.usersMap {
|
||||
users = append(users, user)
|
||||
}
|
||||
i.inbound.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func (i *AnyTLSUserManager) UpdateUser(user CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
i.usersMap[user.Username] = option.AnyTLSUser{Name: user.Username, Password: user.Password}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *AnyTLSUserManager) UpdateUsers(users []CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
clear(i.usersMap)
|
||||
for _, user := range users {
|
||||
i.usersMap[user.Username] = option.AnyTLSUser{Name: user.Username, Password: user.Password}
|
||||
}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *AnyTLSUserManager) DeleteUser(username string) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
delete(i.usersMap, username)
|
||||
i.postUpdate()
|
||||
}
|
||||
89
service/node/inbound/http.go
Normal file
89
service/node/inbound/http.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/protocol/http"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing-box/service/node/constant"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
)
|
||||
|
||||
type HTTPManager struct {
|
||||
inbounds map[string]*HTTPUserManager
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewHTTPManager() *HTTPManager {
|
||||
return &HTTPManager{
|
||||
inbounds: make(map[string]*HTTPUserManager),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *HTTPManager) AddUserManager(inbound adapter.Inbound) error {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
m.inbounds[inbound.Tag()] = &HTTPUserManager{
|
||||
inbound: inbound.(*http.Inbound),
|
||||
usersMap: make(map[string]auth.User),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *HTTPManager) GetUserManager(tag string) (constant.UserManager, bool) {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
inbound, ok := m.inbounds[tag]
|
||||
return inbound, ok
|
||||
}
|
||||
|
||||
func (m *HTTPManager) GetUserManagerTags() []string {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
tags := make([]string, 0, len(m.inbounds))
|
||||
for tag := range m.inbounds {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
type HTTPUserManager struct {
|
||||
inbound *http.Inbound
|
||||
usersMap map[string]auth.User
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func (i *HTTPUserManager) postUpdate() {
|
||||
users := make([]auth.User, 0, len(i.usersMap))
|
||||
for _, user := range i.usersMap {
|
||||
users = append(users, user)
|
||||
}
|
||||
i.inbound.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func (i *HTTPUserManager) UpdateUser(user CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
i.usersMap[user.Username] = auth.User{Username: user.Username, Password: user.Password}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *HTTPUserManager) UpdateUsers(users []CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
clear(i.usersMap)
|
||||
for _, user := range users {
|
||||
i.usersMap[user.Username] = auth.User{Username: user.Username, Password: user.Password}
|
||||
}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *HTTPUserManager) DeleteUser(username string) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
delete(i.usersMap, username)
|
||||
i.postUpdate()
|
||||
}
|
||||
@@ -11,8 +11,9 @@ import (
|
||||
)
|
||||
|
||||
type HysteriaManager struct {
|
||||
access sync.Mutex
|
||||
inbounds map[string]*HysteriaUserManager
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewHysteriaManager() *HysteriaManager {
|
||||
@@ -22,8 +23,8 @@ func NewHysteriaManager() *HysteriaManager {
|
||||
}
|
||||
|
||||
func (m *HysteriaManager) AddUserManager(inbound adapter.Inbound) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
m.inbounds[inbound.Tag()] = &HysteriaUserManager{
|
||||
inbound: inbound.(*hysteria.Inbound),
|
||||
usersMap: make(map[string]option.HysteriaUser),
|
||||
@@ -32,15 +33,15 @@ func (m *HysteriaManager) AddUserManager(inbound adapter.Inbound) error {
|
||||
}
|
||||
|
||||
func (m *HysteriaManager) GetUserManager(tag string) (constant.UserManager, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
inbound, ok := m.inbounds[tag]
|
||||
return inbound, ok
|
||||
}
|
||||
|
||||
func (m *HysteriaManager) GetUserManagerTags() []string {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
tags := make([]string, 0, len(m.inbounds))
|
||||
for tag := range m.inbounds {
|
||||
tags = append(tags, tag)
|
||||
|
||||
@@ -11,8 +11,9 @@ import (
|
||||
)
|
||||
|
||||
type Hysteria2Manager struct {
|
||||
access sync.Mutex
|
||||
inbounds map[string]*Hysteria2UserManager
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewHysteria2Manager() *Hysteria2Manager {
|
||||
@@ -22,8 +23,8 @@ func NewHysteria2Manager() *Hysteria2Manager {
|
||||
}
|
||||
|
||||
func (m *Hysteria2Manager) AddUserManager(inbound adapter.Inbound) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
m.inbounds[inbound.Tag()] = &Hysteria2UserManager{
|
||||
inbound: inbound.(*hysteria2.Inbound),
|
||||
usersMap: make(map[string]option.Hysteria2User),
|
||||
@@ -32,15 +33,15 @@ func (m *Hysteria2Manager) AddUserManager(inbound adapter.Inbound) error {
|
||||
}
|
||||
|
||||
func (m *Hysteria2Manager) GetUserManager(tag string) (constant.UserManager, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
inbound, ok := m.inbounds[tag]
|
||||
return inbound, ok
|
||||
}
|
||||
|
||||
func (m *Hysteria2Manager) GetUserManagerTags() []string {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
tags := make([]string, 0, len(m.inbounds))
|
||||
for tag := range m.inbounds {
|
||||
tags = append(tags, tag)
|
||||
|
||||
89
service/node/inbound/mixed.go
Normal file
89
service/node/inbound/mixed.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/protocol/mixed"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing-box/service/node/constant"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
)
|
||||
|
||||
type MixedManager struct {
|
||||
inbounds map[string]*MixedUserManager
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewMixedManager() *MixedManager {
|
||||
return &MixedManager{
|
||||
inbounds: make(map[string]*MixedUserManager),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MixedManager) AddUserManager(inbound adapter.Inbound) error {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
m.inbounds[inbound.Tag()] = &MixedUserManager{
|
||||
inbound: inbound.(*mixed.Inbound),
|
||||
usersMap: make(map[string]auth.User),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MixedManager) GetUserManager(tag string) (constant.UserManager, bool) {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
inbound, ok := m.inbounds[tag]
|
||||
return inbound, ok
|
||||
}
|
||||
|
||||
func (m *MixedManager) GetUserManagerTags() []string {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
tags := make([]string, 0, len(m.inbounds))
|
||||
for tag := range m.inbounds {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
type MixedUserManager struct {
|
||||
inbound *mixed.Inbound
|
||||
usersMap map[string]auth.User
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func (i *MixedUserManager) postUpdate() {
|
||||
users := make([]auth.User, 0, len(i.usersMap))
|
||||
for _, user := range i.usersMap {
|
||||
users = append(users, user)
|
||||
}
|
||||
i.inbound.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func (i *MixedUserManager) UpdateUser(user CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
i.usersMap[user.Username] = auth.User{Username: user.Username, Password: user.Password}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *MixedUserManager) UpdateUsers(users []CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
clear(i.usersMap)
|
||||
for _, user := range users {
|
||||
i.usersMap[user.Username] = auth.User{Username: user.Username, Password: user.Password}
|
||||
}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *MixedUserManager) DeleteUser(username string) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
delete(i.usersMap, username)
|
||||
i.postUpdate()
|
||||
}
|
||||
@@ -11,8 +11,9 @@ import (
|
||||
)
|
||||
|
||||
type MTProxyManager struct {
|
||||
access sync.Mutex
|
||||
inbounds map[string]*MTProxyUserManager
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewMTProxyManager() *MTProxyManager {
|
||||
@@ -22,8 +23,8 @@ func NewMTProxyManager() *MTProxyManager {
|
||||
}
|
||||
|
||||
func (m *MTProxyManager) AddUserManager(inbound adapter.Inbound) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
m.inbounds[inbound.Tag()] = &MTProxyUserManager{
|
||||
inbound: inbound.(*mtproxy.Inbound),
|
||||
usersMap: make(map[string]option.MTProxyUser),
|
||||
@@ -32,15 +33,15 @@ func (m *MTProxyManager) AddUserManager(inbound adapter.Inbound) error {
|
||||
}
|
||||
|
||||
func (m *MTProxyManager) GetUserManager(tag string) (constant.UserManager, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
inbound, ok := m.inbounds[tag]
|
||||
return inbound, ok
|
||||
}
|
||||
|
||||
func (m *MTProxyManager) GetUserManagerTags() []string {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
tags := make([]string, 0, len(m.inbounds))
|
||||
for tag := range m.inbounds {
|
||||
tags = append(tags, tag)
|
||||
|
||||
89
service/node/inbound/naive.go
Normal file
89
service/node/inbound/naive.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/protocol/naive"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing-box/service/node/constant"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
)
|
||||
|
||||
type NaiveManager struct {
|
||||
inbounds map[string]*NaiveUserManager
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewNaiveManager() *NaiveManager {
|
||||
return &NaiveManager{
|
||||
inbounds: make(map[string]*NaiveUserManager),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *NaiveManager) AddUserManager(inbound adapter.Inbound) error {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
m.inbounds[inbound.Tag()] = &NaiveUserManager{
|
||||
inbound: inbound.(*naive.Inbound),
|
||||
usersMap: make(map[string]auth.User),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *NaiveManager) GetUserManager(tag string) (constant.UserManager, bool) {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
inbound, ok := m.inbounds[tag]
|
||||
return inbound, ok
|
||||
}
|
||||
|
||||
func (m *NaiveManager) GetUserManagerTags() []string {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
tags := make([]string, 0, len(m.inbounds))
|
||||
for tag := range m.inbounds {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
type NaiveUserManager struct {
|
||||
inbound *naive.Inbound
|
||||
usersMap map[string]auth.User
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func (i *NaiveUserManager) postUpdate() {
|
||||
users := make([]auth.User, 0, len(i.usersMap))
|
||||
for _, user := range i.usersMap {
|
||||
users = append(users, user)
|
||||
}
|
||||
i.inbound.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func (i *NaiveUserManager) UpdateUser(user CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
i.usersMap[user.Username] = auth.User{Username: user.Username, Password: user.Password}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *NaiveUserManager) UpdateUsers(users []CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
clear(i.usersMap)
|
||||
for _, user := range users {
|
||||
i.usersMap[user.Username] = auth.User{Username: user.Username, Password: user.Password}
|
||||
}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *NaiveUserManager) DeleteUser(username string) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
delete(i.usersMap, username)
|
||||
i.postUpdate()
|
||||
}
|
||||
89
service/node/inbound/socks.go
Normal file
89
service/node/inbound/socks.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/protocol/socks"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing-box/service/node/constant"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
)
|
||||
|
||||
type SocksManager struct {
|
||||
inbounds map[string]*SocksUserManager
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewSocksManager() *SocksManager {
|
||||
return &SocksManager{
|
||||
inbounds: make(map[string]*SocksUserManager),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SocksManager) AddUserManager(inbound adapter.Inbound) error {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
m.inbounds[inbound.Tag()] = &SocksUserManager{
|
||||
inbound: inbound.(*socks.Inbound),
|
||||
usersMap: make(map[string]auth.User),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *SocksManager) GetUserManager(tag string) (constant.UserManager, bool) {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
inbound, ok := m.inbounds[tag]
|
||||
return inbound, ok
|
||||
}
|
||||
|
||||
func (m *SocksManager) GetUserManagerTags() []string {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
tags := make([]string, 0, len(m.inbounds))
|
||||
for tag := range m.inbounds {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
type SocksUserManager struct {
|
||||
inbound *socks.Inbound
|
||||
usersMap map[string]auth.User
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func (i *SocksUserManager) postUpdate() {
|
||||
users := make([]auth.User, 0, len(i.usersMap))
|
||||
for _, user := range i.usersMap {
|
||||
users = append(users, user)
|
||||
}
|
||||
i.inbound.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func (i *SocksUserManager) UpdateUser(user CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
i.usersMap[user.Username] = auth.User{Username: user.Username, Password: user.Password}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *SocksUserManager) UpdateUsers(users []CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
clear(i.usersMap)
|
||||
for _, user := range users {
|
||||
i.usersMap[user.Username] = auth.User{Username: user.Username, Password: user.Password}
|
||||
}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *SocksUserManager) 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