Compare commits

..

4 Commits

Author SHA1 Message Date
Shtorm
9ff7a84afe Refactor TrustTunnel 2026-06-04 10:03:20 +03:00
Shtorm
e363c2ff78 Add Mieru inbound, refactor sudoku. Fixes 2026-06-04 07:54:26 +03:00
Shtorm
195a33379d Add OpenVPN, TrustTunnel, Sudoku, inbound managers. Fixes 2026-06-04 01:47:50 +03:00
Shtorm
9b3da79c32 Update wireguard 2026-05-31 02:07:13 +03:00
179 changed files with 17188 additions and 1354 deletions

9
.gitignore vendored
View File

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

View File

@@ -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:

View File

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

View File

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

View File

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

View File

@@ -63,7 +63,7 @@ func init() {
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0")
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0")
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_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")

View File

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

View File

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

View File

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

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

View File

@@ -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:

View File

@@ -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.

View File

@@ -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
入站的标签。
入站的标签。

View 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.

View 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
客户端若不发送用户提示,代理服务器将拒绝连接。

View File

@@ -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/) |

View File

@@ -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/) |

View File

@@ -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.

View File

@@ -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/)。

View File

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

View File

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

View File

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

View File

@@ -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": {

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

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

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

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

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

View File

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

View File

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

View 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
// }
}
]
}

View 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",
}
}
}
]
}

View 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"
}
]
}

View 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"
}
]
}

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

View 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"
}
]
}

View File

@@ -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
View File

@@ -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
View File

@@ -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
View 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
View 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`)
})
}

View File

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

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

View File

@@ -6,6 +6,6 @@ type ManagerServiceDatabase struct {
}
type ManagerServiceOptions struct {
Inbounds []string `json:"inbounds"`
Inbounds []string `json:"inbounds"`
Database ManagerServiceDatabase `json:"database"`
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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
View 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"`
}

View File

@@ -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
View 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
View 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"`
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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" {

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

View File

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

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

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

View File

@@ -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{

View File

@@ -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 {

View File

@@ -1 +1 @@
with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,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

View File

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

View File

@@ -1 +1 @@
with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,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

View File

@@ -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")

View File

@@ -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";

View File

@@ -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() {

View File

@@ -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"]);

View File

@@ -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"`
}

View File

@@ -51,3 +51,5 @@ type Repository interface {
UpdateRateLimiter(id int, limiter RateLimiterUpdate) (RateLimiter, error)
DeleteRateLimiter(id int) (RateLimiter, error)
}

View File

@@ -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, "\"")
}

View File

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

View File

@@ -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, "\"")
}

View File

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

View File

@@ -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 {

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

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

View File

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

View File

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

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

View File

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

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

View 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