Add MTProxy, MASQUE, VPN, Link parser. Update AmneziaWG. Remove Tunneling

This commit is contained in:
Sergei Maklagin
2026-04-29 22:11:30 +03:00
parent 09f9f114aa
commit 04908a6a67
158 changed files with 7994 additions and 2277 deletions

View File

@@ -20,6 +20,8 @@ builds:
- with_acme - with_acme
- with_clash_api - with_clash_api
- with_tailscale - with_tailscale
- with_masque
- with_mtproxy
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
- GOTOOLCHAIN=local - GOTOOLCHAIN=local
@@ -61,6 +63,8 @@ builds:
- with_acme - with_acme
- with_clash_api - with_clash_api
- with_tailscale - with_tailscale
- with_masque
- with_mtproxy
- with_manager - with_manager
- with_admin_panel - with_admin_panel
env: env:
@@ -97,6 +101,8 @@ builds:
- with_acme - with_acme
- with_clash_api - with_clash_api
- with_tailscale - with_tailscale
- with_masque
- with_mtproxy
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
targets: targets:

View File

@@ -4,34 +4,37 @@ Sing-box with extended features.
## 🔥 Features ## 🔥 Features
### 🌐 Outbounds ### 🌐 Protocols
- **WARP** — Cloudflare WARP integration through WireGuard - **WARP**
- **Tunnel** — Protocol for creating tunnels across nodes - **Masque**
- **Bond** — Link aggregation for increased throughput - **MTProxy**
- **Mieru** — Secure, hard to classify, hard to probe network protocol - **Mieru**
- **Failover** — Automatic outbound switching for high availability - **VPN**
- **Bond**
- **Fallback**
### 🚦 Limiters ### 🚦 Limiters
- **Bandwidth Limiter** — Upload / download rate limiting - **Bandwidth Limiter**
- **Connection Limiter** — Concurrent connection control - **Connection Limiter**
### 🛡 Encryption & Obfuscation ### 🛡 Encryption & Obfuscation
- **Amnezia 1.5** — WireGuard traffic obfuscation - **Amnezia 2.0**
- **VLESS encryption** — XRAY encryption for VLESS protocol - **VLESS encryption**
### 🔄 Transports ### 🔄 Transports
- **mKCP** — Reliable UDP-based transport - **mKCP**
- **XHTTP** — Modern XRAY transport - **XHTTP**
### 🛠 Services ### 🛠 Services
- **Admin Panel** — Web-based management interface - **Admin Panel**
- **Manager** — Management service for configuring squads, nodes, users, limiters - **Manager**
- **Node Manager** — Service for connecting nodes to remote manager - **Node Manager**
### ⚙ Miscellaneous ### ⚙ Miscellaneous
- **SDNS (DNSCrypt)** — Encrypted DNS queries for enhanced privacy - **Link parser**
- **Extended WireGuard options** — Advanced configuration capabilities - **SDNS (DNSCrypt)**
- **Unified Delay** — Unified latency measurement - **Extended WireGuard options**
- **Unified Delay**
## 📚 Examples ## 📚 Examples

View File

@@ -68,6 +68,8 @@ type DNSTransport interface {
Type() string Type() string
Tag() string Tag() string
Dependencies() []string Dependencies() []string
// Reset closes the transport's existing connections so later requests use fresh connections.
// Exchanges that are currently using those connections may fail.
Reset() Reset()
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error) Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
} }

View File

@@ -48,6 +48,7 @@ type CacheFile interface {
RDRCStore RDRCStore
StoreWARPConfig() bool StoreWARPConfig() bool
StoreMASQUEConfig() bool
LoadMode() string LoadMode() string
StoreMode(mode string) error StoreMode(mode string) error
@@ -59,6 +60,10 @@ type CacheFile interface {
SaveRuleSet(tag string, set *SavedBinary) error SaveRuleSet(tag string, set *SavedBinary) error
LoadWARPConfig(tag string) *SavedBinary LoadWARPConfig(tag string) *SavedBinary
SaveWARPConfig(tag string, set *SavedBinary) error SaveWARPConfig(tag string, set *SavedBinary) error
LoadMASQUEConfig(tag string) *SavedBinary
SaveMASQUEConfig(tag string, set *SavedBinary) error
LoadSubscription(tag string) *SavedBinary
SaveSubscription(tag string, sub *SavedBinary) error
} }
type SavedBinary struct { type SavedBinary struct {

View File

@@ -42,16 +42,15 @@ type InboundManager interface {
} }
type InboundContext struct { type InboundContext struct {
Inbound string Inbound string
InboundType string InboundType string
IPVersion uint8 IPVersion uint8
Network string Network string
Source M.Socksaddr Source M.Socksaddr
Destination M.Socksaddr Destination M.Socksaddr
TunnelSource string Gateway *netip.Addr
TunnelDestination string User string
User string Outbound string
Outbound string
// sniffer // sniffer

View File

@@ -1,6 +1,8 @@
package adapter package adapter
import ( import (
"net/netip"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
@@ -36,6 +38,8 @@ type PlatformInterface interface {
UsePlatformNotification() bool UsePlatformNotification() bool
SendNotification(notification *Notification) error SendNotification(notification *Notification) error
MyInterfaceAddress() []netip.Addr
} }
type FindConnectionOwnerRequest struct { type FindConnectionOwnerRequest struct {

51
adapter/provider.go Normal file
View File

@@ -0,0 +1,51 @@
package adapter
import (
"context"
"time"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/x/list"
)
type Provider interface {
Type() string
Tag() string
Outbounds() []Outbound
Outbound(tag string) (Outbound, bool)
UpdatedAt() time.Time
HealthCheck(ctx context.Context) (map[string]uint16, error)
RegisterCallback(callback ProviderUpdateCallback) *list.Element[ProviderUpdateCallback]
UnregisterCallback(element *list.Element[ProviderUpdateCallback])
}
type ProviderUpdater interface {
Update() error
}
type ProviderSubscriptionInfo interface {
SubscriptionInfo() SubscriptionInfo
}
type ProviderRegistry interface {
option.ProviderOptionsRegistry
CreateProvider(ctx context.Context, router Router, logFactory log.Factory, tag string, providerType string, options any) (Provider, error)
}
type ProviderManager interface {
Lifecycle
Providers() []Provider
Get(tag string) (Provider, bool)
Remove(tag string) error
Create(ctx context.Context, router Router, logFactory log.Factory, tag string, providerType string, options any) error
}
type SubscriptionInfo struct {
Upload int64
Download int64
Total int64
Expire int64
}
type ProviderUpdateCallback = func(tag string) error

267
adapter/provider/adapter.go Normal file
View File

@@ -0,0 +1,267 @@
package provider
import (
"context"
"reflect"
"sync"
"sync/atomic"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/batch"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service"
)
type Adapter struct {
ctx context.Context
outbound adapter.OutboundManager
router adapter.Router
logFactory log.Factory
logger log.ContextLogger
providerType string
providerTag string
outbounds []adapter.Outbound
outboundsByTag map[string]adapter.Outbound
ticker *time.Ticker
checking atomic.Bool
history adapter.URLTestHistoryStorage
callbackAccess sync.Mutex
callbacks list.List[adapter.ProviderUpdateCallback]
link string
enabled 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 {
timeout := time.Duration(options.Timeout)
if timeout == 0 {
timeout = 3 * time.Second
}
interval := time.Duration(options.Interval)
if interval == 0 {
interval = 10 * time.Minute
}
if interval < time.Minute {
interval = time.Minute
}
return Adapter{
ctx: ctx,
outbound: outbound,
router: router,
logFactory: logFactory,
logger: logger,
providerType: providerType,
providerTag: providerTag,
enabled: options.Enabled,
link: options.URL,
timeout: timeout,
interval: interval,
}
}
func (a *Adapter) Start() error {
a.history = service.FromContext[adapter.URLTestHistoryStorage](a.ctx)
if a.history == nil {
if clashServer := service.FromContext[adapter.ClashServer](a.ctx); clashServer != nil {
a.history = clashServer.HistoryStorage()
} else {
a.history = urltest.NewHistoryStorage()
}
}
go a.loopCheck()
return nil
}
func (a *Adapter) Type() string {
return a.providerType
}
func (a *Adapter) Tag() string {
return a.providerTag
}
func (a *Adapter) Outbounds() []adapter.Outbound {
return a.outbounds
}
func (a *Adapter) Outbound(tag string) (adapter.Outbound, bool) {
if a.outboundsByTag == nil {
return nil, false
}
detour, ok := a.outboundsByTag[tag]
return detour, ok
}
func (a *Adapter) UpdateOutbounds(oldOpts []option.Outbound, newOpts []option.Outbound) {
a.removeUseless(newOpts)
var (
oldOptByTag = make(map[string]option.Outbound)
outbounds = make([]adapter.Outbound, 0, len(newOpts))
outboundsByTag = make(map[string]adapter.Outbound)
)
for _, opt := range oldOpts {
oldOptByTag[opt.Tag] = opt
}
for i, opt := range newOpts {
var tag string
if opt.Tag != "" {
tag = F.ToString(a.providerTag, "/", opt.Tag)
} else {
tag = F.ToString(a.providerTag, "/", i)
}
outbound, exist := a.outbound.Outbound(tag)
if !exist || !reflect.DeepEqual(opt, oldOptByTag[opt.Tag]) {
err := a.outbound.Create(
adapter.WithContext(a.ctx, &adapter.InboundContext{
Outbound: tag,
}),
a.router,
a.logFactory.NewLogger(F.ToString("outbound/", opt.Type, "[", tag, "]")),
tag,
opt.Type,
opt.Options,
)
if err != nil {
a.logger.Warn(err, " in ", tag, ", skip create this outbound")
continue
}
outbound, _ = a.outbound.Outbound(tag)
}
outbounds = append(outbounds, outbound)
outboundsByTag[tag] = outbound
}
if a.enabled && a.history != nil {
go a.HealthCheck(a.ctx)
}
a.outbounds = outbounds
a.outboundsByTag = outboundsByTag
}
func (a *Adapter) HealthCheck(ctx context.Context) (map[string]uint16, error) {
if a.ticker != nil {
a.ticker.Reset(a.interval)
}
return a.healthcheck(ctx)
}
func (a *Adapter) RegisterCallback(callback adapter.ProviderUpdateCallback) *list.Element[adapter.ProviderUpdateCallback] {
a.callbackAccess.Lock()
defer a.callbackAccess.Unlock()
return a.callbacks.PushBack(callback)
}
func (a *Adapter) UnregisterCallback(element *list.Element[adapter.ProviderUpdateCallback]) {
a.callbackAccess.Lock()
defer a.callbackAccess.Unlock()
a.callbacks.Remove(element)
}
func (a *Adapter) UpdateGroups() {
for element := a.callbacks.Front(); element != nil; element = element.Next() {
element.Value(a.providerTag)
}
}
func (a *Adapter) Close() error {
if a.ticker != nil {
a.ticker.Stop()
}
outbounds := a.outbounds
a.outbounds = nil
var err error
for _, ob := range outbounds {
if err2 := a.outbound.Remove(ob.Tag()); err2 != nil {
err = E.Append(err, err2, func(err error) error {
return E.Cause(err, "close outbound [", ob.Tag(), "]")
})
}
}
return err
}
func (a *Adapter) loopCheck() {
if !a.enabled {
return
}
a.ticker = time.NewTicker(a.interval)
a.healthcheck(a.ctx)
for {
select {
case <-a.ctx.Done():
return
case <-a.ticker.C:
a.healthcheck(a.ctx)
}
}
}
func (a *Adapter) healthcheck(ctx context.Context) (map[string]uint16, error) {
result := make(map[string]uint16)
if a.checking.Swap(true) {
return result, nil
}
defer a.checking.Store(false)
b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10))
var resultAccess sync.Mutex
checked := make(map[string]bool)
for _, detour := range a.outbounds {
tag := detour.Tag()
if checked[tag] {
continue
}
checked[tag] = true
b.Go(tag, func() (any, error) {
ctx, cancel := context.WithTimeout(a.ctx, a.timeout)
defer cancel()
t, err := urltest.URLTest(ctx, a.link, detour)
if err != nil {
a.logger.Debug("outbound ", tag, " unavailable: ", err)
a.history.DeleteURLTestHistory(tag)
} else {
a.logger.Debug("outbound ", tag, " available: ", t, "ms")
a.history.StoreURLTestHistory(tag, &adapter.URLTestHistory{
Time: time.Now(),
Delay: t,
})
resultAccess.Lock()
result[tag] = t
resultAccess.Unlock()
}
return nil, nil
})
}
b.Wait()
return result, nil
}
func (a *Adapter) removeUseless(newOpts []option.Outbound) {
if len(a.outbounds) == 0 {
return
}
exists := make(map[string]bool)
for i, opt := range newOpts {
var tag string
if opt.Tag != "" {
tag = F.ToString(a.providerTag, "/", opt.Tag)
} else {
tag = F.ToString(a.providerTag, "/", i)
}
exists[tag] = true
}
for _, opt := range a.outbounds {
if !exists[opt.Tag()] {
if err := a.outbound.Remove(opt.Tag()); err != nil {
a.logger.Error(err, "close outbound [", opt.Tag(), "]")
}
}
}
}

157
adapter/provider/manager.go Normal file
View File

@@ -0,0 +1,157 @@
package provider
import (
"context"
"io"
"os"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
var _ adapter.ProviderManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.ProviderRegistry
access sync.Mutex
started bool
stage adapter.StartStage
providers []adapter.Provider
providerByTag map[string]adapter.Provider
wg sync.WaitGroup
}
func NewManager(logger logger.ContextLogger, registry adapter.ProviderRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
providerByTag: make(map[string]adapter.Provider),
}
}
func (m *Manager) Initialize() {
}
func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
providers := m.providers
m.access.Unlock()
for _, provider := range providers {
err := adapter.LegacyStart(provider, stage)
if err != nil {
return E.Cause(err, stage, " provider/", provider.Type(), "[", provider.Tag(), "]")
}
}
return nil
}
func (m *Manager) Close() error {
monitor := taskmonitor.New(m.logger, C.StopTimeout)
m.access.Lock()
if !m.started {
m.access.Unlock()
return nil
}
m.started = false
providers := m.providers
m.providers = nil
m.access.Unlock()
var err error
for _, provider := range providers {
if closer, isCloser := provider.(io.Closer); isCloser {
monitor.Start("close provider/", provider.Type(), "[", provider.Tag(), "]")
err = E.Append(err, closer.Close(), func(err error) error {
return E.Cause(err, "close provider/", provider.Type(), "[", provider.Tag(), "]")
})
monitor.Finish()
}
}
return nil
}
func (m *Manager) Providers() []adapter.Provider {
m.access.Lock()
defer m.access.Unlock()
return m.providers
}
func (m *Manager) Get(tag string) (adapter.Provider, bool) {
m.access.Lock()
provider, found := m.providerByTag[tag]
m.access.Unlock()
return provider, found
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
provider, found := m.providerByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.providerByTag, tag)
index := common.Index(m.providers, func(it adapter.Provider) bool {
return it == provider
})
if index == -1 {
panic("invalid provider index")
}
m.providers = append(m.providers[:index], m.providers[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return common.Close(provider)
}
return nil
}
func (m *Manager) Create(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, providerType string, options any) error {
if tag == "" {
return os.ErrInvalid
}
provider, err := m.registry.CreateProvider(ctx, router, logFactory, tag, providerType, options)
if err != nil {
return err
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
for _, stage := range adapter.ListStartStages {
err = adapter.LegacyStart(provider, stage)
if err != nil {
return E.Cause(err, stage, " provider/", provider.Type(), "[", provider.Tag(), "]")
}
}
}
if existsProvider, loaded := m.providerByTag[tag]; loaded {
if m.started {
err = common.Close(existsProvider)
if err != nil {
return E.Cause(err, "close provider", provider.Type(), "[", existsProvider.Tag(), "]")
}
}
existsIndex := common.Index(m.providers, func(it adapter.Provider) bool {
return it == existsProvider
})
if existsIndex == -1 {
panic("invalid provider index")
}
m.providers = append(m.providers[:existsIndex], m.providers[existsIndex+1:]...)
}
m.providers = append(m.providers, provider)
m.providerByTag[tag] = provider
return nil
}

View File

@@ -0,0 +1,72 @@
package provider
import (
"context"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, options T) (adapter.Provider, error)
func Register[Options any](registry *Registry, providerType string, constructor ConstructorFunc[Options]) {
registry.register(providerType, func() any {
return new(Options)
}, func(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, rawOptions any) (adapter.Provider, error) {
var options *Options
if rawOptions != nil {
options = rawOptions.(*Options)
}
return constructor(ctx, router, logFactory, tag, common.PtrValueOrDefault(options))
})
}
var _ adapter.ProviderRegistry = (*Registry)(nil)
type (
optionsConstructorFunc func() any
constructorFunc func(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, options any) (adapter.Provider, error)
)
type Registry struct {
access sync.Mutex
optionsType map[string]optionsConstructorFunc
constructors map[string]constructorFunc
}
func NewRegistry() *Registry {
return &Registry{
optionsType: make(map[string]optionsConstructorFunc),
constructors: make(map[string]constructorFunc),
}
}
func (r *Registry) CreateOptions(providerType string) (any, bool) {
r.access.Lock()
defer r.access.Unlock()
optionsConstructor, loaded := r.optionsType[providerType]
if !loaded {
return nil, false
}
return optionsConstructor(), true
}
func (r *Registry) CreateProvider(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, providerType string, options any) (adapter.Provider, error) {
r.access.Lock()
defer r.access.Unlock()
constructor, loaded := r.constructors[providerType]
if !loaded {
return nil, E.New("provider type not found: '" + providerType + "'")
}
return constructor(ctx, router, logFactory, tag, options)
}
func (r *Registry) register(providerType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
r.access.Lock()
defer r.access.Unlock()
r.optionsType[providerType] = optionsConstructor
r.constructors[providerType] = constructor
}

47
box.go
View File

@@ -12,6 +12,7 @@ import (
"github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/adapter/provider"
boxService "github.com/sagernet/sing-box/adapter/service" boxService "github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/common/certificate" "github.com/sagernet/sing-box/common/certificate"
"github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/dialer"
@@ -44,6 +45,7 @@ type Box struct {
endpoint *endpoint.Manager endpoint *endpoint.Manager
inbound *inbound.Manager inbound *inbound.Manager
outbound *outbound.Manager outbound *outbound.Manager
provider *provider.Manager
service *boxService.Manager service *boxService.Manager
dnsTransport *dns.TransportManager dnsTransport *dns.TransportManager
dnsRouter *dns.Router dnsRouter *dns.Router
@@ -64,6 +66,7 @@ func Context(
inboundRegistry adapter.InboundRegistry, inboundRegistry adapter.InboundRegistry,
outboundRegistry adapter.OutboundRegistry, outboundRegistry adapter.OutboundRegistry,
endpointRegistry adapter.EndpointRegistry, endpointRegistry adapter.EndpointRegistry,
providerRegistry adapter.ProviderRegistry,
dnsTransportRegistry adapter.DNSTransportRegistry, dnsTransportRegistry adapter.DNSTransportRegistry,
serviceRegistry adapter.ServiceRegistry, serviceRegistry adapter.ServiceRegistry,
) context.Context { ) context.Context {
@@ -82,6 +85,11 @@ func Context(
ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry) ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry)
ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry) ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry)
} }
if service.FromContext[option.ProviderOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.ProviderRegistry](ctx) == nil {
ctx = service.ContextWith[option.ProviderOptionsRegistry](ctx, providerRegistry)
ctx = service.ContextWith[adapter.ProviderRegistry](ctx, providerRegistry)
}
if service.FromContext[adapter.DNSTransportRegistry](ctx) == nil { if service.FromContext[adapter.DNSTransportRegistry](ctx) == nil {
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry) ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry) ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
@@ -104,6 +112,7 @@ func New(options Options) (*Box, error) {
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx) endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx) inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx) outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
providerRegistry := service.FromContext[adapter.ProviderRegistry](ctx)
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx) dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx) serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
@@ -116,6 +125,9 @@ func New(options Options) (*Box, error) {
if outboundRegistry == nil { if outboundRegistry == nil {
return nil, E.New("missing outbound registry in context") return nil, E.New("missing outbound registry in context")
} }
if providerRegistry == nil {
return nil, E.New("missing provider registry in context")
}
if dnsTransportRegistry == nil { if dnsTransportRegistry == nil {
return nil, E.New("missing DNS transport registry in context") return nil, E.New("missing DNS transport registry in context")
} }
@@ -181,11 +193,13 @@ func New(options Options) (*Box, error) {
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry) endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager) inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final) outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
providerManager := provider.NewManager(logFactory.NewLogger("provider"), providerRegistry)
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final) dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry) serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
service.MustRegister[adapter.EndpointManager](ctx, endpointManager) service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
service.MustRegister[adapter.InboundManager](ctx, inboundManager) service.MustRegister[adapter.InboundManager](ctx, inboundManager)
service.MustRegister[adapter.OutboundManager](ctx, outboundManager) service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
service.MustRegister[adapter.ProviderManager](ctx, providerManager)
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager) service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
service.MustRegister[adapter.ServiceManager](ctx, serviceManager) service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions) dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
@@ -276,6 +290,10 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize inbound[", i, "]") return nil, E.Cause(err, "initialize inbound[", i, "]")
} }
} }
options.Outbounds = append(options.Outbounds, option.Outbound{
Tag: "Compatible",
Type: C.TypeDirect,
})
for i, outboundOptions := range options.Outbounds { for i, outboundOptions := range options.Outbounds {
var tag string var tag string
if outboundOptions.Tag != "" { if outboundOptions.Tag != "" {
@@ -302,6 +320,25 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize outbound[", i, "]") return nil, E.Cause(err, "initialize outbound[", i, "]")
} }
} }
for i, providerOptions := range options.Providers {
var tag string
if providerOptions.Tag != "" {
tag = providerOptions.Tag
} else {
tag = F.ToString(i)
}
err = providerManager.Create(
ctx,
router,
logFactory,
tag,
providerOptions.Type,
providerOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize provider[", i, "]")
}
}
for i, serviceOptions := range options.Services { for i, serviceOptions := range options.Services {
var tag string var tag string
if serviceOptions.Tag != "" { if serviceOptions.Tag != "" {
@@ -392,6 +429,7 @@ func New(options Options) (*Box, error) {
endpoint: endpointManager, endpoint: endpointManager,
inbound: inboundManager, inbound: inboundManager,
outbound: outboundManager, outbound: outboundManager,
provider: providerManager,
dnsTransport: dnsTransportManager, dnsTransport: dnsTransportManager,
service: serviceManager, service: serviceManager,
dnsRouter: dnsRouter, dnsRouter: dnsRouter,
@@ -455,11 +493,11 @@ func (s *Box) preStart() error {
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service) err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.provider, s.service)
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router) err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.provider, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
if err != nil { if err != nil {
return err return err
} }
@@ -479,7 +517,7 @@ func (s *Box) start() error {
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service) err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.provider, s.service)
if err != nil { if err != nil {
return err return err
} }
@@ -487,7 +525,7 @@ func (s *Box) start() error {
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service) err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.provider, s.service)
if err != nil { if err != nil {
return err return err
} }
@@ -513,6 +551,7 @@ func (s *Box) Close() error {
{"service", s.service}, {"service", s.service},
{"endpoint", s.endpoint}, {"endpoint", s.endpoint},
{"inbound", s.inbound}, {"inbound", s.inbound},
{"provider", s.provider},
{"outbound", s.outbound}, {"outbound", s.outbound},
{"router", s.router}, {"router", s.router},
{"connection", s.connection}, {"connection", s.connection},

View File

@@ -1,15 +1,12 @@
package cloudflare package cloudflare
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/tidwall/gjson"
) )
type CloudflareApi struct { type CloudflareApi struct {
@@ -25,50 +22,93 @@ func NewCloudflareApi(opts ...CloudflareApiOption) *CloudflareApi {
} }
func (api *CloudflareApi) CreateProfile(ctx context.Context, publicKey string) (*CloudflareProfile, error) { func (api *CloudflareApi) CreateProfile(ctx context.Context, publicKey string) (*CloudflareProfile, error) {
request, err := http.NewRequest("POST", "https://api.cloudflareclient.com/v0i1909051800/reg", strings.NewReader( serial, err := GenerateRandomAndroidSerial()
fmt.Sprintf( if err != nil {
"{\"install_id\":\"\",\"tos\":\"%s\",\"key\":\"%s\",\"fcm_token\":\"\",\"type\":\"ios\",\"locale\":\"en_US\"}", return nil, fmt.Errorf("failed to generate serial: %v", err)
time.Now().Format("2006-01-02T15:04:05.000Z"), }
publicKey, data := Registration{
), Key: publicKey,
)) InstallID: "",
FcmToken: "",
Tos: TimeAsCfString(time.Now()),
Model: "PC",
Serial: serial,
OsVersion: "",
KeyType: KeyTypeWg,
TunType: TunTypeWg,
Locale: "en-US",
}
jsonData, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("failed to marshal json: %v", err)
}
request, err := http.NewRequest("POST", ApiUrl+"/"+ApiVersion+"/reg", bytes.NewBuffer(jsonData))
if err != nil { if err != nil {
return nil, err return nil, err
} }
for k, v := range Headers {
request.Header.Set(k, v)
}
response, err := api.client.Do(request.WithContext(ctx)) response, err := api.client.Do(request.WithContext(ctx))
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer response.Body.Close() defer response.Body.Close()
if response.StatusCode != 200 { if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status code is not 200") return nil, fmt.Errorf("failed to register: %v", response.StatusCode)
}
content, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
} }
profile := new(CloudflareProfile) profile := new(CloudflareProfile)
return profile, json.NewDecoder(strings.NewReader(gjson.Get(string(content), "result").Raw)).Decode(profile) return profile, json.NewDecoder(response.Body).Decode(profile)
} }
func (api *CloudflareApi) GetProfile(ctx context.Context, authToken string, id string) (*CloudflareProfile, error) { func (api *CloudflareApi) EnrollKey(ctx context.Context, authToken string, id string, keyType, tunType, publicKey string) (*CloudflareProfile, error) {
request, err := http.NewRequest("GET", "https://api.cloudflareclient.com/v0i1909051800/reg/"+id, nil) deviceUpdate := DeviceUpdate{
Name: "PC",
Key: publicKey,
KeyType: keyType,
TunType: tunType,
}
jsonData, err := json.Marshal(deviceUpdate)
if err != nil {
return nil, fmt.Errorf("failed to marshal json: %v", err)
}
request, err := http.NewRequest("PATCH", ApiUrl+"/"+ApiVersion+"/reg/"+id, bytes.NewBuffer(jsonData))
if err != nil { if err != nil {
return nil, err return nil, err
} }
for k, v := range Headers {
request.Header.Set(k, v)
}
request.Header.Set("Authorization", "Bearer "+authToken) request.Header.Set("Authorization", "Bearer "+authToken)
response, err := api.client.Do(request.WithContext(ctx)) response, err := api.client.Do(request.WithContext(ctx))
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer response.Body.Close() defer response.Body.Close()
if response.StatusCode != 200 { if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status code is not 200") return nil, fmt.Errorf("failed to enroll key: %v", response.StatusCode)
} }
content, err := io.ReadAll(response.Body) profile := new(CloudflareProfile)
return profile, json.NewDecoder(response.Body).Decode(profile)
}
func (api *CloudflareApi) GetProfile(ctx context.Context, authToken string, id string) (*CloudflareProfile, error) {
request, err := http.NewRequest("GET", ApiUrl+"/"+ApiVersion+"/reg/"+id, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for k, v := range Headers {
request.Header.Set(k, v)
}
request.Header.Set("Authorization", "Bearer "+authToken)
response, err := api.client.Do(request.WithContext(ctx))
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get profile: %v", response.StatusCode)
}
profile := new(CloudflareProfile) profile := new(CloudflareProfile)
return profile, json.NewDecoder(strings.NewReader(gjson.Get(string(content), "result").Raw)).Decode(profile) return profile, json.NewDecoder(response.Body).Decode(profile)
} }

View File

@@ -0,0 +1,25 @@
package cloudflare
const (
ApiUrl = "https://api.cloudflareclient.com"
ApiVersion = "v0a4471"
ConnectSNI = "consumer-masque.cloudflareclient.com"
// unused for now
ZeroTierSNI = "zt-masque.cloudflareclient.com"
ConnectURI = "https://cloudflareaccess.com"
DefaultModel = "PC"
KeyTypeWg = "curve25519"
TunTypeWg = "wireguard"
KeyTypeMasque = "secp256r1"
TunTypeMasque = "masque"
DefaultLocale = "en_US"
DefaultEndpointH2V4 = "162.159.198.2"
DefaultEndpointH2V6 = ""
)
var Headers = map[string]string{
"User-Agent": "WARP for Android",
"CF-Client-Version": "a-6.35-4471",
"Content-Type": "application/json; charset=UTF-8",
"Connection": "Keep-Alive",
}

132
common/cloudflare/models.go Normal file
View File

@@ -0,0 +1,132 @@
package cloudflare
import (
"strings"
)
type Registration struct {
Key string `json:"key"`
InstallID string `json:"install_id"`
FcmToken string `json:"fcm_token"`
Tos string `json:"tos"`
Model string `json:"model"`
Serial string `json:"serial_number"`
OsVersion string `json:"os_version"`
KeyType string `json:"key_type"`
TunType string `json:"tunnel_type"`
Locale string `json:"locale"`
}
type CloudflareProfile struct {
ID string `json:"id"`
Type string `json:"type"`
Model string `json:"model"`
Name string `json:"name"`
Key string `json:"key"`
KeyType string `json:"key_type"`
TunType string `json:"tunnel_type"`
Account Account `json:"account"`
Config Config `json:"config"`
// WarpEnabled not set for ZeroTier
WarpEnabled bool `json:"warp_enabled,omitempty"`
// Waitlist not set for ZeroTier
Waitlist bool `json:"waitlist_enabled,omitempty"`
Created string `json:"created"`
Updated string `json:"updated"`
// Tos not set for ZeroTier
Tos string `json:"tos,omitempty"`
// Place not set for ZeroTier
Place int `json:"place,omitempty"`
Locale string `json:"locale"`
// Enabled not set for ZeroTier
Enabled bool `json:"enabled,omitempty"`
InstallID string `json:"install_id"`
// Token only set for /reg call
Token string `json:"token,omitempty"`
FcmToken string `json:"fcm_token"`
// SerialNumber not set for ZeroTier
SerialNumber string `json:"serial_number,omitempty"`
Policy Policy `json:"policy"`
}
type Account struct {
ID string `json:"id"`
AccountType string `json:"account_type"`
// Created not set for ZeroTier
Created string `json:"created,omitempty"`
// Updated not set for ZeroTier
Updated string `json:"updated,omitempty"`
// Managed only set for ZeroTier
Managed string `json:"managed,omitempty"`
// Organization only set for ZeroTier
Organization string `json:"organization,omitempty"`
// PremiumData not set for ZeroTier
PremiumData int `json:"premium_data,omitempty"`
// Quota not set for ZeroTier
Quota int `json:"quota,omitempty"`
// WarpPlus not set for ZeroTier
WarpPlus bool `json:"warp_plus,omitempty"`
// ReferralCode not set for ZeroTier
ReferralCount int `json:"referral_count,omitempty"`
// ReferralRenewalCount not set for ZeroTier
ReferralRenewalCount int `json:"referral_renewal_countdown,omitempty"`
// Role not set for ZeroTier
Role string `json:"role,omitempty"`
// License not set for ZeroTier
License string `json:"license,omitempty"`
}
type Config struct {
ClientID string `json:"client_id"`
Peers []Peer `json:"peers"`
Interface struct {
Addresses struct {
V4 string `json:"v4"`
V6 string `json:"v6"`
} `json:"addresses"`
} `json:"interface"`
Services struct {
HTTPProxy string `json:"http_proxy"`
} `json:"services"`
}
type Peer struct {
PublicKey string `json:"public_key"`
Endpoint struct {
V4 string `json:"v4"`
V6 string `json:"v6"`
Host string `json:"host"`
Ports []int `json:"ports"`
} `json:"endpoint"`
}
type Policy struct {
TunnelProtocol string `json:"tunnel_protocol"`
}
type DeviceUpdate struct {
Key string `json:"key"`
KeyType string `json:"key_type"`
TunType string `json:"tunnel_type"`
Name string `json:"name,omitempty"`
}
type APIError struct {
Result interface{} `json:"result"`
Success bool `json:"success"`
Errors []ErrorInfo `json:"errors"`
Messages []string `json:"messages"`
}
type ErrorInfo struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (e *APIError) Error() string {
errors := make([]string, len(e.Errors))
for i, err := range e.Errors {
errors[i] = err.Message
}
return strings.Join(errors, ",")
}

View File

@@ -4,12 +4,14 @@ import (
"context" "context"
"net" "net"
"net/http" "net/http"
"time"
) )
type CloudflareApiOption func(api *CloudflareApi) type CloudflareApiOption func(api *CloudflareApi)
func WithDialContext(dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) CloudflareApiOption { func WithDialContext(dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) CloudflareApiOption {
return func(api *CloudflareApi) { return func(api *CloudflareApi) {
api.client.Timeout = 30 * time.Second
api.client.Transport = &http.Transport{ api.client.Transport = &http.Transport{
DialContext: dialContext, DialContext: dialContext,
} }

View File

@@ -1,64 +0,0 @@
package cloudflare
import "time"
type CloudflareProfile struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Key string `json:"key"`
Account struct {
ID string `json:"id"`
AccountType string `json:"account_type"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
PremiumData int `json:"premium_data"`
Quota int `json:"quota"`
Usage int `json:"usage"`
WARPPlus bool `json:"warp_plus"`
ReferralCount int `json:"referral_count"`
ReferralRenewalCountdown int `json:"referral_renewal_countdown"`
Role string `json:"role"`
License string `json:"license"`
TTL time.Time `json:"ttl"`
} `json:"account"`
Config struct {
ClientID string `json:"client_id"`
Interface struct {
Addresses struct {
V4 string `json:"v4"`
V6 string `json:"v6"`
} `json:"addresses"`
} `json:"interface"`
Peers []struct {
PublicKey string `json:"public_key"`
Endpoint struct {
V4 string `json:"v4"`
V6 string `json:"v6"`
Host string `json:"host"`
Ports []int `json:"ports"`
} `json:"endpoint"`
} `json:"peers"`
Services struct {
HTTPProxy string `json:"http_proxy"`
} `json:"services"`
Metrics struct {
Ping int `json:"ping"`
Report int `json:"report"`
} `json:"metrics"`
} `json:"config"`
Token string `json:"token"`
WARPEnabled bool `json:"warp_enabled"`
WaitlistEnabled bool `json:"waitlist_enabled"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Tos time.Time `json:"tos"`
Place int `json:"place"`
Locale string `json:"locale"`
Enabled bool `json:"enabled"`
InstallID string `json:"install_id"`
FcmToken string `json:"fcm_token"`
Policy struct {
TunnelProtocol string `json:"tunnel_protocol"`
} `json:"policy"`
}

View File

@@ -0,0 +1,19 @@
package cloudflare
import (
"crypto/rand"
"encoding/hex"
"time"
)
func GenerateRandomAndroidSerial() (string, error) {
serial := make([]byte, 8)
if _, err := rand.Read(serial); err != nil {
return "", err
}
return hex.EncodeToString(serial), nil
}
func TimeAsCfString(t time.Time) string {
return t.Format("2006-01-02T15:04:05.000-07:00")
}

View File

@@ -11,3 +11,13 @@ func ContextWithIsExternalConnection(ctx context.Context) context.Context {
func IsExternalConnectionFromContext(ctx context.Context) bool { func IsExternalConnectionFromContext(ctx context.Context) bool {
return ctx.Value(contextKeyIsExternalConnection{}) != nil return ctx.Value(contextKeyIsExternalConnection{}) != nil
} }
type contextKeyIsProviderConnection struct{}
func ContextWithIsProviderConnection(ctx context.Context) context.Context {
return context.WithValue(ctx, contextKeyIsProviderConnection{}, true)
}
func IsProviderConnectionFromContext(ctx context.Context) bool {
return ctx.Value(contextKeyIsProviderConnection{}) != nil
}

View File

@@ -17,30 +17,31 @@ type Group struct {
type groupConnItem struct { type groupConnItem struct {
conn io.Closer conn io.Closer
isExternal bool isExternal bool
isProvider bool
} }
func NewGroup() *Group { func NewGroup() *Group {
return &Group{} return &Group{}
} }
func (g *Group) NewConn(conn net.Conn, isExternal bool) net.Conn { func (g *Group) NewConn(conn net.Conn, isExternal bool, isProvider bool) net.Conn {
g.access.Lock() g.access.Lock()
defer g.access.Unlock() defer g.access.Unlock()
item := g.connections.PushBack(&groupConnItem{conn, isExternal}) item := g.connections.PushBack(&groupConnItem{conn, isExternal, isProvider})
return &Conn{Conn: conn, group: g, element: item} return &Conn{Conn: conn, group: g, element: item}
} }
func (g *Group) NewPacketConn(conn net.PacketConn, isExternal bool) net.PacketConn { func (g *Group) NewPacketConn(conn net.PacketConn, isExternal bool, isProvider bool) net.PacketConn {
g.access.Lock() g.access.Lock()
defer g.access.Unlock() defer g.access.Unlock()
item := g.connections.PushBack(&groupConnItem{conn, isExternal}) item := g.connections.PushBack(&groupConnItem{conn, isExternal, isProvider})
return &PacketConn{PacketConn: conn, group: g, element: item} return &PacketConn{PacketConn: conn, group: g, element: item}
} }
func (g *Group) NewSingPacketConn(conn N.PacketConn, isExternal bool) N.PacketConn { func (g *Group) NewSingPacketConn(conn N.PacketConn, isExternal bool, isProvider bool) N.PacketConn {
g.access.Lock() g.access.Lock()
defer g.access.Unlock() defer g.access.Unlock()
item := g.connections.PushBack(&groupConnItem{conn, isExternal}) item := g.connections.PushBack(&groupConnItem{conn, isExternal, isProvider})
return &SingPacketConn{PacketConn: conn, group: g, element: item} return &SingPacketConn{PacketConn: conn, group: g, element: item}
} }

View File

@@ -12,7 +12,6 @@ type klock struct {
ref uint64 ref uint64
} }
// Create new Kmutex
func New[T comparable]() *Kmutex[T] { func New[T comparable]() *Kmutex[T] {
l := sync.Mutex{} l := sync.Mutex{}
return &Kmutex[T]{ return &Kmutex[T]{
@@ -21,7 +20,6 @@ func New[T comparable]() *Kmutex[T] {
} }
} }
// Unlock Kmutex by unique ID
func (km *Kmutex[T]) Unlock(key T) { func (km *Kmutex[T]) Unlock(key T) {
km.l.Lock() km.l.Lock()
defer km.l.Unlock() defer km.l.Unlock()
@@ -37,7 +35,6 @@ func (km *Kmutex[T]) Unlock(key T) {
kl.cond.Signal() kl.cond.Signal()
} }
// Lock Kmutex by unique ID
func (km *Kmutex[T]) Lock(key T) { func (km *Kmutex[T]) Lock(key T) {
km.l.Lock() km.l.Lock()
defer km.l.Unlock() defer km.l.Unlock()

View File

@@ -0,0 +1,74 @@
package tls
import (
"context"
"crypto/ecdsa"
"crypto/tls"
"crypto/x509"
"time"
"github.com/sagernet/quic-go/http3"
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 NewMASQUEClient(ctx context.Context, logger logger.ContextLogger, serverName string, cert [][]byte, privateKey *ecdsa.PrivateKey, peerPublicKey *ecdsa.PublicKey, options option.MASQUEOutboundTLSOptions) (Config, error) {
var tlsConfig tls.Config
tlsConfig.ServerName = serverName
tlsConfig.InsecureSkipVerify = true
tlsConfig.NextProtos = []string{http3.NextProtoH3}
tlsConfig.Certificates = []tls.Certificate{
{
Certificate: cert,
PrivateKey: privateKey,
},
}
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)
}
}
for _, curve := range options.CurvePreferences {
tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, tls.CurveID(curve))
}
if !options.Insecure {
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return nil
}
cert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return err
}
if _, ok := cert.PublicKey.(*ecdsa.PublicKey); !ok {
return x509.ErrUnsupportedAlgorithm
}
if !cert.PublicKey.(*ecdsa.PublicKey).Equal(peerPublicKey) {
return x509.CertificateInvalidError{Cert: cert, Reason: 10, Detail: "remote endpoint has a different public key than what we trust in config.json"}
}
return nil
}
}
var config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
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
}

68
common/utils.go Normal file
View File

@@ -0,0 +1,68 @@
package common
import (
"encoding/base64"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"github.com/sagernet/sing/common/json/badoption"
)
func StringToType[T any](str string) T {
var value T
v := reflect.ValueOf(&value).Elem()
switch any(value).(type) {
case badoption.Duration:
d, err := time.ParseDuration(str)
if err != nil {
v.SetInt(StringToType[int64](str))
} else {
v.Set(reflect.ValueOf(d))
}
return value
case badoption.HTTPHeader:
headers := badoption.HTTPHeader{}
reg := regexp.MustCompile(`^[ \t]*?(\S+?):[ \t]*?(\S+?)[ \t]*?$`)
for _, header := range strings.Split(str, "\n") {
result := reg.FindStringSubmatch(header)
if result != nil {
key := result[1]
headers[key] = strings.Split(result[2], ",")
}
}
v.Set(reflect.ValueOf(headers))
return value
}
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
i, _ := strconv.ParseInt(str, 10, 64)
v.SetInt(i)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
i, _ := strconv.ParseUint(str, 10, 64)
v.SetUint(i)
case reflect.Float32, reflect.Float64:
f, _ := strconv.ParseFloat(str, 64)
v.SetFloat(f)
case reflect.Bool:
b, _ := strconv.ParseBool(str)
v.SetBool(b)
default:
panic("unsupported type")
}
return value
}
func DecodeBase64URLSafe(content string) (string, error) {
s := strings.ReplaceAll(content, " ", "-")
s = strings.ReplaceAll(s, "/", "_")
s = strings.ReplaceAll(s, "+", "-")
s = strings.ReplaceAll(s, "=", "")
result, err := base64.RawURLEncoding.DecodeString(s)
if err != nil {
return content, nil
}
return string(result), nil
}

20
constant/provider.go Normal file
View File

@@ -0,0 +1,20 @@
package constant
const (
ProviderTypeInline = "inline"
ProviderTypeLocal = "local"
ProviderTypeRemote = "remote"
)
func ProviderDisplayName(providerType string) string {
switch providerType {
case ProviderTypeInline:
return "Inline"
case ProviderTypeLocal:
return "Local"
case ProviderTypeRemote:
return "Remote"
default:
return "Unknown"
}
}

View File

@@ -16,6 +16,9 @@ const (
TypeNaive = "naive" TypeNaive = "naive"
TypeWireGuard = "wireguard" TypeWireGuard = "wireguard"
TypeWARP = "warp" TypeWARP = "warp"
TypeMASQUE = "masque"
TypeMTProxy = "mtproxy"
TypeParser = "parser"
TypeHysteria = "hysteria" TypeHysteria = "hysteria"
TypeTor = "tor" TypeTor = "tor"
TypeSSH = "ssh" TypeSSH = "ssh"
@@ -27,8 +30,8 @@ const (
TypeTUIC = "tuic" TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2" TypeHysteria2 = "hysteria2"
TypeBond = "bond" TypeBond = "bond"
TypeTunnelServer = "tunnel-server" TypeVPNServer = "vpn-server"
TypeTunnelClient = "tunnel-client" TypeVPNClient = "vpn-client"
TypeTailscale = "tailscale" TypeTailscale = "tailscale"
TypeConnectionLimiter = "connection-limiter" TypeConnectionLimiter = "connection-limiter"
TypeBandwidthLimiter = "bandwidth-limiter" TypeBandwidthLimiter = "bandwidth-limiter"
@@ -47,7 +50,7 @@ const (
) )
const ( const (
TypeFailover = "failover" TypeFallback = "fallback"
TypeSelector = "selector" TypeSelector = "selector"
TypeURLTest = "urltest" TypeURLTest = "urltest"
) )
@@ -84,6 +87,12 @@ func ProxyDisplayName(proxyType string) string {
return "WireGuard" return "WireGuard"
case TypeWARP: case TypeWARP:
return "WARP" return "WARP"
case TypeMASQUE:
return "MASQUE"
case TypeMTProxy:
return "MTProxy"
case TypeParser:
return "Parser"
case TypeHysteria: case TypeHysteria:
return "Hysteria" return "Hysteria"
case TypeTor: case TypeTor:
@@ -106,18 +115,18 @@ func ProxyDisplayName(proxyType string) string {
return "Mieru" return "Mieru"
case TypeAnyTLS: case TypeAnyTLS:
return "AnyTLS" return "AnyTLS"
case TypeFailover: case TypeFallback:
return "Failover" return "Fallback"
case TypeTailscale: case TypeTailscale:
return "Tailscale" return "Tailscale"
case TypeSelector: case TypeSelector:
return "Selector" return "Selector"
case TypeURLTest: case TypeURLTest:
return "URLTest" return "URLTest"
case TypeTunnelClient: case TypeVPNClient:
return "Tunnel client" return "VPN Client"
case TypeTunnelServer: case TypeVPNServer:
return "Tunnel server" return "VPN Server"
default: default:
return "Unknown" return "Unknown"
} }

View File

@@ -1,20 +0,0 @@
package constant
type WARPConfig struct {
PrivateKey string `json:"private_key"`
Interface struct {
Addresses struct {
V4 string `json:"v4"`
V6 string `json:"v6"`
} `json:"addresses"`
} `json:"interface"`
Peers []struct {
PublicKey string `json:"public_key"`
Endpoint struct {
V4 string `json:"v4"`
V6 string `json:"v6"`
Host string `json:"host"`
Ports []int `json:"ports"`
} `json:"endpoint"`
} `json:"peers"`
}

View File

@@ -1,145 +0,0 @@
package transport
import (
"context"
"os"
"sync"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
type TransportState int
const (
StateNew TransportState = iota
StateStarted
StateClosing
StateClosed
)
var (
ErrTransportClosed = os.ErrClosed
ErrConnectionReset = E.New("connection reset")
)
type BaseTransport struct {
dns.TransportAdapter
Logger logger.ContextLogger
mutex sync.Mutex
state TransportState
inFlight int32
queriesComplete chan struct{}
closeCtx context.Context
closeCancel context.CancelFunc
}
func NewBaseTransport(adapter dns.TransportAdapter, logger logger.ContextLogger) *BaseTransport {
ctx, cancel := context.WithCancel(context.Background())
return &BaseTransport{
TransportAdapter: adapter,
Logger: logger,
state: StateNew,
closeCtx: ctx,
closeCancel: cancel,
}
}
func (t *BaseTransport) State() TransportState {
t.mutex.Lock()
defer t.mutex.Unlock()
return t.state
}
func (t *BaseTransport) SetStarted() error {
t.mutex.Lock()
defer t.mutex.Unlock()
switch t.state {
case StateNew:
t.state = StateStarted
return nil
case StateStarted:
return nil
default:
return ErrTransportClosed
}
}
func (t *BaseTransport) BeginQuery() bool {
t.mutex.Lock()
defer t.mutex.Unlock()
if t.state != StateStarted {
return false
}
t.inFlight++
return true
}
func (t *BaseTransport) EndQuery() {
t.mutex.Lock()
if t.inFlight > 0 {
t.inFlight--
}
if t.inFlight == 0 && t.queriesComplete != nil {
close(t.queriesComplete)
t.queriesComplete = nil
}
t.mutex.Unlock()
}
func (t *BaseTransport) CloseContext() context.Context {
return t.closeCtx
}
func (t *BaseTransport) Shutdown(ctx context.Context) error {
t.mutex.Lock()
if t.state >= StateClosing {
t.mutex.Unlock()
return nil
}
if t.state == StateNew {
t.state = StateClosed
t.mutex.Unlock()
t.closeCancel()
return nil
}
t.state = StateClosing
if t.inFlight == 0 {
t.state = StateClosed
t.mutex.Unlock()
t.closeCancel()
return nil
}
t.queriesComplete = make(chan struct{})
queriesComplete := t.queriesComplete
t.mutex.Unlock()
t.closeCancel()
select {
case <-queriesComplete:
t.mutex.Lock()
t.state = StateClosed
t.mutex.Unlock()
return nil
case <-ctx.Done():
t.mutex.Lock()
t.state = StateClosed
t.mutex.Unlock()
return ctx.Err()
}
}
func (t *BaseTransport) Close() error {
ctx, cancel := context.WithTimeout(context.Background(), C.TCPTimeout)
defer cancel()
return t.Shutdown(ctx)
}

547
dns/transport/conn_pool.go Normal file
View File

@@ -0,0 +1,547 @@
package transport
import (
"context"
"net"
"sync"
"time"
"github.com/sagernet/sing/common/x/list"
)
type ConnPoolMode int
const (
ConnPoolSingle ConnPoolMode = iota
ConnPoolOrdered
)
type ConnPoolOptions[T comparable] struct {
Mode ConnPoolMode
IsAlive func(T) bool
Close func(T, error)
}
type ConnPool[T comparable] struct {
options ConnPoolOptions[T]
access sync.Mutex
closed bool
state *connPoolState[T]
}
type connPoolState[T comparable] struct {
ctx context.Context
cancel context.CancelCauseFunc
all map[T]struct{}
idle list.List[T]
idleElements map[T]*list.Element[T]
shared T
hasShared bool
sharedClaimed bool
sharedCtx context.Context
sharedCancel context.CancelCauseFunc
connecting *connPoolConnect[T]
}
type connPoolConnect[T comparable] struct {
done chan struct{}
err error
}
type connPoolDialContext struct {
context.Context
parent context.Context
}
func (c connPoolDialContext) Deadline() (time.Time, bool) {
return c.parent.Deadline()
}
func (c connPoolDialContext) Value(key any) any {
return c.parent.Value(key)
}
func NewConnPool[T comparable](options ConnPoolOptions[T]) *ConnPool[T] {
return &ConnPool[T]{
options: options,
state: newConnPoolState[T](options.Mode),
}
}
func newConnPoolState[T comparable](mode ConnPoolMode) *connPoolState[T] {
ctx, cancel := context.WithCancelCause(context.Background())
state := &connPoolState[T]{
ctx: ctx,
cancel: cancel,
all: make(map[T]struct{}),
}
if mode == ConnPoolOrdered {
state.idleElements = make(map[T]*list.Element[T])
}
return state
}
func (p *ConnPool[T]) Acquire(ctx context.Context, dial func(context.Context) (T, error)) (T, bool, error) {
switch p.options.Mode {
case ConnPoolSingle:
conn, _, created, err := p.acquireShared(ctx, dial)
return conn, created, err
case ConnPoolOrdered:
return p.acquireOrdered(ctx, dial)
default:
var zero T
return zero, false, net.ErrClosed
}
}
func (p *ConnPool[T]) AcquireShared(ctx context.Context, dial func(context.Context) (T, error)) (T, context.Context, bool, error) {
if p.options.Mode != ConnPoolSingle {
var zero T
return zero, nil, false, net.ErrClosed
}
return p.acquireShared(ctx, dial)
}
func (p *ConnPool[T]) Release(conn T, reuse bool) {
var (
closeConn bool
closeErr error
)
p.access.Lock()
if p.closed || p.state == nil {
closeConn = true
closeErr = net.ErrClosed
p.access.Unlock()
if closeConn {
p.options.Close(conn, closeErr)
}
return
}
currentState := p.state
_, tracked := currentState.all[conn]
if !tracked {
closeConn = true
closeErr = p.closeCause(currentState)
p.access.Unlock()
if closeConn {
p.options.Close(conn, closeErr)
}
return
}
if !reuse || !p.options.IsAlive(conn) {
delete(currentState.all, conn)
switch p.options.Mode {
case ConnPoolSingle:
if currentState.hasShared && currentState.shared == conn {
var zero T
currentState.shared = zero
currentState.hasShared = false
currentState.sharedClaimed = false
currentState.sharedCtx = nil
if currentState.sharedCancel != nil {
currentState.sharedCancel(net.ErrClosed)
currentState.sharedCancel = nil
}
}
case ConnPoolOrdered:
if element, loaded := currentState.idleElements[conn]; loaded {
currentState.idle.Remove(element)
delete(currentState.idleElements, conn)
}
}
closeConn = true
closeErr = net.ErrClosed
p.access.Unlock()
if closeConn {
p.options.Close(conn, closeErr)
}
return
}
if p.options.Mode == ConnPoolOrdered {
if _, loaded := currentState.idleElements[conn]; !loaded {
currentState.idleElements[conn] = currentState.idle.PushBack(conn)
}
}
p.access.Unlock()
}
func (p *ConnPool[T]) Invalidate(conn T, cause error) {
p.access.Lock()
if p.closed || p.state == nil {
p.access.Unlock()
p.options.Close(conn, cause)
return
}
currentState := p.state
_, tracked := currentState.all[conn]
if !tracked {
p.access.Unlock()
return
}
delete(currentState.all, conn)
switch p.options.Mode {
case ConnPoolSingle:
if currentState.hasShared && currentState.shared == conn {
var zero T
currentState.shared = zero
currentState.hasShared = false
currentState.sharedClaimed = false
currentState.sharedCtx = nil
if currentState.sharedCancel != nil {
currentState.sharedCancel(cause)
currentState.sharedCancel = nil
}
}
case ConnPoolOrdered:
if element, loaded := currentState.idleElements[conn]; loaded {
currentState.idle.Remove(element)
delete(currentState.idleElements, conn)
}
}
p.access.Unlock()
p.options.Close(conn, cause)
}
func (p *ConnPool[T]) Reset() {
p.access.Lock()
if p.closed {
p.access.Unlock()
return
}
oldState := p.state
p.state = newConnPoolState[T](p.options.Mode)
p.access.Unlock()
p.closeState(oldState, net.ErrClosed)
}
func (p *ConnPool[T]) Close() error {
p.access.Lock()
if p.closed {
p.access.Unlock()
return nil
}
p.closed = true
oldState := p.state
p.state = nil
p.access.Unlock()
p.closeState(oldState, net.ErrClosed)
return nil
}
func (p *ConnPool[T]) acquireOrdered(ctx context.Context, dial func(context.Context) (T, error)) (T, bool, error) {
var zero T
for {
var (
staleConn T
hasStale bool
)
p.access.Lock()
if p.closed {
p.access.Unlock()
return zero, false, net.ErrClosed
}
currentState := p.state
if element := currentState.idle.Front(); element != nil {
conn := currentState.idle.Remove(element)
delete(currentState.idleElements, conn)
if p.options.IsAlive(conn) {
p.access.Unlock()
return conn, false, nil
}
delete(currentState.all, conn)
staleConn = conn
hasStale = true
}
p.access.Unlock()
if hasStale {
p.options.Close(staleConn, net.ErrClosed)
continue
}
conn, err := p.dial(ctx, currentState, dial)
if err != nil {
return zero, false, err
}
p.access.Lock()
if p.closed {
p.access.Unlock()
p.options.Close(conn, net.ErrClosed)
return zero, false, net.ErrClosed
}
if p.state != currentState {
cause := p.closeCause(currentState)
p.access.Unlock()
p.options.Close(conn, cause)
return zero, false, cause
}
currentState.all[conn] = struct{}{}
p.access.Unlock()
return conn, true, nil
}
}
func (p *ConnPool[T]) acquireShared(ctx context.Context, dial func(context.Context) (T, error)) (T, context.Context, bool, error) {
var zero T
for {
var (
staleConn T
hasStale bool
state *connPoolConnect[T]
current *connPoolState[T]
startDial bool
)
p.access.Lock()
if p.closed {
p.access.Unlock()
return zero, nil, false, net.ErrClosed
}
current = p.state
if current.hasShared {
conn := current.shared
if p.options.IsAlive(conn) {
created := !current.sharedClaimed
current.sharedClaimed = true
connCtx := current.sharedCtx
p.access.Unlock()
return conn, connCtx, created, nil
}
delete(current.all, conn)
var zeroConn T
current.shared = zeroConn
current.hasShared = false
current.sharedClaimed = false
current.sharedCtx = nil
if current.sharedCancel != nil {
current.sharedCancel(net.ErrClosed)
current.sharedCancel = nil
}
staleConn = conn
hasStale = true
p.access.Unlock()
p.options.Close(staleConn, net.ErrClosed)
continue
}
if current.connecting == nil {
current.connecting = &connPoolConnect[T]{
done: make(chan struct{}),
}
startDial = true
}
state = current.connecting
p.access.Unlock()
if hasStale {
continue
}
if startDial {
go p.connectSingle(current, state, ctx, dial)
}
select {
case <-state.done:
conn, connCtx, created, retry, err := p.collectShared(current, state, startDial)
if retry {
continue
}
return conn, connCtx, created, err
case <-ctx.Done():
return zero, nil, false, ctx.Err()
case <-current.ctx.Done():
p.access.Lock()
closed := p.closed
p.access.Unlock()
if closed {
return zero, nil, false, net.ErrClosed
}
}
}
}
func (p *ConnPool[T]) connectSingle(current *connPoolState[T], state *connPoolConnect[T], ctx context.Context, dial func(context.Context) (T, error)) {
conn, err := p.dial(ctx, current, dial)
if err != nil {
p.access.Lock()
if current.connecting == state {
current.connecting = nil
}
state.err = err
p.access.Unlock()
close(state.done)
return
}
var closeErr error
p.access.Lock()
if current.connecting == state {
current.connecting = nil
}
if p.closed {
closeErr = net.ErrClosed
state.err = closeErr
} else if p.state != current {
closeErr = p.closeCause(current)
state.err = closeErr
} else {
sharedCtx, sharedCancel := context.WithCancelCause(current.ctx)
current.shared = conn
current.hasShared = true
current.sharedClaimed = false
current.sharedCtx = sharedCtx
current.sharedCancel = sharedCancel
current.all[conn] = struct{}{}
}
p.access.Unlock()
if closeErr != nil {
p.options.Close(conn, closeErr)
}
close(state.done)
}
func (p *ConnPool[T]) collectShared(current *connPoolState[T], state *connPoolConnect[T], startDial bool) (T, context.Context, bool, bool, error) {
var zero T
p.access.Lock()
if state.err != nil {
err := state.err
p.access.Unlock()
if startDial {
return zero, nil, false, false, err
}
return zero, nil, false, true, nil
}
if p.closed {
p.access.Unlock()
return zero, nil, false, false, net.ErrClosed
}
if p.state != current {
cause := p.closeCause(current)
p.access.Unlock()
return zero, nil, false, false, cause
}
if !current.hasShared {
p.access.Unlock()
return zero, nil, false, true, nil
}
conn := current.shared
if !p.options.IsAlive(conn) {
delete(current.all, conn)
var zeroConn T
current.shared = zeroConn
current.hasShared = false
current.sharedClaimed = false
current.sharedCtx = nil
if current.sharedCancel != nil {
current.sharedCancel(net.ErrClosed)
current.sharedCancel = nil
}
p.access.Unlock()
p.options.Close(conn, net.ErrClosed)
return zero, nil, false, true, nil
}
created := !current.sharedClaimed
current.sharedClaimed = true
connCtx := current.sharedCtx
p.access.Unlock()
return conn, connCtx, created, false, nil
}
func (p *ConnPool[T]) dial(ctx context.Context, current *connPoolState[T], dial func(context.Context) (T, error)) (T, error) {
var zero T
if err := ctx.Err(); err != nil {
return zero, err
}
if cause := context.Cause(current.ctx); cause != nil {
return zero, cause
}
dialCtx, cancel := context.WithCancelCause(current.ctx)
var (
stateAccess sync.Mutex
dialComplete bool
)
stopCancel := context.AfterFunc(ctx, func() {
stateAccess.Lock()
if !dialComplete {
cancel(context.Cause(ctx))
}
stateAccess.Unlock()
})
select {
case <-ctx.Done():
stateAccess.Lock()
dialComplete = true
stateAccess.Unlock()
stopCancel()
cancel(context.Cause(ctx))
return zero, ctx.Err()
default:
}
conn, err := dial(connPoolDialContext{
Context: dialCtx,
parent: ctx,
})
stateAccess.Lock()
dialComplete = true
stateAccess.Unlock()
stopCancel()
if err != nil {
if cause := context.Cause(dialCtx); cause != nil {
return zero, cause
}
return zero, err
}
if cause := context.Cause(dialCtx); cause != nil {
p.options.Close(conn, cause)
return zero, cause
}
return conn, nil
}
func (p *ConnPool[T]) closeState(state *connPoolState[T], cause error) {
if state == nil {
return
}
state.cancel(cause)
if state.sharedCancel != nil {
state.sharedCancel(cause)
}
for conn := range state.all {
p.options.Close(conn, cause)
}
}
func (p *ConnPool[T]) closeCause(state *connPoolState[T]) error {
_ = state
return net.ErrClosed
}

View File

@@ -1,321 +0,0 @@
package transport
import (
"context"
"net"
"sync"
"time"
E "github.com/sagernet/sing/common/exceptions"
)
type ConnectorCallbacks[T any] struct {
IsClosed func(connection T) bool
Close func(connection T)
Reset func(connection T)
}
type Connector[T any] struct {
dial func(ctx context.Context) (T, error)
callbacks ConnectorCallbacks[T]
access sync.Mutex
connection T
hasConnection bool
connectionCancel context.CancelFunc
connecting chan struct{}
closeCtx context.Context
closed bool
}
func NewConnector[T any](closeCtx context.Context, dial func(context.Context) (T, error), callbacks ConnectorCallbacks[T]) *Connector[T] {
return &Connector[T]{
dial: dial,
callbacks: callbacks,
closeCtx: closeCtx,
}
}
func NewSingleflightConnector(closeCtx context.Context, dial func(context.Context) (*Connection, error)) *Connector[*Connection] {
return NewConnector(closeCtx, dial, ConnectorCallbacks[*Connection]{
IsClosed: func(connection *Connection) bool {
return connection.IsClosed()
},
Close: func(connection *Connection) {
connection.CloseWithError(ErrTransportClosed)
},
Reset: func(connection *Connection) {
connection.CloseWithError(ErrConnectionReset)
},
})
}
type contextKeyConnecting struct{}
var errRecursiveConnectorDial = E.New("recursive connector dial")
type connectorDialResult[T any] struct {
connection T
cancel context.CancelFunc
err error
}
func (c *Connector[T]) Get(ctx context.Context) (T, error) {
var zero T
for {
c.access.Lock()
if c.closed {
c.access.Unlock()
return zero, ErrTransportClosed
}
if c.hasConnection && !c.callbacks.IsClosed(c.connection) {
connection := c.connection
c.access.Unlock()
return connection, nil
}
c.hasConnection = false
if c.connectionCancel != nil {
c.connectionCancel()
c.connectionCancel = nil
}
if isRecursiveConnectorDial(ctx, c) {
c.access.Unlock()
return zero, errRecursiveConnectorDial
}
if c.connecting != nil {
connecting := c.connecting
c.access.Unlock()
select {
case <-connecting:
continue
case <-ctx.Done():
return zero, ctx.Err()
case <-c.closeCtx.Done():
return zero, ErrTransportClosed
}
}
if err := ctx.Err(); err != nil {
c.access.Unlock()
return zero, err
}
connecting := make(chan struct{})
c.connecting = connecting
dialContext := context.WithValue(ctx, contextKeyConnecting{}, c)
dialResult := make(chan connectorDialResult[T], 1)
c.access.Unlock()
go func() {
connection, cancel, err := c.dialWithCancellation(dialContext)
dialResult <- connectorDialResult[T]{
connection: connection,
cancel: cancel,
err: err,
}
}()
select {
case result := <-dialResult:
return c.completeDial(ctx, connecting, result)
case <-ctx.Done():
go func() {
result := <-dialResult
_, _ = c.completeDial(ctx, connecting, result)
}()
return zero, ctx.Err()
case <-c.closeCtx.Done():
go func() {
result := <-dialResult
_, _ = c.completeDial(ctx, connecting, result)
}()
return zero, ErrTransportClosed
}
}
}
func isRecursiveConnectorDial[T any](ctx context.Context, connector *Connector[T]) bool {
dialConnector, loaded := ctx.Value(contextKeyConnecting{}).(*Connector[T])
return loaded && dialConnector == connector
}
func (c *Connector[T]) completeDial(ctx context.Context, connecting chan struct{}, result connectorDialResult[T]) (T, error) {
var zero T
c.access.Lock()
defer c.access.Unlock()
defer func() {
if c.connecting == connecting {
c.connecting = nil
}
close(connecting)
}()
if result.err != nil {
return zero, result.err
}
if c.closed || c.closeCtx.Err() != nil {
result.cancel()
c.callbacks.Close(result.connection)
return zero, ErrTransportClosed
}
if err := ctx.Err(); err != nil {
result.cancel()
c.callbacks.Close(result.connection)
return zero, err
}
c.connection = result.connection
c.hasConnection = true
c.connectionCancel = result.cancel
return c.connection, nil
}
func (c *Connector[T]) dialWithCancellation(ctx context.Context) (T, context.CancelFunc, error) {
var zero T
if err := ctx.Err(); err != nil {
return zero, nil, err
}
connCtx, cancel := context.WithCancel(c.closeCtx)
var (
stateAccess sync.Mutex
dialComplete bool
)
stopCancel := context.AfterFunc(ctx, func() {
stateAccess.Lock()
if !dialComplete {
cancel()
}
stateAccess.Unlock()
})
select {
case <-ctx.Done():
stateAccess.Lock()
dialComplete = true
stateAccess.Unlock()
stopCancel()
cancel()
return zero, nil, ctx.Err()
default:
}
connection, err := c.dial(valueContext{connCtx, ctx})
stateAccess.Lock()
dialComplete = true
stateAccess.Unlock()
stopCancel()
if err != nil {
cancel()
return zero, nil, err
}
return connection, cancel, nil
}
type valueContext struct {
context.Context
parent context.Context
}
func (v valueContext) Value(key any) any {
return v.parent.Value(key)
}
func (v valueContext) Deadline() (time.Time, bool) {
return v.parent.Deadline()
}
func (c *Connector[T]) Close() error {
c.access.Lock()
defer c.access.Unlock()
if c.closed {
return nil
}
c.closed = true
if c.connectionCancel != nil {
c.connectionCancel()
c.connectionCancel = nil
}
if c.hasConnection {
c.callbacks.Close(c.connection)
c.hasConnection = false
}
return nil
}
func (c *Connector[T]) Reset() {
c.access.Lock()
defer c.access.Unlock()
if c.connectionCancel != nil {
c.connectionCancel()
c.connectionCancel = nil
}
if c.hasConnection {
c.callbacks.Reset(c.connection)
c.hasConnection = false
}
}
type Connection struct {
net.Conn
closeOnce sync.Once
done chan struct{}
closeError error
}
func WrapConnection(conn net.Conn) *Connection {
return &Connection{
Conn: conn,
done: make(chan struct{}),
}
}
func (c *Connection) Done() <-chan struct{} {
return c.done
}
func (c *Connection) IsClosed() bool {
select {
case <-c.done:
return true
default:
return false
}
}
func (c *Connection) CloseError() error {
select {
case <-c.done:
if c.closeError != nil {
return c.closeError
}
return ErrTransportClosed
default:
return nil
}
}
func (c *Connection) Close() error {
return c.CloseWithError(ErrTransportClosed)
}
func (c *Connection) CloseWithError(err error) error {
var returnError error
c.closeOnce.Do(func() {
c.closeError = err
returnError = c.Conn.Close()
close(c.done)
})
return returnError
}

View File

@@ -1,407 +0,0 @@
package transport
import (
"context"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/require"
)
type testConnectorConnection struct{}
func TestConnectorRecursiveGetFailsFast(t *testing.T) {
t.Parallel()
var (
dialCount atomic.Int32
closeCount atomic.Int32
connector *Connector[*testConnectorConnection]
)
dial := func(ctx context.Context) (*testConnectorConnection, error) {
dialCount.Add(1)
_, err := connector.Get(ctx)
if err != nil {
return nil, err
}
return &testConnectorConnection{}, nil
}
connector = NewConnector(context.Background(), dial, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {
closeCount.Add(1)
},
Reset: func(connection *testConnectorConnection) {
closeCount.Add(1)
},
})
_, err := connector.Get(context.Background())
require.ErrorIs(t, err, errRecursiveConnectorDial)
require.EqualValues(t, 1, dialCount.Load())
require.EqualValues(t, 0, closeCount.Load())
}
func TestConnectorRecursiveGetAcrossConnectorsAllowed(t *testing.T) {
t.Parallel()
var (
outerDialCount atomic.Int32
innerDialCount atomic.Int32
outerConnector *Connector[*testConnectorConnection]
innerConnector *Connector[*testConnectorConnection]
)
innerConnector = NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
innerDialCount.Add(1)
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {},
Reset: func(connection *testConnectorConnection) {},
})
outerConnector = NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
outerDialCount.Add(1)
_, err := innerConnector.Get(ctx)
if err != nil {
return nil, err
}
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {},
Reset: func(connection *testConnectorConnection) {},
})
_, err := outerConnector.Get(context.Background())
require.NoError(t, err)
require.EqualValues(t, 1, outerDialCount.Load())
require.EqualValues(t, 1, innerDialCount.Load())
}
func TestConnectorDialContextPreservesValueAndDeadline(t *testing.T) {
t.Parallel()
type contextKey struct{}
var (
dialValue any
dialDeadline time.Time
dialHasDeadline bool
)
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
dialValue = ctx.Value(contextKey{})
dialDeadline, dialHasDeadline = ctx.Deadline()
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {},
Reset: func(connection *testConnectorConnection) {},
})
deadline := time.Now().Add(time.Minute)
requestContext, cancel := context.WithDeadline(context.WithValue(context.Background(), contextKey{}, "test-value"), deadline)
defer cancel()
_, err := connector.Get(requestContext)
require.NoError(t, err)
require.Equal(t, "test-value", dialValue)
require.True(t, dialHasDeadline)
require.WithinDuration(t, deadline, dialDeadline, time.Second)
}
func TestConnectorDialSkipsCanceledRequest(t *testing.T) {
t.Parallel()
var dialCount atomic.Int32
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
dialCount.Add(1)
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {},
Reset: func(connection *testConnectorConnection) {},
})
requestContext, cancel := context.WithCancel(context.Background())
cancel()
_, err := connector.Get(requestContext)
require.ErrorIs(t, err, context.Canceled)
require.EqualValues(t, 0, dialCount.Load())
}
func TestConnectorCanceledRequestDoesNotCacheConnection(t *testing.T) {
t.Parallel()
var (
dialCount atomic.Int32
closeCount atomic.Int32
)
dialStarted := make(chan struct{}, 1)
releaseDial := make(chan struct{})
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
dialCount.Add(1)
select {
case dialStarted <- struct{}{}:
default:
}
<-releaseDial
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {
closeCount.Add(1)
},
Reset: func(connection *testConnectorConnection) {},
})
requestContext, cancel := context.WithCancel(context.Background())
result := make(chan error, 1)
go func() {
_, err := connector.Get(requestContext)
result <- err
}()
<-dialStarted
cancel()
close(releaseDial)
err := <-result
require.ErrorIs(t, err, context.Canceled)
require.EqualValues(t, 1, dialCount.Load())
require.Eventually(t, func() bool {
return closeCount.Load() == 1
}, time.Second, 10*time.Millisecond)
_, err = connector.Get(context.Background())
require.NoError(t, err)
require.EqualValues(t, 2, dialCount.Load())
}
func TestConnectorCanceledRequestReturnsBeforeIgnoredDialCompletes(t *testing.T) {
t.Parallel()
var (
dialCount atomic.Int32
closeCount atomic.Int32
)
dialStarted := make(chan struct{}, 1)
releaseDial := make(chan struct{})
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
dialCount.Add(1)
select {
case dialStarted <- struct{}{}:
default:
}
<-releaseDial
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {
closeCount.Add(1)
},
Reset: func(connection *testConnectorConnection) {},
})
requestContext, cancel := context.WithCancel(context.Background())
result := make(chan error, 1)
go func() {
_, err := connector.Get(requestContext)
result <- err
}()
<-dialStarted
cancel()
select {
case err := <-result:
require.ErrorIs(t, err, context.Canceled)
case <-time.After(time.Second):
t.Fatal("Get did not return after request cancel")
}
require.EqualValues(t, 1, dialCount.Load())
require.EqualValues(t, 0, closeCount.Load())
close(releaseDial)
require.Eventually(t, func() bool {
return closeCount.Load() == 1
}, time.Second, 10*time.Millisecond)
_, err := connector.Get(context.Background())
require.NoError(t, err)
require.EqualValues(t, 2, dialCount.Load())
}
func TestConnectorWaiterDoesNotStartNewDialBeforeCanceledDialCompletes(t *testing.T) {
t.Parallel()
var (
dialCount atomic.Int32
closeCount atomic.Int32
)
firstDialStarted := make(chan struct{}, 1)
secondDialStarted := make(chan struct{}, 1)
releaseFirstDial := make(chan struct{})
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
attempt := dialCount.Add(1)
switch attempt {
case 1:
select {
case firstDialStarted <- struct{}{}:
default:
}
<-releaseFirstDial
case 2:
select {
case secondDialStarted <- struct{}{}:
default:
}
}
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {
closeCount.Add(1)
},
Reset: func(connection *testConnectorConnection) {},
})
requestContext, cancel := context.WithCancel(context.Background())
firstResult := make(chan error, 1)
go func() {
_, err := connector.Get(requestContext)
firstResult <- err
}()
<-firstDialStarted
cancel()
secondResult := make(chan error, 1)
go func() {
_, err := connector.Get(context.Background())
secondResult <- err
}()
select {
case <-secondDialStarted:
t.Fatal("second dial started before first dial completed")
case <-time.After(100 * time.Millisecond):
}
select {
case err := <-firstResult:
require.ErrorIs(t, err, context.Canceled)
case <-time.After(time.Second):
t.Fatal("first Get did not return after request cancel")
}
close(releaseFirstDial)
require.Eventually(t, func() bool {
return closeCount.Load() == 1
}, time.Second, 10*time.Millisecond)
select {
case <-secondDialStarted:
case <-time.After(time.Second):
t.Fatal("second dial did not start after first dial completed")
}
err := <-secondResult
require.NoError(t, err)
require.EqualValues(t, 2, dialCount.Load())
}
func TestConnectorDialContextNotCanceledByRequestContextAfterDial(t *testing.T) {
t.Parallel()
var dialContext context.Context
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
dialContext = ctx
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {},
Reset: func(connection *testConnectorConnection) {},
})
requestContext, cancel := context.WithCancel(context.Background())
_, err := connector.Get(requestContext)
require.NoError(t, err)
require.NotNil(t, dialContext)
cancel()
select {
case <-dialContext.Done():
t.Fatal("dial context canceled by request context after successful dial")
case <-time.After(100 * time.Millisecond):
}
err = connector.Close()
require.NoError(t, err)
}
func TestConnectorDialContextCanceledOnClose(t *testing.T) {
t.Parallel()
var dialContext context.Context
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
dialContext = ctx
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {},
Reset: func(connection *testConnectorConnection) {},
})
_, err := connector.Get(context.Background())
require.NoError(t, err)
require.NotNil(t, dialContext)
select {
case <-dialContext.Done():
t.Fatal("dial context canceled before connector close")
default:
}
err = connector.Close()
require.NoError(t, err)
select {
case <-dialContext.Done():
case <-time.After(time.Second):
t.Fatal("dial context not canceled after connector close")
}
}

View File

@@ -31,14 +31,13 @@ func RegisterTransport(registry *dns.TransportRegistry) {
} }
type Transport struct { type Transport struct {
*transport.BaseTransport dns.TransportAdapter
ctx context.Context
dialer N.Dialer dialer N.Dialer
serverAddr M.Socksaddr serverAddr M.Socksaddr
tlsConfig tls.Config tlsConfig tls.Config
connector *transport.Connector[*quic.Conn] connection *transport.ConnPool[*quic.Conn]
} }
func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) { func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) {
@@ -63,93 +62,76 @@ func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options
return nil, E.New("invalid server address: ", serverAddr) return nil, E.New("invalid server address: ", serverAddr)
} }
t := &Transport{ return &Transport{
BaseTransport: transport.NewBaseTransport( TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions),
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions), dialer: transportDialer,
logger, serverAddr: serverAddr,
), tlsConfig: tlsConfig,
ctx: ctx, connection: transport.NewConnPool(transport.ConnPoolOptions[*quic.Conn]{
dialer: transportDialer, Mode: transport.ConnPoolSingle,
serverAddr: serverAddr, IsAlive: func(conn *quic.Conn) bool {
tlsConfig: tlsConfig, return conn != nil && !common.Done(conn.Context())
} },
Close: func(conn *quic.Conn, _ error) {
t.connector = transport.NewConnector(t.CloseContext(), t.dial, transport.ConnectorCallbacks[*quic.Conn]{ conn.CloseWithError(0, "")
IsClosed: func(connection *quic.Conn) bool { },
return common.Done(connection.Context()) }),
}, }, nil
Close: func(connection *quic.Conn) {
connection.CloseWithError(0, "")
},
Reset: func(connection *quic.Conn) {
connection.CloseWithError(0, "")
},
})
return t, nil
}
func (t *Transport) dial(ctx context.Context) (*quic.Conn, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, E.Cause(err, "dial UDP connection")
}
earlyConnection, err := sQUIC.DialEarly(
ctx,
bufio.NewUnbindPacketConn(conn),
t.serverAddr.UDPAddr(),
t.tlsConfig,
nil,
)
if err != nil {
conn.Close()
return nil, E.Cause(err, "establish QUIC connection")
}
return earlyConnection, nil
} }
func (t *Transport) Start(stage adapter.StartStage) error { func (t *Transport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart { if stage != adapter.StartStateStart {
return nil return nil
} }
err := t.SetStarted()
if err != nil {
return err
}
return dialer.InitializeDetour(t.dialer) return dialer.InitializeDetour(t.dialer)
} }
func (t *Transport) Close() error { func (t *Transport) Close() error {
return E.Errors(t.BaseTransport.Close(), t.connector.Close()) return t.connection.Close()
} }
func (t *Transport) Reset() { func (t *Transport) Reset() {
t.connector.Reset() t.connection.Reset()
} }
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if !t.BeginQuery() {
return nil, transport.ErrTransportClosed
}
defer t.EndQuery()
var ( var (
conn *quic.Conn conn *quic.Conn
err error err error
response *mDNS.Msg response *mDNS.Msg
) )
for i := 0; i < 2; i++ { for i := 0; i < 2; i++ {
conn, err = t.connector.Get(ctx) conn, _, err = t.connection.Acquire(ctx, func(ctx context.Context) (*quic.Conn, error) {
rawConn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, E.Cause(err, "dial UDP connection")
}
earlyConnection, err := sQUIC.DialEarly(
ctx,
bufio.NewUnbindPacketConn(rawConn),
t.serverAddr.UDPAddr(),
t.tlsConfig,
nil,
)
if err != nil {
rawConn.Close()
return nil, E.Cause(err, "establish QUIC connection")
}
return earlyConnection, nil
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
response, err = t.exchange(ctx, message, conn) response, err = t.exchange(ctx, message, conn)
if err == nil { if err == nil {
t.connection.Release(conn, true)
return response, nil return response, nil
} else if !isQUICRetryError(err) { } else if !isQUICRetryError(err) {
t.connection.Release(conn, true)
return nil, err return nil, err
} else { } else {
t.connector.Reset() t.connection.Release(conn, true)
t.Reset()
continue continue
} }
} }

View File

@@ -2,7 +2,6 @@ package transport
import ( import (
"context" "context"
"sync"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -17,7 +16,6 @@ import (
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list"
mDNS "github.com/miekg/dns" mDNS "github.com/miekg/dns"
) )
@@ -29,13 +27,13 @@ func RegisterTLS(registry *dns.TransportRegistry) {
} }
type TLSTransport struct { type TLSTransport struct {
*BaseTransport dns.TransportAdapter
logger logger.ContextLogger
dialer tls.Dialer dialer tls.Dialer
serverAddr M.Socksaddr serverAddr M.Socksaddr
tlsConfig tls.Config tlsConfig tls.Config
access sync.Mutex connections *ConnPool[*tlsDNSConn]
connections list.List[*tlsDNSConn]
} }
type tlsDNSConn struct { type tlsDNSConn struct {
@@ -66,10 +64,20 @@ func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options o
func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr, tlsConfig tls.Config) *TLSTransport { func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr, tlsConfig tls.Config) *TLSTransport {
return &TLSTransport{ return &TLSTransport{
BaseTransport: NewBaseTransport(adapter, logger), TransportAdapter: adapter,
dialer: tls.NewDialer(dialer, tlsConfig), logger: logger,
serverAddr: serverAddr, dialer: tls.NewDialer(dialer, tlsConfig),
tlsConfig: tlsConfig, serverAddr: serverAddr,
tlsConfig: tlsConfig,
connections: NewConnPool(ConnPoolOptions[*tlsDNSConn]{
Mode: ConnPoolOrdered,
IsAlive: func(conn *tlsDNSConn) bool {
return conn != nil
},
Close: func(conn *tlsDNSConn, _ error) {
conn.Close()
},
}),
} }
} }
@@ -77,53 +85,43 @@ func (t *TLSTransport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart { if stage != adapter.StartStateStart {
return nil return nil
} }
err := t.SetStarted()
if err != nil {
return err
}
return dialer.InitializeDetour(t.dialer) return dialer.InitializeDetour(t.dialer)
} }
func (t *TLSTransport) Close() error { func (t *TLSTransport) Close() error {
t.access.Lock() return t.connections.Close()
for connection := t.connections.Front(); connection != nil; connection = connection.Next() {
connection.Value.Close()
}
t.connections.Init()
t.access.Unlock()
return t.BaseTransport.Close()
} }
func (t *TLSTransport) Reset() { func (t *TLSTransport) Reset() {
t.access.Lock() t.connections.Reset()
defer t.access.Unlock()
for connection := t.connections.Front(); connection != nil; connection = connection.Next() {
connection.Value.Close()
}
t.connections.Init()
} }
func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if !t.BeginQuery() { var lastErr error
return nil, ErrTransportClosed for attempt := 0; attempt < 2; attempt++ {
} conn, created, err := t.connections.Acquire(ctx, func(ctx context.Context) (*tlsDNSConn, error) {
defer t.EndQuery() tlsConn, err := t.dialer.DialTLSContext(ctx, t.serverAddr)
if err != nil {
t.access.Lock() return nil, E.Cause(err, "dial TLS connection")
conn := t.connections.PopFront() }
t.access.Unlock() return &tlsDNSConn{Conn: tlsConn}, nil
if conn != nil { })
if err != nil {
return nil, err
}
response, err := t.exchange(ctx, message, conn) response, err := t.exchange(ctx, message, conn)
if err == nil { if err == nil {
t.connections.Release(conn, true)
return response, nil return response, nil
} }
t.Logger.DebugContext(ctx, "discarded pooled connection: ", err) lastErr = err
t.logger.DebugContext(ctx, "discarded pooled connection: ", err)
t.connections.Release(conn, false)
if created {
return nil, err
}
} }
tlsConn, err := t.dialer.DialTLSContext(ctx, t.serverAddr) return nil, lastErr
if err != nil {
return nil, E.Cause(err, "dial TLS connection")
}
return t.exchange(ctx, message, &tlsDNSConn{Conn: tlsConn})
} }
func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg, error) { func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg, error) {
@@ -133,22 +131,12 @@ func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tl
conn.queryId++ conn.queryId++
err := WriteMessage(conn, conn.queryId, message) err := WriteMessage(conn, conn.queryId, message)
if err != nil { if err != nil {
conn.Close()
return nil, E.Cause(err, "write request") return nil, E.Cause(err, "write request")
} }
response, err := ReadMessage(conn) response, err := ReadMessage(conn)
if err != nil { if err != nil {
conn.Close()
return nil, E.Cause(err, "read response") return nil, E.Cause(err, "read response")
} }
t.access.Lock()
if t.State() >= StateClosing {
t.access.Unlock()
conn.Close()
return response, nil
}
conn.SetDeadline(time.Time{}) conn.SetDeadline(time.Time{})
t.connections.PushBack(conn)
t.access.Unlock()
return response, nil return response, nil
} }

View File

@@ -2,6 +2,7 @@ package transport
import ( import (
"context" "context"
"net"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -27,13 +28,14 @@ func RegisterUDP(registry *dns.TransportRegistry) {
} }
type UDPTransport struct { type UDPTransport struct {
*BaseTransport dns.TransportAdapter
logger logger.ContextLogger
dialer N.Dialer dialer N.Dialer
serverAddr M.Socksaddr serverAddr M.Socksaddr
udpSize atomic.Int32 udpSize atomic.Int32
connector *Connector[*Connection] connection *ConnPool[net.Conn]
callbackAccess sync.RWMutex callbackAccess sync.RWMutex
queryId uint16 queryId uint16
@@ -63,43 +65,38 @@ func NewUDP(ctx context.Context, logger log.ContextLogger, tag string, options o
func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialerInstance N.Dialer, serverAddr M.Socksaddr) *UDPTransport { func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialerInstance N.Dialer, serverAddr M.Socksaddr) *UDPTransport {
t := &UDPTransport{ t := &UDPTransport{
BaseTransport: NewBaseTransport(adapter, logger), TransportAdapter: adapter,
dialer: dialerInstance, logger: logger,
serverAddr: serverAddr, dialer: dialerInstance,
callbacks: make(map[uint16]*udpCallback), serverAddr: serverAddr,
callbacks: make(map[uint16]*udpCallback),
connection: NewConnPool(ConnPoolOptions[net.Conn]{
Mode: ConnPoolSingle,
IsAlive: func(conn net.Conn) bool {
return conn != nil
},
Close: func(conn net.Conn, cause error) {
conn.Close()
},
}),
} }
t.udpSize.Store(2048) t.udpSize.Store(2048)
t.connector = NewSingleflightConnector(t.CloseContext(), t.dial)
return t return t
} }
func (t *UDPTransport) dial(ctx context.Context) (*Connection, error) {
rawConn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, E.Cause(err, "dial UDP connection")
}
conn := WrapConnection(rawConn)
go t.recvLoop(conn)
return conn, nil
}
func (t *UDPTransport) Start(stage adapter.StartStage) error { func (t *UDPTransport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart { if stage != adapter.StartStateStart {
return nil return nil
} }
err := t.SetStarted()
if err != nil {
return err
}
return dialer.InitializeDetour(t.dialer) return dialer.InitializeDetour(t.dialer)
} }
func (t *UDPTransport) Close() error { func (t *UDPTransport) Close() error {
return E.Errors(t.BaseTransport.Close(), t.connector.Close()) return t.connection.Close()
} }
func (t *UDPTransport) Reset() { func (t *UDPTransport) Reset() {
t.connector.Reset() t.connection.Reset()
} }
func (t *UDPTransport) nextAvailableQueryId() (uint16, error) { func (t *UDPTransport) nextAvailableQueryId() (uint16, error) {
@@ -116,17 +113,12 @@ func (t *UDPTransport) nextAvailableQueryId() (uint16, error) {
} }
func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if !t.BeginQuery() {
return nil, ErrTransportClosed
}
defer t.EndQuery()
response, err := t.exchange(ctx, message) response, err := t.exchange(ctx, message)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if response.Truncated { if response.Truncated {
t.Logger.InfoContext(ctx, "response truncated, retrying with TCP") t.logger.InfoContext(ctx, "response truncated, retrying with TCP")
return t.exchangeTCP(ctx, message) return t.exchangeTCP(ctx, message)
} }
return response, nil return response, nil
@@ -158,16 +150,25 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
break break
} }
if t.udpSize.CompareAndSwap(current, udpSize) { if t.udpSize.CompareAndSwap(current, udpSize) {
t.connector.Reset() t.Reset()
break break
} }
} }
} }
conn, err := t.connector.Get(ctx) conn, connCtx, created, err := t.connection.AcquireShared(ctx, func(ctx context.Context) (net.Conn, error) {
rawConn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, E.Cause(err, "dial UDP connection")
}
return rawConn, nil
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
if created {
go t.recvLoop(conn)
}
callback := &udpCallback{ callback := &udpCallback{
done: make(chan struct{}), done: make(chan struct{}),
@@ -177,6 +178,7 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
queryId, err := t.nextAvailableQueryId() queryId, err := t.nextAvailableQueryId()
if err != nil { if err != nil {
t.callbackAccess.Unlock() t.callbackAccess.Unlock()
t.connection.Release(conn, true)
return nil, err return nil, err
} }
t.callbacks[queryId] = callback t.callbacks[queryId] = callback
@@ -203,30 +205,30 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
_, err = conn.Write(rawMessage) _, err = conn.Write(rawMessage)
if err != nil { if err != nil {
conn.CloseWithError(err) t.connection.Invalidate(conn, err)
return nil, E.Cause(err, "write request") return nil, E.Cause(err, "write request")
} }
select { select {
case <-callback.done: case <-callback.done:
t.connection.Release(conn, true)
callback.response.Id = originalId callback.response.Id = originalId
return callback.response, nil return callback.response, nil
case <-conn.Done(): case <-connCtx.Done():
return nil, conn.CloseError() return nil, context.Cause(connCtx)
case <-t.CloseContext().Done():
return nil, ErrTransportClosed
case <-ctx.Done(): case <-ctx.Done():
t.connection.Release(conn, true)
return nil, ctx.Err() return nil, ctx.Err()
} }
} }
func (t *UDPTransport) recvLoop(conn *Connection) { func (t *UDPTransport) recvLoop(conn net.Conn) {
for { for {
buffer := buf.NewSize(int(t.udpSize.Load())) buffer := buf.NewSize(int(t.udpSize.Load()))
_, err := buffer.ReadOnceFrom(conn) _, err := buffer.ReadOnceFrom(conn)
if err != nil { if err != nil {
buffer.Release() buffer.Release()
conn.CloseWithError(err) t.connection.Invalidate(conn, err)
return return
} }
@@ -234,7 +236,7 @@ func (t *UDPTransport) recvLoop(conn *Connection) {
err = message.Unpack(buffer.Bytes()) err = message.Unpack(buffer.Bytes())
buffer.Release() buffer.Release()
if err != nil { if err != nil {
t.Logger.Debug("discarded malformed UDP response: ", err) t.logger.Debug("discarded malformed UDP response: ", err)
continue continue
} }

View File

@@ -2,6 +2,11 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
#### 1.13.11
* Fix process searcher failure introduced in 1.13.9
* Fixes and improvements
#### 1.13.10 #### 1.13.10
* Fix process searcher failure introduced in 1.13.9 * Fix process searcher failure introduced in 1.13.9

View File

@@ -44,8 +44,8 @@
"uuid": "257f20d0-294a-4f07-9f2c-9efee9a37400" "uuid": "257f20d0-294a-4f07-9f2c-9efee9a37400"
}, },
{ {
"type": "failover", "type": "fallback",
"tag": "failover-out", "tag": "fallback-out",
"outbounds": [ "outbounds": [
"vless-1-out", "vless-1-out",
"vless-2-out", "vless-2-out",
@@ -54,7 +54,7 @@
} }
], ],
"route": { "route": {
"final": "failover-out", "final": "fallback-out",
"default_domain_resolver": "default", "default_domain_resolver": "default",
"auto_detect_interface": true "auto_detect_interface": true
} }

View File

@@ -15,22 +15,14 @@
{ {
"type": "direct", "type": "direct",
"tag": "direct-out" "tag": "direct-out"
},
{
"type": "dns",
"tag": "dns-out"
} }
], ],
"route": { "route": {
"rules": [ "rules": [
{ {
"protocol": "dns", "protocol": "dns",
"outbound": "dns-out" "action": "hijack-dns"
}, }
{
"port": 53,
"outbound": "dns-out"
},
], ],
"final": "direct-out" "final": "direct-out"
}, },

View File

@@ -26,10 +26,6 @@
"type": "direct", "type": "direct",
"tag": "direct-out" "tag": "direct-out"
}, },
{
"type": "dns",
"tag": "dns-out"
},
{ {
"type": "bandwidth-limiter", "type": "bandwidth-limiter",
"tag": "bandwidth-limiter", "tag": "bandwidth-limiter",
@@ -51,11 +47,7 @@
"rules": [ "rules": [
{ {
"protocol": "dns", "protocol": "dns",
"outbound": "dns-out" "action": "hijack-dns"
},
{
"port": 53,
"outbound": "dns-out"
} }
], ],
"final": "connection-limiter" "final": "connection-limiter"

View File

@@ -0,0 +1,58 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "mixed",
"tag": "mixed-in",
"listen_port": 7897
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "masque",
"tag": "masque-out",
"use_http2": false,
"use_ipv6": false,
"profile": {
"detour": "direct",
// For getting existing MASQUE device profile, else sing-box will create new profile
"id": "",
"auth_token": ""
},
"udp_timeout": "5m0s",
"udp_keepalive_period": "30s",
"udp_initial_packet_size": 0,
"reconnect_delay": "5s",
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#fields
"insecure": false,
"cipher_suites": [],
"curve_preferences": [],
"fragment": false,
"fragment_fallback_delay": "",
"record_fragment": false,
"kernel_tx": false,
"kernel_rx": false,
}
// Dial Fields
}
],
"route": {
"final": "masque-out",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}

View File

@@ -0,0 +1,83 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "mtproxy",
// https://sing-box.sagernet.org/configuration/shared/listen/
"listen": "0.0.0.0",
"listen_port": 3128,
"users": [
{
"name": "user1",
"secret": "7hBO-dCS4EBzenlKbdLFxyNnb29nbGUuY29t"
}
],
// concurrency is a size of the worker pool for connection management.
"concurrency": 8192,
// domain_fronting_port is a port we use to connect to a fronting domain.
"domain_fronting_port": 443,
// domain_fronting_ip is an IP address to use when connecting to the fronting
// domain instead of resolving the hostname from the secret via DNS.
"domain_fronting_ip": "",
// domain_fronting_proxy_protocol is used if communication between upstream
// endpoint and sing-box supports proxy protocol.
"domain_fronting_proxy_protocol": false,
// prefer_ip defines an IP connectivity preference. Valid values are:
// 'prefer-ipv4', 'prefer-ipv6', 'only-ipv4', 'only-ipv6'.
"prefer_ip": "prefer-ipv4",
// auto_update defines if it is required to auto update proxy list from
// Telegram instead of relying on a hardcoded list.
"auto_update": false,
// allow_fallback_on_unknown_dc defines how proxy behaves if unknown DC was
// requested. If this setting is set to false, then such connection will be
// rejected. Otherwise, proxy will chose any DC.
"allow_fallback_on_unknown_dc": false,
// tolerate_time_skewness is a time boundary that defines a time range where
// faketls timestamp is acceptable.
"tolerate_time_skewness": "",
// idle_timeout is a timeout for relay when we have to break a stream.
"idle_timeout": "5m",
// handshake_timeout is a timeout during which all handshake ceremonies must
// be completed, otherwise this process will be aborted
"handshake_timeout": "10s",
// doppelganger_urls is a list of URLs that should be crawled by
// sing-box to calculate parameters for statistical distribution of a
// traffic for fronting domains.
"doppelganger_urls": [],
// doppelganger_per_raid defines how many time each URL from
// doppelganger_urls list should be crawled per raid.
"doppelganger_per_raid": 10,
// doppelganger_each defines a time period between each raid. We recommend
// to use hours here.
"doppelganger_each": "6h",
// doppelganger_drs defines if TLS Dynamic Record Sizing is active.
"doppelganger_drs": false,
// throttle_max_connections is the total connection limit.
"throttle_max_connections": 0,
// throttle_check_interval is how often the throttle recomputes per-user
// caps.
"throttle_check_interval": "5s"
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
}
],
"route": {
"final": "direct",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}

View File

@@ -0,0 +1,37 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"inbounds": [
{
"type": "mixed",
"tag": "mixed-in",
"listen_port": 7897
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "parser",
"tag": "vless-out",
// Supported protocols: hysteria, hysteria2, shadowsocks, trojan, tuic, vless, vmess
"link": "vless://b5e41c8c-c437-4689-b863-76208a3efb4b@0.0.0.0:443?..."
}
],
"route": {
"final": "vless-out",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}

View File

@@ -1,49 +0,0 @@
{
"log": {
"level": "info"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"endpoints": [
{
"type": "tunnel-server",
"tag": "tunnel",
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13",
"users": [
{
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe"
}
],
"inbound": {
"type": "vless",
"tag": "vless-in",
"listen": "0.0.0.0",
"listen_port": 8000,
"users": [
{
"name": "vless",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
}
]
}
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct-out"
}
],
"route": {
"final": "direct-out",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}

View File

@@ -1,66 +0,0 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"endpoints": [
{
"type": "tunnel-server",
"tag": "tunnel",
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13",
"users": [
{
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe"
},
{
"uuid": "487f6073-3300-4819-a07d-39652e45fb4d",
"key": "3d74d616-2502-4c17-9cc3-92c366550f4f"
}
],
"inbound": {
"type": "vless",
"tag": "vless-in",
"listen": "0.0.0.0",
"listen_port": 8000,
"users": [
{
"name": "vless",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
}
]
}
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct-out"
}
],
"route": {
"rules": [
{
"tunnel_source": [
"9b65b7e1-04c8-4717-8f45-2aa61fd25937",
"487f6073-3300-4819-a07d-39652e45fb4d"
],
"tunnel_destination": [
"9b65b7e1-04c8-4717-8f45-2aa61fd25937",
"487f6073-3300-4819-a07d-39652e45fb4d"
],
"outbound": "tunnel"
}
],
"final": "direct-out",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}

View File

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

View File

@@ -0,0 +1,51 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"endpoints": [
{
"type": "vpn-server",
"tag": "vpn",
"address": "10.0.0.1",
"users": [
{
"address": "10.0.0.2",
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe"
}
],
"inbounds": [
{
"type": "vless",
"tag": "vless-in",
"listen": "0.0.0.0",
"listen_port": 8000,
"users": [
{
"name": "vless",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
}
]
}
]
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct-out"
}
],
"route": {
"final": "direct-out",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}

View File

@@ -12,9 +12,9 @@
}, },
"endpoints": [ "endpoints": [
{ {
"type": "tunnel-client", "type": "vpn-client",
"tag": "tunnel", "tag": "vpn",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937", "address": "10.0.0.2",
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe", "key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe",
"outbound": { "outbound": {
"type": "vless", "type": "vless",
@@ -42,8 +42,8 @@
"route": { "route": {
"rules": [ "rules": [
{ {
"outbound": "tunnel", "outbound": "vpn",
"override_tunnel_destination": "487f6073-3300-4819-a07d-39652e45fb4d" "override_gateway": "10.0.0.3"
} }
], ],
"final": "direct-out", "final": "direct-out",

View File

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

View File

@@ -0,0 +1,61 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"type": "local",
"tag": "default"
}
]
},
"endpoints": [
{
"type": "vpn-server",
"tag": "vpn",
"address": "10.0.0.1",
"users": [
{
"address": "10.0.0.2",
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe"
},
{
"address": "10.0.0.3",
"key": "3d74d616-2502-4c17-9cc3-92c366550f4f"
}
],
"inbounds": [
{
"type": "vless",
"tag": "vless-in",
"listen": "0.0.0.0",
"listen_port": 8000,
"users": [
{
"name": "vless",
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
}
]
}
]
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct-out"
}
],
"route": {
"rules": [
{
"source_ip_cidr": "10.0.0.0/24",
"outbound": "vpn"
}
],
"final": "direct-out",
"default_domain_resolver": "default",
"auto_detect_interface": true
}
}

View File

@@ -29,10 +29,6 @@
"server_port": 8000, "server_port": 8000,
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937", "uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
"network": "tcp" "network": "tcp"
},
{
"type": "dns",
"tag": "dns-out"
} }
], ],
"route": { "route": {

View File

@@ -12,12 +12,12 @@
}, },
"endpoints": [ "endpoints": [
{ {
"type": "tunnel-server", "type": "vpn-server",
"tag": "tunnel", "tag": "vpn",
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13", "address": "10.0.0.1",
"users": [ "users": [
{ {
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937", "address": "10.0.0.2",
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe" "key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe"
} }
], ],
@@ -45,8 +45,8 @@
"rules": [ "rules": [
{ {
"inbound": "vless-in", "inbound": "vless-in",
"outbound": "tunnel", "outbound": "vpn",
"override_tunnel_destination": "9b65b7e1-04c8-4717-8f45-2aa61fd25937" "override_gateway": "10.0.0.2"
} }
], ],
"final": "direct-out", "final": "direct-out",

View File

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

View File

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

View File

@@ -12,12 +12,12 @@
}, },
"endpoints": [ "endpoints": [
{ {
"type": "tunnel-server", "type": "vpn-server",
"tag": "tunnel", "tag": "vpn",
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13", "address": "10.0.0.1",
"users": [ "users": [
{ {
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937", "address": "10.0.0.2",
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe" "key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe"
} }
], ],
@@ -51,8 +51,8 @@
"route": { "route": {
"rules": [ "rules": [
{ {
"outbound": "tunnel", "outbound": "vpn",
"override_tunnel_destination": "9b65b7e1-04c8-4717-8f45-2aa61fd25937" "override_gateway": "10.0.0.2"
} }
], ],
"final": "direct-out", "final": "direct-out",

View File

@@ -44,6 +44,7 @@ type CacheFile struct {
storeFakeIP bool storeFakeIP bool
storeRDRC bool storeRDRC bool
storeWARPConfig bool storeWARPConfig bool
storeMASQUEConfig bool
rdrcTimeout time.Duration rdrcTimeout time.Duration
DB *bbolt.DB DB *bbolt.DB
resetAccess sync.Mutex resetAccess sync.Mutex
@@ -82,17 +83,18 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
} }
} }
return &CacheFile{ return &CacheFile{
ctx: ctx, ctx: ctx,
path: filemanager.BasePath(ctx, path), path: filemanager.BasePath(ctx, path),
cacheID: cacheIDBytes, cacheID: cacheIDBytes,
storeFakeIP: options.StoreFakeIP, storeFakeIP: options.StoreFakeIP,
storeRDRC: options.StoreRDRC, storeRDRC: options.StoreRDRC,
storeWARPConfig: options.StoreWARPConfig, storeWARPConfig: options.StoreWARPConfig,
rdrcTimeout: rdrcTimeout, storeMASQUEConfig: options.StoreMASQUEConfig,
saveDomain: make(map[netip.Addr]string), rdrcTimeout: rdrcTimeout,
saveAddress4: make(map[string]netip.Addr), saveDomain: make(map[netip.Addr]string),
saveAddress6: make(map[string]netip.Addr), saveAddress4: make(map[string]netip.Addr),
saveRDRC: make(map[saveRDRCCacheKey]bool), saveAddress6: make(map[string]netip.Addr),
saveRDRC: make(map[saveRDRCCacheKey]bool),
} }
} }
@@ -366,6 +368,10 @@ func (c *CacheFile) StoreWARPConfig() bool {
return c.storeWARPConfig return c.storeWARPConfig
} }
func (c *CacheFile) StoreMASQUEConfig() bool {
return c.storeMASQUEConfig
}
func (c *CacheFile) LoadWARPConfig(tag string) *adapter.SavedBinary { func (c *CacheFile) LoadWARPConfig(tag string) *adapter.SavedBinary {
var savedConfig adapter.SavedBinary var savedConfig adapter.SavedBinary
err := c.DB.View(func(t *bbolt.Tx) error { err := c.DB.View(func(t *bbolt.Tx) error {
@@ -398,3 +404,69 @@ func (c *CacheFile) SaveWARPConfig(tag string, set *adapter.SavedBinary) error {
return bucket.Put([]byte(tag), configBinary) return bucket.Put([]byte(tag), configBinary)
}) })
} }
func (c *CacheFile) LoadMASQUEConfig(tag string) *adapter.SavedBinary {
var savedConfig adapter.SavedBinary
err := c.DB.View(func(t *bbolt.Tx) error {
bucket := c.bucket(t, bucketRuleSet)
if bucket == nil {
return os.ErrNotExist
}
configBinary := bucket.Get([]byte(tag))
if len(configBinary) == 0 {
return os.ErrInvalid
}
return savedConfig.UnmarshalBinary(configBinary)
})
if err != nil {
return nil
}
return &savedConfig
}
func (c *CacheFile) SaveMASQUEConfig(tag string, set *adapter.SavedBinary) error {
return c.DB.Batch(func(t *bbolt.Tx) error {
bucket, err := c.createBucket(t, bucketRuleSet)
if err != nil {
return err
}
configBinary, err := set.MarshalBinary()
if err != nil {
return err
}
return bucket.Put([]byte(tag), configBinary)
})
}
func (c *CacheFile) LoadSubscription(tag string) *adapter.SavedBinary {
var savedSet adapter.SavedBinary
err := c.DB.View(func(t *bbolt.Tx) error {
bucket := c.bucket(t, bucketRuleSet)
if bucket == nil {
return os.ErrNotExist
}
setBinary := bucket.Get([]byte(tag))
if len(setBinary) == 0 {
return os.ErrInvalid
}
return savedSet.UnmarshalBinary(setBinary)
})
if err != nil {
return nil
}
return &savedSet
}
func (c *CacheFile) SaveSubscription(tag string, sub *adapter.SavedBinary) error {
return c.DB.Batch(func(t *bbolt.Tx) error {
bucket, err := c.createBucket(t, bucketRuleSet)
if err != nil {
return err
}
setBinary, err := sub.MarshalBinary()
if err != nil {
return err
}
return bucket.Put([]byte(tag), setBinary)
})
}

View File

@@ -4,48 +4,78 @@ import (
"context" "context"
"net/http" "net/http"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common/json/badjson"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/render" "github.com/go-chi/render"
) )
func proxyProviderRouter() http.Handler { func proxyProviderRouter(server *Server) http.Handler {
r := chi.NewRouter() r := chi.NewRouter()
r.Get("/", getProviders) r.Get("/", getProviders(server))
r.Route("/{name}", func(r chi.Router) { r.Route("/{name}", func(r chi.Router) {
r.Use(parseProviderName, findProviderByName) r.Use(parseProviderName, findProviderByName(server))
r.Get("/", getProvider) r.Get("/", getProvider(server))
r.Put("/", updateProvider) r.Put("/", updateProvider)
r.Get("/healthcheck", healthCheckProvider) r.Get("/healthcheck", healthCheckProvider)
}) })
return r return r
} }
func getProviders(w http.ResponseWriter, r *http.Request) { func getProviders(server *Server) func(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, render.M{ return func(w http.ResponseWriter, r *http.Request) {
"providers": render.M{}, providerMap := make(render.M)
}) for _, provider := range server.provider.Providers() {
providerMap[provider.Tag()] = providerInfo(server, provider)
}
render.JSON(w, r, render.M{
"providers": providerMap,
})
}
} }
func getProvider(w http.ResponseWriter, r *http.Request) { func getProvider(server *Server) func(w http.ResponseWriter, r *http.Request) {
/*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider) return func(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, provider)*/ provider := r.Context().Value(CtxKeyProvider).(adapter.Provider)
render.NoContent(w, r) render.JSON(w, r, providerInfo(server, provider))
}
}
func providerInfo(server *Server, p adapter.Provider) *badjson.JSONObject {
var info badjson.JSONObject
proxies := make([]*badjson.JSONObject, 0)
for _, detour := range p.Outbounds() {
proxies = append(proxies, proxyInfo(server, detour))
}
info.Put("type", "Proxy") // Proxy, Rule
info.Put("vehicleType", C.ProviderDisplayName(p.Type())) // HTTP, File, Compatible
info.Put("name", p.Tag())
info.Put("proxies", proxies)
info.Put("updatedAt", p.UpdatedAt())
if p, ok := p.(adapter.ProviderSubscriptionInfo); ok {
info.Put("subscriptionInfo", p.SubscriptionInfo())
}
return &info
} }
func updateProvider(w http.ResponseWriter, r *http.Request) { func updateProvider(w http.ResponseWriter, r *http.Request) {
/*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider) provider := r.Context().Value(CtxKeyProvider).(adapter.Provider)
if err := provider.Update(); err != nil { if provider, isUpdater := provider.(adapter.ProviderUpdater); isUpdater {
render.Status(r, http.StatusServiceUnavailable) if err := provider.Update(); err != nil {
render.JSON(w, r, newError(err.Error())) render.Status(r, http.StatusServiceUnavailable)
return render.JSON(w, r, newError(err.Error()))
}*/ return
}
}
render.NoContent(w, r) render.NoContent(w, r)
} }
func healthCheckProvider(w http.ResponseWriter, r *http.Request) { func healthCheckProvider(w http.ResponseWriter, r *http.Request) {
/*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider) provider := r.Context().Value(CtxKeyProvider).(adapter.Provider)
provider.HealthCheck()*/ provider.HealthCheck(r.Context())
render.NoContent(w, r) render.NoContent(w, r)
} }
@@ -57,18 +87,19 @@ func parseProviderName(next http.Handler) http.Handler {
}) })
} }
func findProviderByName(next http.Handler) http.Handler { func findProviderByName(server *Server) func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return func(next http.Handler) http.Handler {
/*name := r.Context().Value(CtxKeyProviderName).(string) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
providers := tunnel.ProxyProviders() name := r.Context().Value(CtxKeyProviderName).(string)
provider, exist := providers[name] provider, exist := server.provider.Get(name)
if !exist {*/ if !exist {
render.Status(r, http.StatusNotFound) render.Status(r, http.StatusNotFound)
render.JSON(w, r, ErrNotFound) render.JSON(w, r, ErrNotFound)
//return return
//} }
// ctx := context.WithValue(r.Context(), CtxKeyProvider, provider) ctx := context.WithValue(r.Context(), CtxKeyProvider, provider)
// next.ServeHTTP(w, r.WithContext(ctx)) next.ServeHTTP(w, r.WithContext(ctx))
}) })
}
} }

View File

@@ -46,6 +46,7 @@ type Server struct {
dnsRouter adapter.DNSRouter dnsRouter adapter.DNSRouter
outbound adapter.OutboundManager outbound adapter.OutboundManager
endpoint adapter.EndpointManager endpoint adapter.EndpointManager
provider adapter.ProviderManager
logger log.Logger logger log.Logger
httpServer *http.Server httpServer *http.Server
trafficManager *trafficontrol.Manager trafficManager *trafficontrol.Manager
@@ -71,6 +72,7 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
dnsRouter: service.FromContext[adapter.DNSRouter](ctx), dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
outbound: service.FromContext[adapter.OutboundManager](ctx), outbound: service.FromContext[adapter.OutboundManager](ctx),
endpoint: service.FromContext[adapter.EndpointManager](ctx), endpoint: service.FromContext[adapter.EndpointManager](ctx),
provider: service.FromContext[adapter.ProviderManager](ctx),
logger: logFactory.NewLogger("clash-api"), logger: logFactory.NewLogger("clash-api"),
httpServer: &http.Server{ httpServer: &http.Server{
Addr: options.ExternalController, Addr: options.ExternalController,
@@ -122,7 +124,7 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
r.Mount("/proxies", proxyRouter(s, s.router)) r.Mount("/proxies", proxyRouter(s, s.router))
r.Mount("/rules", ruleRouter(s.router)) r.Mount("/rules", ruleRouter(s.router))
r.Mount("/connections", connectionRouter(s.ctx, s.router, trafficManager)) r.Mount("/connections", connectionRouter(s.ctx, s.router, trafficManager))
r.Mount("/providers/proxies", proxyProviderRouter()) r.Mount("/providers/proxies", proxyProviderRouter(s))
r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/providers/rules", ruleProviderRouter())
r.Mount("/script", scriptRouter()) r.Mount("/script", scriptRouter())
r.Mount("/profile", profileRouter()) r.Mount("/profile", profileRouter())

View File

@@ -3,6 +3,7 @@ package libbox
import ( import (
"bytes" "bytes"
"context" "context"
"net/netip"
"os" "os"
box "github.com/sagernet/sing-box" box "github.com/sagernet/sing-box"
@@ -33,7 +34,7 @@ func baseContext(platformInterface PlatformInterface) context.Context {
} }
ctx := context.Background() ctx := context.Background()
ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID) ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID)
return box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry()) return box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.ProviderRegistry(), dnsRegistry, include.ServiceRegistry())
} }
func parseConfig(ctx context.Context, configContent string) (option.Options, error) { func parseConfig(ctx context.Context, configContent string) (option.Options, error) {
@@ -144,6 +145,10 @@ func (s *platformInterfaceStub) SendNotification(notification *adapter.Notificat
return nil return nil
} }
func (s *platformInterfaceStub) MyInterfaceAddress() []netip.Addr {
return nil
}
func (s *platformInterfaceStub) UsePlatformLocalDNSTransport() bool { func (s *platformInterfaceStub) UsePlatformLocalDNSTransport() bool {
return false return false
} }

View File

@@ -29,6 +29,7 @@ type platformInterfaceWrapper struct {
useProcFS bool useProcFS bool
networkManager adapter.NetworkManager networkManager adapter.NetworkManager
myTunName string myTunName string
myTunAddress []netip.Addr
defaultInterfaceAccess sync.Mutex defaultInterfaceAccess sync.Mutex
defaultInterface *control.Interface defaultInterface *control.Interface
isExpensive bool isExpensive bool
@@ -78,9 +79,25 @@ func (w *platformInterfaceWrapper) OpenInterface(options *tun.Options, platformO
} }
options.FileDescriptor = dupFd options.FileDescriptor = dupFd
w.myTunName = options.Name w.myTunName = options.Name
w.myTunAddress = myTunAddress(options)
return tun.New(*options) return tun.New(*options)
} }
func myTunAddress(options *tun.Options) []netip.Addr {
addresses := make([]netip.Addr, 0, len(options.Inet4Address)+len(options.Inet6Address))
for _, prefix := range options.Inet4Address {
addresses = append(addresses, prefix.Addr())
}
for _, prefix := range options.Inet6Address {
addresses = append(addresses, prefix.Addr())
}
return addresses
}
func (w *platformInterfaceWrapper) MyInterfaceAddress() []netip.Addr {
return w.myTunAddress
}
func (w *platformInterfaceWrapper) UsePlatformDefaultInterfaceMonitor() bool { func (w *platformInterfaceWrapper) UsePlatformDefaultInterfaceMonitor() bool {
return true return true
} }

31
go.mod
View File

@@ -1,8 +1,9 @@
module github.com/sagernet/sing-box module github.com/sagernet/sing-box
go 1.25.5 go 1.26.1
require ( require (
github.com/Diniboy1123/connect-ip-go v0.0.0-20260409225322-8d7bb0a858a2
github.com/GoAdminGroup/go-admin v1.2.26 github.com/GoAdminGroup/go-admin v1.2.26
github.com/GoAdminGroup/themes v0.0.48 github.com/GoAdminGroup/themes v0.0.48
github.com/anthropics/anthropic-sdk-go v1.26.0 github.com/anthropics/anthropic-sdk-go v1.26.0
@@ -33,7 +34,7 @@ require (
github.com/miekg/dns v1.1.72 github.com/miekg/dns v1.1.72
github.com/openai/openai-go/v3 v3.26.0 github.com/openai/openai-go/v3 v3.26.0
github.com/oschwald/maxminddb-golang v1.13.1 github.com/oschwald/maxminddb-golang v1.13.1
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache/v2 v2.0.0-00010101000000-000000000000
github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
github.com/sagernet/cors v1.2.1 github.com/sagernet/cors v1.2.1
@@ -55,9 +56,11 @@ require (
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
github.com/spf13/cobra v1.10.2 github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/vishvananda/netns v0.0.5 github.com/vishvananda/netns v0.0.5
github.com/yosida95/uritemplate/v3 v3.0.2
go.uber.org/zap v1.27.1 go.uber.org/zap v1.27.1
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.49.0 golang.org/x/crypto v0.49.0
@@ -72,7 +75,12 @@ require (
) )
require ( require (
github.com/kr/pretty v0.3.1 // indirect github.com/OneOfOne/xxhash v1.2.8 // indirect
github.com/dunglas/httpsfv v1.1.0 // indirect
github.com/panjf2000/ants/v2 v2.12.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b // indirect
github.com/yl2chen/cidranger v1.0.2 // indirect
gvisor.dev/gvisor v0.0.0-20260408064518-65a410b0d584 // indirect gvisor.dev/gvisor v0.0.0-20260408064518-65a410b0d584 // indirect
) )
@@ -95,6 +103,7 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // indirect github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // indirect
github.com/dolonet/mtg-multi v1.8.0
github.com/ebitengine/purego v0.9.1 // indirect github.com/ebitengine/purego v0.9.1 // indirect
github.com/florianl/go-nfqueue/v2 v2.0.2 // indirect github.com/florianl/go-nfqueue/v2 v2.0.2 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
@@ -127,7 +136,7 @@ require (
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jsimonetti/rtnetlink v1.4.0 // indirect github.com/jsimonetti/rtnetlink v1.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 github.com/klauspost/cpuid/v2 v2.3.0
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/libdns/libdns v1.1.1 // indirect github.com/libdns/libdns v1.1.1 // indirect
@@ -141,7 +150,7 @@ require (
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/nxadm/tail v1.4.11 // indirect github.com/nxadm/tail v1.4.11 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect github.com/pires/go-proxyproto v0.11.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus-community/pro-bing v0.4.0 // indirect github.com/prometheus-community/pro-bing v0.4.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
@@ -186,7 +195,7 @@ require (
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
github.com/tidwall/gjson v1.18.0 github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
@@ -210,13 +219,13 @@ require (
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1
lukechampine.com/blake3 v1.4.1 lukechampine.com/blake3 v1.4.1
xorm.io/builder v0.3.7 // indirect xorm.io/builder v0.3.7 // indirect
xorm.io/xorm v1.0.2 // indirect xorm.io/xorm v1.0.2 // indirect
) )
replace github.com/sagernet/wireguard-go => github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.3.4 replace github.com/sagernet/wireguard-go => github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.4.0
replace github.com/sagernet/tailscale => github.com/shtorm-7/tailscale v1.92.4-sing-box-1.13-mod.7-extended-1.0.2 replace github.com/sagernet/tailscale => github.com/shtorm-7/tailscale v1.92.4-sing-box-1.13-mod.7-extended-1.0.2
@@ -225,3 +234,9 @@ 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/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/starifly/sing-vmess v0.2.7-mod.9
replace github.com/patrickmn/go-cache/v2 => github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.0.2
replace github.com/dolonet/mtg-multi => github.com/shtorm-7/mtg-multi v1.8.0-extended-1.0.0
replace github.com/Diniboy1123/connect-ip-go => github.com/shtorm-7/connect-ip-go v1.0.0-extended-1.0.0

55
go.sum
View File

@@ -24,6 +24,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e h1:n+DcnTNkQnHlwpsrHoQtkrJIO7CBx029fw6oR4vIob4= github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e h1:n+DcnTNkQnHlwpsrHoQtkrJIO7CBx029fw6oR4vIob4=
github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ= github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ=
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
@@ -43,6 +45,8 @@ github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSv
github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc= 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/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
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/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc= github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc=
github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg= github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg=
@@ -64,9 +68,10 @@ github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmC
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
github.com/database64128/netx-go v0.1.1 h1:dT5LG7Gs7zFZBthFBbzWE6K8wAHjSNAaK7wCYZT7NzM= github.com/database64128/netx-go v0.1.1 h1:dT5LG7Gs7zFZBthFBbzWE6K8wAHjSNAaK7wCYZT7NzM=
github.com/database64128/netx-go v0.1.1/go.mod h1:LNlYVipaYkQArRFDNNJ02VkNV+My9A5XR/IGS7sIBQc= github.com/database64128/netx-go v0.1.1/go.mod h1:LNlYVipaYkQArRFDNNJ02VkNV+My9A5XR/IGS7sIBQc=
github.com/database64128/tfo-go/v2 v2.3.2 h1:UhZMKiMq3swZGUiETkLBDzQnZBPSAeBMClpJGlnJ5Fw= github.com/database64128/tfo-go/v2 v2.3.2 h1:UhZMKiMq3swZGUiETkLBDzQnZBPSAeBMClpJGlnJ5Fw=
@@ -94,6 +99,8 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54=
github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
@@ -224,6 +231,8 @@ github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k=
github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
@@ -234,8 +243,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -268,6 +277,8 @@ github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczG
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
@@ -320,14 +331,15 @@ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2sz
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE= github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= 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 h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21 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.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -345,10 +357,13 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
@@ -448,15 +463,23 @@ github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1h
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
github.com/shtorm-7/connect-ip-go v1.0.0-extended-1.0.0 h1:ws7BIsYLd31Wjifq88BYCHRVlgO+07iwil39s6ERba8=
github.com/shtorm-7/connect-ip-go v1.0.0-extended-1.0.0/go.mod h1:mRwx4w32qQxsWB2kThuHpbo7iNjJiq1jYWubgqEPjHA=
github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0 h1:e5s7RKBd2rIPR0StbvZ2vTVtJ5jDTsTk5wtIIapZTRg= github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0 h1:e5s7RKBd2rIPR0StbvZ2vTVtJ5jDTsTk5wtIIapZTRg=
github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI= github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI=
github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.0.2 h1:+1tb8QNU0n2p/8Ct0A3/uHYImYXFhnN4lHOJoIdAV2s=
github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.0.2/go.mod h1:Ek4yz5OK6stwhLKgLsRRYDI+FA+ZWvRJiWLjsi/vMM4=
github.com/shtorm-7/mtg-multi v1.8.0-extended-1.0.0 h1:Gr0oINXDOAuQ+eoenfT53UWm1Y47QA7A4PLzgbVFNWo=
github.com/shtorm-7/mtg-multi v1.8.0-extended-1.0.0/go.mod h1:3rvdhwdPABkwKBdvgMt3VwMn9uSq8hpoHRezZ5jRJU0=
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 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-mux v0.3.4-extended-1.0.0/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
github.com/shtorm-7/tailscale v1.92.4-sing-box-1.13-mod.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 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/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.3.4 h1:t/2ZxRo8cwvydImFaKuUSDrcZYhX753JiXGe7411krI= github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.4.0 h1:z25EapzvkpyLgaq2T0o7eeoshBR3U4AhqMOBq1gRtrA=
github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.3.4/go.mod h1:Me2JlCDYHxnd0mnuX7L5LXAeDHCltI7vSKq3eTE6SVE= github.com/shtorm-7/wireguard-go v0.0.2-beta.1-extended-1.4.0/go.mod h1:Me2JlCDYHxnd0mnuX7L5LXAeDHCltI7vSKq3eTE6SVE=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
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/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= 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.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -466,6 +489,8 @@ github.com/starifly/sing-vmess v0.2.7-mod.9 h1:xobAmejSbBQ0A3f/EtJ9cJd3m6gK7dDPc
github.com/starifly/sing-vmess v0.2.7-mod.9/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs= github.com/starifly/sing-vmess v0.2.7-mod.9/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/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=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -501,6 +526,12 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/txthinking/runnergroup v0.0.0-20250224021307-5864ffeb65ae h1:ArVM1jICfm7g4E4dBet+KHUFMLuxmj1Nxdp/tr3ByCU=
github.com/txthinking/runnergroup v0.0.0-20250224021307-5864ffeb65ae/go.mod h1:cldYm15/XHcGt7ndItnEWHwFZo7dinU+2QoyjfErhsI=
github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e h1:xA7GVlbz6teIF4FdvuqwbX6C4tiqNk2PH7FRPIDerao=
github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM=
github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b h1:p+bJ3v5uUdEVMCoeFUs+BNJPsqt+Y6BLbDaPfTcbcH8=
github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b/go.mod h1:OYRfF6eb5wY9VRFkXJH8FFBi3plw2v+giaIu7P054pM=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
@@ -510,6 +541,10 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= 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 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
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 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
@@ -534,6 +569,8 @@ go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 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/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=

12
include/masque.go Normal file
View File

@@ -0,0 +1,12 @@
//go:build with_masque
package include
import (
"github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/protocol/masque"
)
func registerMASQUEOutbound(registry *outbound.Registry) {
masque.RegisterOutbound(registry)
}

20
include/masque_stub.go Normal file
View File

@@ -0,0 +1,20 @@
//go:build !with_masque
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 registerMASQUEOutbound(registry *outbound.Registry) {
outbound.Register[option.MASQUEOutboundOptions](registry, C.TypeMASQUE, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.MASQUEOutboundOptions) (adapter.Outbound, error) {
return nil, E.New(`MASQUE outbound is not included in this build, rebuild with -tags with_masque`)
})
}

12
include/mtproxy.go Normal file
View File

@@ -0,0 +1,12 @@
//go:build with_mtproxy
package include
import (
"github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/protocol/mtproxy"
)
func registerMTProxyInbound(registry *inbound.Registry) {
mtproxy.RegisterInbound(registry)
}

20
include/mtproxy_stub.go Normal file
View File

@@ -0,0 +1,20 @@
//go:build !with_mtproxy
package include
import (
"context"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/inbound"
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 registerMTProxyInbound(registry *inbound.Registry) {
inbound.Register[option.MTProxyInboundOptions](registry, C.TypeMTProxy, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.MTProxyInboundOptions) (adapter.Inbound, error) {
return nil, E.New(`MTProxy is not included in this build, rebuild with -tags with_mtproxy`)
})
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/adapter/provider"
"github.com/sagernet/sing-box/adapter/service" "github.com/sagernet/sing-box/adapter/service"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns"
@@ -28,6 +29,7 @@ import (
"github.com/sagernet/sing-box/protocol/mieru" "github.com/sagernet/sing-box/protocol/mieru"
"github.com/sagernet/sing-box/protocol/mixed" "github.com/sagernet/sing-box/protocol/mixed"
"github.com/sagernet/sing-box/protocol/naive" "github.com/sagernet/sing-box/protocol/naive"
"github.com/sagernet/sing-box/protocol/parser"
"github.com/sagernet/sing-box/protocol/redirect" "github.com/sagernet/sing-box/protocol/redirect"
"github.com/sagernet/sing-box/protocol/shadowsocks" "github.com/sagernet/sing-box/protocol/shadowsocks"
"github.com/sagernet/sing-box/protocol/shadowtls" "github.com/sagernet/sing-box/protocol/shadowtls"
@@ -36,9 +38,11 @@ import (
"github.com/sagernet/sing-box/protocol/tor" "github.com/sagernet/sing-box/protocol/tor"
"github.com/sagernet/sing-box/protocol/trojan" "github.com/sagernet/sing-box/protocol/trojan"
"github.com/sagernet/sing-box/protocol/tun" "github.com/sagernet/sing-box/protocol/tun"
"github.com/sagernet/sing-box/protocol/tunnel"
"github.com/sagernet/sing-box/protocol/vless" "github.com/sagernet/sing-box/protocol/vless"
"github.com/sagernet/sing-box/protocol/vmess" "github.com/sagernet/sing-box/protocol/vmess"
"github.com/sagernet/sing-box/protocol/vpn"
localProvider "github.com/sagernet/sing-box/provider/local"
remoteProvider "github.com/sagernet/sing-box/provider/remote"
"github.com/sagernet/sing-box/service/admin_panel" "github.com/sagernet/sing-box/service/admin_panel"
"github.com/sagernet/sing-box/service/manager" "github.com/sagernet/sing-box/service/manager"
"github.com/sagernet/sing-box/service/node" "github.com/sagernet/sing-box/service/node"
@@ -50,7 +54,7 @@ import (
) )
func Context(ctx context.Context) context.Context { func Context(ctx context.Context) context.Context {
return box.Context(ctx, InboundRegistry(), OutboundRegistry(), EndpointRegistry(), DNSTransportRegistry(), ServiceRegistry()) return box.Context(ctx, InboundRegistry(), OutboundRegistry(), EndpointRegistry(), ProviderRegistry(), DNSTransportRegistry(), ServiceRegistry())
} }
func InboundRegistry() *inbound.Registry { func InboundRegistry() *inbound.Registry {
@@ -77,6 +81,7 @@ func InboundRegistry() *inbound.Registry {
registerQUICInbounds(registry) registerQUICInbounds(registry)
registerStubForRemovedInbounds(registry) registerStubForRemovedInbounds(registry)
registerMTProxyInbound(registry)
return registry return registry
} }
@@ -88,7 +93,7 @@ func OutboundRegistry() *outbound.Registry {
block.RegisterOutbound(registry) block.RegisterOutbound(registry)
group.RegisterFailover(registry) group.RegisterFallback(registry)
group.RegisterSelector(registry) group.RegisterSelector(registry)
group.RegisterURLTest(registry) group.RegisterURLTest(registry)
@@ -104,12 +109,15 @@ func OutboundRegistry() *outbound.Registry {
vless.RegisterOutbound(registry) vless.RegisterOutbound(registry)
mieru.RegisterOutbound(registry) mieru.RegisterOutbound(registry)
anytls.RegisterOutbound(registry) anytls.RegisterOutbound(registry)
registerMASQUEOutbound(registry)
bond.RegisterOutbound(registry) bond.RegisterOutbound(registry)
bandwidth.RegisterOutbound(registry) bandwidth.RegisterOutbound(registry)
connection.RegisterOutbound(registry) connection.RegisterOutbound(registry)
parser.RegisterOutbound(registry)
registerQUICOutbounds(registry) registerQUICOutbounds(registry)
registerStubForRemovedOutbounds(registry) registerStubForRemovedOutbounds(registry)
@@ -119,8 +127,8 @@ func OutboundRegistry() *outbound.Registry {
func EndpointRegistry() *endpoint.Registry { func EndpointRegistry() *endpoint.Registry {
registry := endpoint.NewRegistry() registry := endpoint.NewRegistry()
tunnel.RegisterServerEndpoint(registry) vpn.RegisterServerEndpoint(registry)
tunnel.RegisterClientEndpoint(registry) vpn.RegisterClientEndpoint(registry)
registerWireGuardEndpoint(registry) registerWireGuardEndpoint(registry)
registerTailscaleEndpoint(registry) registerTailscaleEndpoint(registry)
@@ -128,6 +136,16 @@ func EndpointRegistry() *endpoint.Registry {
return registry return registry
} }
func ProviderRegistry() *provider.Registry {
registry := provider.NewRegistry()
localProvider.RegisterProviderInline(registry)
localProvider.RegisterProviderLocal(registry)
remoteProvider.RegisterProvider(registry)
return registry
}
func DNSTransportRegistry() *dns.TransportRegistry { func DNSTransportRegistry() *dns.TransportRegistry {
registry := dns.NewTransportRegistry() registry := dns.NewTransportRegistry()

View File

@@ -4,10 +4,11 @@ package include
import ( import (
"github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/protocol/warp"
"github.com/sagernet/sing-box/protocol/wireguard" "github.com/sagernet/sing-box/protocol/wireguard"
) )
func registerWireGuardEndpoint(registry *endpoint.Registry) { func registerWireGuardEndpoint(registry *endpoint.Registry) {
wireguard.RegisterEndpoint(registry) wireguard.RegisterEndpoint(registry)
wireguard.RegisterWARPEndpoint(registry) warp.RegisterEndpoint(registry)
} }

9
option/cloudflare.go Normal file
View File

@@ -0,0 +1,9 @@
package option
type CloudflareProfile struct {
ID string `json:"id,omitempty"`
AuthToken string `json:"auth_token,omitempty"`
PrivateKey string `json:"private_key,omitempty"`
Recreate bool `json:"recreate,omitempty"`
Detour string `json:"detour,omitempty"`
}

View File

@@ -11,13 +11,14 @@ type ExperimentalOptions struct {
} }
type CacheFileOptions struct { type CacheFileOptions struct {
Enabled bool `json:"enabled,omitempty"` Enabled bool `json:"enabled,omitempty"`
Path string `json:"path,omitempty"` Path string `json:"path,omitempty"`
CacheID string `json:"cache_id,omitempty"` CacheID string `json:"cache_id,omitempty"`
StoreFakeIP bool `json:"store_fakeip,omitempty"` StoreFakeIP bool `json:"store_fakeip,omitempty"`
StoreRDRC bool `json:"store_rdrc,omitempty"` StoreRDRC bool `json:"store_rdrc,omitempty"`
StoreWARPConfig bool `json:"store_warp_config,omitempty"` StoreWARPConfig bool `json:"store_warp_config,omitempty"`
RDRCTimeout badoption.Duration `json:"rdrc_timeout,omitempty"` StoreMASQUEConfig bool `json:"store_masque_config,omitempty"`
RDRCTimeout badoption.Duration `json:"rdrc_timeout,omitempty"`
} }
type ClashAPIOptions struct { type ClashAPIOptions struct {

9
option/failover.go Normal file
View File

@@ -0,0 +1,9 @@
package option
type FailoverInboundOptions struct {
Inbounds []Inbound `json:"inbounds"`
}
type FailoverOutboundOptions struct {
Outbounds []Outbound `json:"outbounds"`
}

View File

@@ -3,13 +3,13 @@ package option
import "github.com/sagernet/sing/common/json/badoption" import "github.com/sagernet/sing/common/json/badoption"
type SelectorOutboundOptions struct { type SelectorOutboundOptions struct {
Outbounds []string `json:"outbounds"` GroupCommonOption
Default string `json:"default,omitempty"` Default string `json:"default,omitempty"`
InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"`
} }
type URLTestOutboundOptions struct { type URLTestOutboundOptions struct {
Outbounds []string `json:"outbounds"` GroupCommonOption
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
Interval badoption.Duration `json:"interval,omitempty"` Interval badoption.Duration `json:"interval,omitempty"`
Tolerance uint16 `json:"tolerance,omitempty"` Tolerance uint16 `json:"tolerance,omitempty"`
@@ -17,6 +17,14 @@ type URLTestOutboundOptions struct {
InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"`
} }
type FailoverOutboundOptions struct { type FallbackOutboundOptions struct {
Outbounds []string `json:"outbounds"` Outbounds []string `json:"outbounds"`
} }
type GroupCommonOption struct {
Outbounds []string `json:"outbounds"`
Providers []string `json:"providers"`
Exclude *badoption.Regexp `json:"exclude,omitempty"`
Include *badoption.Regexp `json:"include,omitempty"`
UseAllProviders bool `json:"use_all_providers,omitempty"`
}

32
option/masque.go Normal file
View File

@@ -0,0 +1,32 @@
package option
import (
"github.com/sagernet/sing/common/json/badoption"
)
type MASQUEOutboundOptions struct {
UseHTTP2 bool `json:"use_http2,omitempty"`
UseIPv6 bool `json:"use_ipv6,omitempty"`
Profile CloudflareProfile `json:"profile,omitempty"`
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
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
}
type MASQUEOutboundTLSOptions struct {
Insecure bool `json:"insecure,omitempty"`
CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"`
CurvePreferences badoption.Listable[CurvePreference] `json:"curve_preferences,omitempty"`
Fragment bool `json:"fragment,omitempty"`
FragmentFallbackDelay badoption.Duration `json:"fragment_fallback_delay,omitempty"`
RecordFragment bool `json:"record_fragment,omitempty"`
KernelTx bool `json:"kernel_tx,omitempty"`
KernelRx bool `json:"kernel_rx,omitempty"`
}
type MASQUEOutboundTLSOptionsContainer struct {
TLS *OutboundTLSOptions `json:"tls,omitempty"`
}

89
option/mtproxy.go Normal file
View File

@@ -0,0 +1,89 @@
package option
import (
"time"
"github.com/sagernet/sing/common/json/badoption"
)
type MTProxyInboundOptions struct {
ListenOptions
Users []MTProxyUser `json:"users,omitempty"`
Concurrency uint `json:"concurrency,omitempty"`
DomainFrontingPort uint `json:"domain_fronting_port,omitempty"`
DomainFrontingIP string `json:"domain_fronting_ip,omitempty"`
DomainFrontingProxyProtocol bool `json:"domain_fronting_proxy_protocol,omitempty"`
PreferIP string `json:"prefer_ip,omitempty"`
AutoUpdate bool `json:"auto_update,omitempty"`
AllowFallbackOnUnknownDC bool `json:"allow_fallback_on_unknown_dc,omitempty"`
TolerateTimeSkewness badoption.Duration `json:"tolerate_time_skewness,omitempty"`
IdleTimeout badoption.Duration `json:"idle_timeout,omitempty"`
HandshakeTimeout badoption.Duration `json:"handshake_timeout,omitempty"`
DoppelGangerURLs []string `json:"doppelganger_urls,omitempty"`
DoppelGangerPerRaid uint `json:"doppelganger_per_raid,omitempty"`
DoppelGangerEach badoption.Duration `json:"doppelganger_each,omitempty"`
DoppelGangerDRS bool `json:"doppelganger_drs,omitempty"`
ThrottleMaxConnections uint `json:"throttle_max_connections,omitempty"`
ThrottleCheckInterval badoption.Duration `json:"throttle_check_interval,omitempty"`
}
func (o *MTProxyInboundOptions) GetConcurrency() uint {
if o.Concurrency == 0 {
return 8192
}
return o.Concurrency
}
func (o *MTProxyInboundOptions) GetDomainFrontingPort() uint {
if o.DomainFrontingPort == 0 {
return 443
}
return o.DomainFrontingPort
}
func (o *MTProxyInboundOptions) GetPreferIP() string {
if o.PreferIP == "" {
return "prefer-ipv4"
}
return o.PreferIP
}
func (o *MTProxyInboundOptions) GetIdleTimeout() time.Duration {
if o.IdleTimeout == 0 {
return 5 * time.Minute
}
return o.IdleTimeout.Build()
}
func (o *MTProxyInboundOptions) GetHandshakeTimeout() time.Duration {
if o.HandshakeTimeout == 0 {
return 10 * time.Second
}
return o.HandshakeTimeout.Build()
}
func (o *MTProxyInboundOptions) GetDoppelGangerPerRaid() uint {
if o.DoppelGangerPerRaid == 0 {
return 10
}
return o.DoppelGangerPerRaid
}
func (o *MTProxyInboundOptions) GetDoppelGangerEach() time.Duration {
if o.HandshakeTimeout == 0 {
return 6 * time.Hour
}
return o.DoppelGangerEach.Build()
}
func (o *MTProxyInboundOptions) GetThrottleCheckInterval() time.Duration {
if o.ThrottleCheckInterval == 0 {
return 5 * time.Second
}
return o.ThrottleCheckInterval.Build()
}
type MTProxyUser struct {
Name string `json:"name"`
Secret string `json:"secret"`
}

View File

@@ -19,6 +19,7 @@ type _Options struct {
Endpoints []Endpoint `json:"endpoints,omitempty"` Endpoints []Endpoint `json:"endpoints,omitempty"`
Inbounds []Inbound `json:"inbounds,omitempty"` Inbounds []Inbound `json:"inbounds,omitempty"`
Outbounds []Outbound `json:"outbounds,omitempty"` Outbounds []Outbound `json:"outbounds,omitempty"`
Providers []Provider `json:"providers,omitempty"`
Route *RouteOptions `json:"route,omitempty"` Route *RouteOptions `json:"route,omitempty"`
Services []Service `json:"services,omitempty"` Services []Service `json:"services,omitempty"`
Experimental *ExperimentalOptions `json:"experimental,omitempty"` Experimental *ExperimentalOptions `json:"experimental,omitempty"`

6
option/parser.go Normal file
View File

@@ -0,0 +1,6 @@
package option
type ParserOutboundOptions struct {
DialerOptions
Link string `json:"link"`
}

75
option/provider.go Normal file
View File

@@ -0,0 +1,75 @@
package option
import (
"context"
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"
"github.com/sagernet/sing/service"
)
type ProviderOptionsRegistry interface {
CreateOptions(providerType string) (any, bool)
}
type _Provider struct {
Type string `json:"type"`
Tag string `json:"tag,omitempty"`
Options any `json:"-"`
}
type Provider _Provider
func (h *Provider) MarshalJSONContext(ctx context.Context) ([]byte, error) {
return badjson.MarshallObjectsContext(ctx, (*_Provider)(h), h.Options)
}
func (h *Provider) UnmarshalJSONContext(ctx context.Context, content []byte) error {
err := json.UnmarshalContext(ctx, content, (*_Provider)(h))
if err != nil {
return err
}
registry := service.FromContext[ProviderOptionsRegistry](ctx)
if registry == nil {
return E.New("missing provider options registry in context")
}
options, loaded := registry.CreateOptions(h.Type)
if !loaded {
return E.New("unknown provider type: ", h.Type)
}
err = badjson.UnmarshallExcludedContext(ctx, content, (*_Provider)(h), options)
if err != nil {
return err
}
h.Options = options
return nil
}
type ProviderLocalOptions struct {
Path string `json:"path"`
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
}
type ProviderRemoteOptions struct {
URL string `json:"url"`
UserAgent string `json:"user_agent,omitempty"`
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"`
}
type ProviderInlineOptions struct {
Outbounds []Outbound `json:"outbounds,omitempty"`
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
}
type ProviderHealthCheckOptions struct {
Enabled bool `json:"enabled,omitempty"`
URL string `json:"url,omitempty"`
Interval badoption.Duration `json:"interval,omitempty"`
Timeout badoption.Duration `json:"timeout,omitempty"`
}

View File

@@ -88,8 +88,6 @@ type RawDefaultRule struct {
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
Port badoption.Listable[uint16] `json:"port,omitempty"` Port badoption.Listable[uint16] `json:"port,omitempty"`
PortRange badoption.Listable[string] `json:"port_range,omitempty"` PortRange badoption.Listable[string] `json:"port_range,omitempty"`
TunnelSource badoption.Listable[string] `json:"tunnel_source,omitempty"`
TunnelDestination badoption.Listable[string] `json:"tunnel_destination,omitempty"`
ProcessName badoption.Listable[string] `json:"process_name,omitempty"` ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`

View File

@@ -155,9 +155,10 @@ type RouteActionOptions struct {
} }
type RawRouteOptionsActionOptions struct { type RawRouteOptionsActionOptions struct {
OverrideAddress string `json:"override_address,omitempty"` OverrideAddress string `json:"override_address,omitempty"`
OverridePort uint16 `json:"override_port,omitempty"` OverridePort uint16 `json:"override_port,omitempty"`
OverrideTunnelDestination string `json:"override_tunnel_destination,omitempty"`
OverrideGateway string `json:"override_gateway,omitempty"`
NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"` NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"`
FallbackDelay uint32 `json:"fallback_delay,omitempty"` FallbackDelay uint32 `json:"fallback_delay,omitempty"`

View File

@@ -90,8 +90,6 @@ type RawDefaultDNSRule struct {
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
Port badoption.Listable[uint16] `json:"port,omitempty"` Port badoption.Listable[uint16] `json:"port,omitempty"`
PortRange badoption.Listable[string] `json:"port_range,omitempty"` PortRange badoption.Listable[string] `json:"port_range,omitempty"`
TunnelSource badoption.Listable[string] `json:"tunnel_source,omitempty"`
TunnelDestination badoption.Listable[string] `json:"tunnel_destination,omitempty"`
ProcessName badoption.Listable[string] `json:"process_name,omitempty"` ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`

View File

@@ -194,8 +194,6 @@ type DefaultHeadlessRule struct {
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
Port badoption.Listable[uint16] `json:"port,omitempty"` Port badoption.Listable[uint16] `json:"port,omitempty"`
PortRange badoption.Listable[string] `json:"port_range,omitempty"` PortRange badoption.Listable[string] `json:"port_range,omitempty"`
TunnelSource badoption.Listable[string] `json:"tunnel_source,omitempty"`
TunnelDestination badoption.Listable[string] `json:"tunnel_destination,omitempty"`
ProcessName badoption.Listable[string] `json:"process_name,omitempty"` ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`

View File

@@ -1,21 +0,0 @@
package option
import "github.com/sagernet/sing/common/json/badoption"
type TunnelClientEndpointOptions struct {
UUID string `json:"uuid"`
Key string `json:"key"`
Outbound Outbound `json:"outbound"`
}
type TunnelServerEndpointOptions struct {
UUID string `json:"uuid"`
Users []TunnelUser `json:"users"`
Inbound Inbound `json:"inbound"`
ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"`
}
type TunnelUser struct {
UUID string `json:"uuid"`
Key string `json:"key"`
}

25
option/vpn.go Normal file
View File

@@ -0,0 +1,25 @@
package option
import (
"net/netip"
"github.com/sagernet/sing/common/json/badoption"
)
type VPNClientEndpointOptions struct {
Address netip.Addr `json:"address"`
Key string `json:"key"`
Outbound Outbound `json:"outbound"`
}
type VPNServerEndpointOptions struct {
Address netip.Addr `json:"address"`
Users []VPNUser `json:"users"`
Inbounds []Inbound `json:"inbounds"`
ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"`
}
type VPNUser struct {
Address netip.Addr `json:"address"`
Key string `json:"key"`
}

18
option/warp.go Normal file
View File

@@ -0,0 +1,18 @@
package option
import "github.com/sagernet/sing/common/json/badoption"
type WARPEndpointOptions struct {
System bool `json:"system,omitempty"`
Name string `json:"name,omitempty"`
ListenPort uint16 `json:"listen_port,omitempty"`
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
PersistentKeepaliveInterval uint16 `json:"persistent_keepalive_interval,omitempty"`
Reserved []uint8 `json:"reserved,omitempty"`
Workers int `json:"workers,omitempty"`
PreallocatedBuffersPerPool uint32 `json:"preallocated_buffers_per_pool,omitempty"`
DisablePauses bool `json:"disable_pauses,omitempty"`
Amnezia *WireGuardAmnezia `json:"amnezia,omitempty"`
Profile CloudflareProfile `json:"profile,omitempty"`
DialerOptions
}

View File

@@ -33,29 +33,6 @@ type WireGuardPeer struct {
Reserved []uint8 `json:"reserved,omitempty"` Reserved []uint8 `json:"reserved,omitempty"`
} }
type WireGuardWARPEndpointOptions struct {
System bool `json:"system,omitempty"`
Name string `json:"name,omitempty"`
ListenPort uint16 `json:"listen_port,omitempty"`
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
PersistentKeepaliveInterval uint16 `json:"persistent_keepalive_interval,omitempty"`
Reserved []uint8 `json:"reserved,omitempty"`
Workers int `json:"workers,omitempty"`
PreallocatedBuffersPerPool uint32 `json:"preallocated_buffers_per_pool,omitempty"`
DisablePauses bool `json:"disable_pauses,omitempty"`
Amnezia *WireGuardAmnezia `json:"amnezia,omitempty"`
Profile WARPProfile `json:"profile,omitempty"`
DialerOptions
}
type WARPProfile struct {
ID string `json:"id,omitempty"`
PrivateKey string `json:"private_key,omitempty"`
AuthToken string `json:"auth_token,omitempty"`
Recreate bool `json:"recreate,omitempty"`
Detour string `json:"detour,omitempty"`
}
type WireGuardAmnezia struct { type WireGuardAmnezia struct {
JC int `json:"jc,omitempty"` JC int `json:"jc,omitempty"`
JMin int `json:"jmin,omitempty"` JMin int `json:"jmin,omitempty"`

30
parser/clash/anytls.go Normal file
View File

@@ -0,0 +1,30 @@
package clash
import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json/badoption"
)
type AnyTLSOption struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
TLSOptions `yaml:",inline"`
Password string `yaml:"password"`
UDP bool `yaml:"udp,omitempty"`
IdleSessionCheckInterval int `yaml:"idle-session-check-interval,omitempty"`
IdleSessionTimeout int `yaml:"idle-session-timeout,omitempty"`
MinIdleSession int `yaml:"min-idle-session,omitempty"`
}
func (a *AnyTLSOption) Build() any {
a.TLS = true
return &option.AnyTLSOutboundOptions{
DialerOptions: a.DialerOptions.Build(),
ServerOptions: a.ServerOptions.Build(),
OutboundTLSOptionsContainer: clashTLSOptions(a.Server, &a.TLSOptions),
Password: a.Password,
IdleSessionCheckInterval: badoption.Duration(a.IdleSessionCheckInterval),
IdleSessionTimeout: badoption.Duration(a.IdleSessionTimeout),
MinIdleSession: a.MinIdleSession,
}
}

181
parser/clash/base.go Normal file
View File

@@ -0,0 +1,181 @@
package clash
import (
"encoding/base64"
"strings"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json/badoption"
)
type HTTPOptions struct {
Method string `yaml:"method,omitempty"`
Path []string `yaml:"path,omitempty"`
Headers badoption.HTTPHeader `yaml:"headers,omitempty"`
}
type HTTP2Options struct {
Host []string `yaml:"host,omitempty"`
Path string `yaml:"path,omitempty"`
}
type GrpcOptions struct {
GrpcServiceName string `yaml:"grpc-service-name,omitempty"`
}
type WSOptions struct {
Path string `yaml:"path,omitempty"`
Headers map[string]string `yaml:"headers,omitempty"`
MaxEarlyData int `yaml:"max-early-data,omitempty"`
EarlyDataHeaderName string `yaml:"early-data-header-name,omitempty"`
V2rayHttpUpgrade bool `yaml:"v2ray-http-upgrade,omitempty"`
}
type MuxOptions struct {
Enabled bool `yaml:"enabled,omitempty"`
Protocol string `yaml:"protocol,omitempty"`
MaxConnections int `yaml:"max-connections,omitempty"`
MinStreams int `yaml:"min-streams,omitempty"`
MaxStreams int `yaml:"max-streams,omitempty"`
Padding bool `yaml:"padding,omitempty"`
BrutalOpts *BrutalOptions `yaml:"brutal-opts,omitempty"`
}
func (s *MuxOptions) Build() *option.OutboundMultiplexOptions {
if s == nil {
return nil
}
return &option.OutboundMultiplexOptions{
Enabled: s.Enabled,
Protocol: s.Protocol,
MaxConnections: s.MaxConnections,
MinStreams: s.MinStreams,
MaxStreams: s.MaxStreams,
Padding: s.Padding,
Brutal: s.BrutalOpts.Build(),
}
}
type BrutalOptions struct {
Enabled bool `yaml:"enabled,omitempty"`
Up string `yaml:"up,omitempty"`
Down string `yaml:"down,omitempty"`
}
func (b *BrutalOptions) Build() *option.BrutalOptions {
if b == nil {
return nil
}
return &option.BrutalOptions{
Enabled: b.Enabled,
UpMbps: clashSpeedToIntMbps(b.Up),
DownMbps: clashSpeedToIntMbps(b.Down),
}
}
type RealityOptions struct {
PublicKey string `yaml:"public-key"`
ShortID string `yaml:"short-id"`
}
func (r *RealityOptions) Build() *option.OutboundRealityOptions {
if r == nil {
return nil
}
return &option.OutboundRealityOptions{
Enabled: true,
PublicKey: r.PublicKey,
ShortID: r.ShortID,
}
}
type ECHOptions struct {
Enable bool `yaml:"enable,omitempty"`
Config string `yaml:"config,omitempty"`
}
func (e *ECHOptions) Build() *option.OutboundECHOptions {
if e == nil {
return nil
}
list, err := base64.StdEncoding.DecodeString(e.Config)
if err != nil {
return nil
}
return &option.OutboundECHOptions{
Enabled: e.Enable,
Config: trimStringArray(strings.Split(string(list), "\n")),
}
}
type TLSOptions struct {
TLS bool `yaml:"tls,omitempty"`
SNI string `yaml:"sni,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
ALPN []string `yaml:"alpn,omitempty"`
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
CustomCA string `yaml:"ca,omitempty"`
CustomCAString string `yaml:"ca-str,omitempty"`
Certificate string `yaml:"certificate,omitempty"`
PrivateKey string `yaml:"private-key,omitempty"`
ECHOpts *ECHOptions `yaml:"ech-opts,omitempty"`
RealityOpts *RealityOptions `yaml:"reality-opts,omitempty"`
}
func (t *TLSOptions) Build() *option.OutboundTLSOptions {
if t == nil {
return nil
}
options := &option.OutboundTLSOptions{
Enabled: t.TLS,
ServerName: t.SNI,
Insecure: t.SkipCertVerify,
ALPN: t.ALPN,
UTLS: clashClientFingerprint(t.ClientFingerprint),
Certificate: trimStringArray(strings.Split(t.CustomCAString, "\n")),
CertificatePath: t.CustomCA,
ECH: t.ECHOpts.Build(),
Reality: t.RealityOpts.Build(),
}
if strings.HasPrefix(t.Certificate, "-----BEGIN ") {
options.ClientCertificate = trimStringArray(strings.Split(t.Certificate, "\n"))
} else {
options.ClientCertificatePath = t.Certificate
}
if strings.HasPrefix(t.PrivateKey, "-----BEGIN ") {
options.ClientKey = trimStringArray(strings.Split(t.PrivateKey, "\n"))
} else {
options.ClientKeyPath = t.PrivateKey
}
return options
}
type DialerOptions struct {
TFO bool `yaml:"tfo,omitempty"`
MPTCP bool `yaml:"mptcp,omitempty"`
Interface string `yaml:"interface-name,omitempty"`
RoutingMark int `yaml:"routing-mark,omitempty"`
DialerProxy string `yaml:"dialer-proxy,omitempty"`
}
func (b *DialerOptions) Build() option.DialerOptions {
return option.DialerOptions{
Detour: b.DialerProxy,
BindInterface: b.Interface,
TCPFastOpen: b.TFO,
TCPMultiPath: b.MPTCP,
RoutingMark: option.FwMark(b.RoutingMark),
}
}
type ServerOptions struct {
Server string `yaml:"server"`
Port int `yaml:"port"`
}
func (s *ServerOptions) Build() option.ServerOptions {
return option.ServerOptions{
Server: s.Server,
ServerPort: uint16(s.Port),
}
}

23
parser/clash/http.go Normal file
View File

@@ -0,0 +1,23 @@
package clash
import "github.com/sagernet/sing-box/option"
type HttpOption struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
*TLSOptions `yaml:",inline"`
UserName string `yaml:"username,omitempty"`
Password string `yaml:"password,omitempty"`
Headers map[string]string `yaml:"headers,omitempty"`
}
func (h *HttpOption) Build() any {
return &option.HTTPOutboundOptions{
DialerOptions: h.DialerOptions.Build(),
ServerOptions: h.ServerOptions.Build(),
Username: h.UserName,
Password: h.Password,
OutboundTLSOptionsContainer: clashTLSOptions(h.Server, h.TLSOptions),
Headers: clashHeaders(h.Headers),
}
}

47
parser/clash/hysteria.go Normal file
View File

@@ -0,0 +1,47 @@
package clash
import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json/badoption"
)
type HysteriaOption struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
TLSOptions `yaml:",inline"`
Ports string `yaml:"ports,omitempty"`
Up string `yaml:"up"`
UpSpeed int `yaml:"up-speed,omitempty"` // compatible with Stash
Down string `yaml:"down"`
DownSpeed int `yaml:"down-speed,omitempty"` // compatible with Stash
Auth string `yaml:"auth,omitempty"`
AuthString string `yaml:"auth-str,omitempty"`
Obfs string `yaml:"obfs,omitempty"`
ReceiveWindowConn int `yaml:"recv-window-conn,omitempty"`
ReceiveWindow int `yaml:"recv-window,omitempty"`
DisableMTUDiscovery bool `yaml:"disable-mtu-discovery,omitempty"`
FastOpen bool `yaml:"fast-open,omitempty"`
HopInterval int `yaml:"hop-interval,omitempty"`
}
func (h *HysteriaOption) Build() any {
h.TLS = true
h.TFO = h.FastOpen
return &option.HysteriaOutboundOptions{
DialerOptions: h.DialerOptions.Build(),
ServerOptions: h.ServerOptions.Build(),
ServerPorts: clashPorts(h.Ports),
HopInterval: badoption.Duration(h.HopInterval),
Up: clashSpeedToNetworkBytes(h.Up),
UpMbps: h.UpSpeed,
Down: clashSpeedToNetworkBytes(h.Down),
DownMbps: h.DownSpeed,
Obfs: h.Obfs,
Auth: []byte(h.Auth),
AuthString: h.AuthString,
ReceiveWindowConn: uint64(h.ReceiveWindowConn),
ReceiveWindow: uint64(h.ReceiveWindow),
DisableMTUDiscovery: h.DisableMTUDiscovery,
OutboundTLSOptionsContainer: clashTLSOptions(h.Server, &h.TLSOptions),
}
}

34
parser/clash/hysteria2.go Normal file
View File

@@ -0,0 +1,34 @@
package clash
import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json/badoption"
)
type Hysteria2Option struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
TLSOptions `yaml:",inline"`
Ports string `yaml:"ports,omitempty"`
HopInterval int `yaml:"hop-interval,omitempty"`
Up string `yaml:"up,omitempty"`
Down string `yaml:"down,omitempty"`
Password string `yaml:"password,omitempty"`
Obfs string `yaml:"obfs,omitempty"`
ObfsPassword string `yaml:"obfs-password,omitempty"`
}
func (h *Hysteria2Option) Build() any {
h.TLS = true
return &option.Hysteria2OutboundOptions{
DialerOptions: h.DialerOptions.Build(),
ServerOptions: h.ServerOptions.Build(),
ServerPorts: clashPorts(h.Ports),
HopInterval: badoption.Duration(h.HopInterval),
UpMbps: clashSpeedToIntMbps(h.Up),
DownMbps: clashSpeedToIntMbps(h.Down),
Obfs: clashHysteria2Obfs(h.Obfs, h.ObfsPassword),
Password: h.Password,
OutboundTLSOptionsContainer: clashTLSOptions(h.Server, &h.TLSOptions),
}
}

106
parser/clash/parser.go Normal file
View File

@@ -0,0 +1,106 @@
package clash
import (
"context"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"gopkg.in/yaml.v3"
)
type ClashConfig struct {
Proxies []ClashProxy `yaml:"proxies"`
}
type _ClashProxy struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
Options Proxy `yaml:"-"`
SingType string `yaml:"-"`
}
type ClashProxy _ClashProxy
type Proxy interface {
Build() any
}
func (c *ClashProxy) UnmarshalYAML(value *yaml.Node) error {
err := value.Decode((*_ClashProxy)(c))
if err != nil {
return err
}
var options Proxy
switch c.Type {
case "ss":
c.SingType = C.TypeShadowsocks
options = &ShadowSocksOption{}
case "tuic":
c.SingType = C.TypeTUIC
options = &TuicOption{}
case "vmess":
c.SingType = C.TypeVMess
options = &VmessOption{}
case "vless":
c.SingType = C.TypeVLESS
options = &VlessOption{}
case "socks5":
c.SingType = C.TypeSOCKS
options = &Socks5Option{}
case "http":
c.SingType = C.TypeHTTP
options = &HttpOption{}
case "trojan":
c.SingType = C.TypeTrojan
options = &TrojanOption{}
case "hysteria":
c.SingType = C.TypeHysteria
options = &HysteriaOption{}
case "hysteria2":
c.SingType = C.TypeHysteria2
options = &Hysteria2Option{}
case "ssh":
c.SingType = C.TypeSSH
options = &SSHOption{}
case "anytls":
c.SingType = C.TypeAnyTLS
options = &AnyTLSOption{}
default:
return nil
}
err = value.Decode(options)
if err != nil {
return err
}
c.Options = options
return nil
}
func (c *ClashProxy) Build() option.Outbound {
outbound := option.Outbound{
Tag: c.Name,
Type: c.SingType,
}
if c.Options != nil {
outbound.Options = c.Options.Build()
}
return outbound
}
func ParseClashSubscription(_ context.Context, content string) ([]option.Outbound, error) {
config := &ClashConfig{}
err := yaml.Unmarshal([]byte(content), &config)
if err != nil {
return nil, E.Cause(err, "parse clash config")
}
outbounds := common.FilterIsInstance(config.Proxies, func(proxy ClashProxy) (option.Outbound, bool) {
if proxy.SingType == "" {
return option.Outbound{}, false
}
return proxy.Build(), true
})
return outbounds, nil
}

View File

@@ -0,0 +1,51 @@
package clash
import (
"strings"
"github.com/sagernet/sing-box/option"
F "github.com/sagernet/sing/common/format"
)
type ShadowSocksOption struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
Password string `yaml:"password"`
Cipher string `yaml:"cipher"`
UDP bool `yaml:"udp,omitempty"`
Plugin string `yaml:"plugin,omitempty"`
PluginOpts map[string]any `yaml:"plugin-opts,omitempty"`
UDPOverTCP bool `yaml:"udp-over-tcp,omitempty"`
UDPOverTCPVersion int `yaml:"udp-over-tcp-version,omitempty"`
MuxOpts *MuxOptions `yaml:"smux,omitempty"`
}
func (s *ShadowSocksOption) Build() any {
return &option.ShadowsocksOutboundOptions{
DialerOptions: s.DialerOptions.Build(),
ServerOptions: s.ServerOptions.Build(),
Password: s.Password,
Method: clashShadowsocksCipher(s.Cipher),
Plugin: clashPluginName(s.Plugin),
PluginOptions: clashPluginOptions(s.Plugin, s.PluginOpts),
Network: clashNetworks(s.UDP),
UDPOverTCP: &option.UDPOverTCPOptions{
Enabled: s.UDPOverTCP,
Version: uint8(s.UDPOverTCPVersion),
},
Multiplex: s.MuxOpts.Build(),
}
}
type shadowsocksPluginOptionsBuilder map[string]any
func (o shadowsocksPluginOptionsBuilder) Build() string {
var opts []string
for key, value := range o {
if value == nil {
continue
}
opts = append(opts, F.ToString(key, "=", value))
}
return strings.Join(opts, ";")
}

21
parser/clash/socks5.go Normal file
View File

@@ -0,0 +1,21 @@
package clash
import "github.com/sagernet/sing-box/option"
type Socks5Option struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
UserName string `yaml:"username,omitempty"`
Password string `yaml:"password,omitempty"`
UDP bool `yaml:"udp,omitempty"`
}
func (s *Socks5Option) Build() any {
return &option.SOCKSOutboundOptions{
DialerOptions: s.DialerOptions.Build(),
ServerOptions: s.ServerOptions.Build(),
Username: s.UserName,
Password: s.Password,
Network: clashNetworks(s.UDP),
}
}

36
parser/clash/ssh.go Normal file
View File

@@ -0,0 +1,36 @@
package clash
import (
"strings"
"github.com/sagernet/sing-box/option"
)
type SSHOption struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
UserName string `yaml:"username"`
Password string `yaml:"password,omitempty"`
PrivateKey string `yaml:"private-key,omitempty"`
PrivateKeyPassphrase string `yaml:"private-key-passphrase,omitempty"`
HostKey []string `yaml:"host-key,omitempty"`
HostKeyAlgorithms []string `yaml:"host-key-algorithms,omitempty"`
}
func (s *SSHOption) Build() any {
options := &option.SSHOutboundOptions{
DialerOptions: s.DialerOptions.Build(),
ServerOptions: s.ServerOptions.Build(),
User: s.UserName,
Password: s.Password,
PrivateKeyPassphrase: s.PrivateKeyPassphrase,
HostKey: s.HostKey,
HostKeyAlgorithms: s.HostKeyAlgorithms,
}
if strings.Contains(s.PrivateKey, "PRIVATE KEY") {
options.PrivateKey = trimStringArray(strings.Split(s.PrivateKey, "\n"))
} else {
options.PrivateKeyPath = s.PrivateKey
}
return options
}

28
parser/clash/trojan.go Normal file
View File

@@ -0,0 +1,28 @@
package clash
import "github.com/sagernet/sing-box/option"
type TrojanOption struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
TLSOptions `yaml:",inline"`
Password string `yaml:"password"`
UDP bool `yaml:"udp,omitempty"`
Network string `yaml:"network,omitempty"`
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
MuxOpts *MuxOptions `yaml:"smux,omitempty"`
}
func (t *TrojanOption) Build() any {
t.TLS = true
return &option.TrojanOutboundOptions{
DialerOptions: t.DialerOptions.Build(),
ServerOptions: t.ServerOptions.Build(),
Password: t.Password,
Network: clashNetworks(t.UDP),
OutboundTLSOptionsContainer: clashTLSOptions(t.Server, &t.TLSOptions),
Multiplex: t.MuxOpts.Build(),
Transport: clashTransport(t.Network, HTTPOptions{}, HTTP2Options{}, t.GrpcOpts, t.WSOpts),
}
}

47
parser/clash/tuic.go Normal file
View File

@@ -0,0 +1,47 @@
package clash
import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json/badoption"
)
type TuicOption struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
TLSOptions `yaml:",inline"`
UUID string `yaml:"uuid,omitempty"`
Password string `yaml:"password,omitempty"`
Ip string `yaml:"ip,omitempty"`
HeartbeatInterval int `yaml:"heartbeat-interval,omitempty"`
DisableSni bool `yaml:"disable-sni,omitempty"`
ReduceRtt bool `yaml:"reduce-rtt,omitempty"`
UdpRelayMode string `yaml:"udp-relay-mode,omitempty"`
CongestionController string `yaml:"congestion-controller,omitempty"`
FastOpen bool `yaml:"fast-open,omitempty"`
DisableMTUDiscovery bool `yaml:"disable-mtu-discovery,omitempty"`
UDPOverStream bool `yaml:"udp-over-stream,omitempty"`
}
func (t *TuicOption) Build() any {
t.TLS = true
t.TFO = t.FastOpen
options := &option.TUICOutboundOptions{
DialerOptions: t.DialerOptions.Build(),
ServerOptions: t.ServerOptions.Build(),
UUID: t.UUID,
Password: t.Password,
CongestionControl: t.CongestionController,
UDPRelayMode: t.UdpRelayMode,
UDPOverStream: t.UDPOverStream,
ZeroRTTHandshake: t.ReduceRtt,
Heartbeat: badoption.Duration(t.HeartbeatInterval),
OutboundTLSOptionsContainer: clashTLSOptions(t.Server, &t.TLSOptions),
}
if t.Ip != "" {
options.Server = t.Ip
}
if t.DisableSni {
options.TLS.DisableSNI = true
}
return options
}

205
parser/clash/utils.go Normal file
View File

@@ -0,0 +1,205 @@
package clash
import (
"strconv"
"strings"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/byteformats"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json/badoption"
N "github.com/sagernet/sing/common/network"
)
func clashClientFingerprint(clientFingerprint string) *option.OutboundUTLSOptions {
if clientFingerprint == "" {
return nil
}
return &option.OutboundUTLSOptions{
Enabled: true,
Fingerprint: clientFingerprint,
}
}
func clashHeaders(headers map[string]string) map[string]badoption.Listable[string] {
if headers == nil {
return nil
}
result := make(map[string]badoption.Listable[string])
for key, value := range headers {
result[key] = []string{value}
}
return result
}
func clashHysteria2Obfs(obfs string, password string) *option.Hysteria2Obfs {
if obfs == "" {
return nil
}
return &option.Hysteria2Obfs{
Type: obfs,
Password: password,
}
}
func clashNetworks(udpEnabled bool) option.NetworkList {
if !udpEnabled {
return N.NetworkTCP
}
return ""
}
func clashPluginName(plugin string) string {
switch plugin {
case "obfs":
return "obfs-local"
}
return plugin
}
func clashPluginOptions(plugin string, opts map[string]any) string {
options := make(shadowsocksPluginOptionsBuilder)
switch plugin {
case "obfs":
options["obfs"] = opts["mode"]
options["obfs-host"] = opts["host"]
case "v2ray-plugin":
options["mode"] = opts["mode"]
options["tls"] = opts["tls"]
options["host"] = opts["host"]
options["path"] = opts["path"]
}
return options.Build()
}
func clashPorts(ports string) badoption.Listable[string] {
if ports == "" {
return nil
}
serverPorts := badoption.Listable[string]{}
ports = strings.ReplaceAll(ports, "/", ",")
for _, port := range strings.Split(ports, ",") {
if port == "" {
continue
}
port = strings.Replace(port, "-", ":", 1)
serverPorts = append(serverPorts, port)
}
return serverPorts
}
func clashShadowsocksCipher(cipher string) string {
switch cipher {
case "dummy":
return "none"
}
return cipher
}
func clashStringList(list []string) string {
if len(list) > 0 {
return list[0]
}
return ""
}
func clashSpeedToIntMbps(speed string) int {
if speed == "" {
return 0
}
if num, err := strconv.Atoi(speed); err == nil {
return num
}
networkBytes := byteformats.NetworkBytesCompat{}
if err := networkBytes.UnmarshalJSON([]byte(speed)); err != nil {
return 0
}
return int(networkBytes.Value() / byteformats.MByte * 8)
}
func clashSpeedToNetworkBytes(speed string) *byteformats.NetworkBytesCompat {
if speed == "" {
return nil
}
networkBytes := &byteformats.NetworkBytesCompat{}
if num, err := strconv.Atoi(speed); err == nil {
speed = F.ToString(num, "Mbps")
}
if err := networkBytes.UnmarshalJSON([]byte(speed)); err != nil {
return nil
}
return networkBytes
}
func clashTransport(network string, httpOpts HTTPOptions, h2Opts HTTP2Options, grpcOpts GrpcOptions, wsOpts WSOptions) *option.V2RayTransportOptions {
switch network {
case "http":
return &option.V2RayTransportOptions{
Type: C.V2RayTransportTypeHTTP,
HTTPOptions: option.V2RayHTTPOptions{
Method: httpOpts.Method,
Path: clashStringList(httpOpts.Path),
Headers: httpOpts.Headers,
},
}
case "h2":
return &option.V2RayTransportOptions{
Type: C.V2RayTransportTypeHTTP,
HTTPOptions: option.V2RayHTTPOptions{
Path: h2Opts.Path,
Host: h2Opts.Host,
},
}
case "grpc":
return &option.V2RayTransportOptions{
Type: C.V2RayTransportTypeGRPC,
GRPCOptions: option.V2RayGRPCOptions{
ServiceName: grpcOpts.GrpcServiceName,
},
}
case "ws":
headers := clashHeaders(wsOpts.Headers)
if wsOpts.V2rayHttpUpgrade {
var host string
if headers != nil && headers["Host"] != nil {
host = headers["Host"][0]
}
return &option.V2RayTransportOptions{
Type: C.V2RayTransportTypeHTTPUpgrade,
HTTPUpgradeOptions: option.V2RayHTTPUpgradeOptions{
Host: host,
Path: wsOpts.Path,
Headers: headers,
},
}
}
return &option.V2RayTransportOptions{
Type: C.V2RayTransportTypeWebsocket,
WebsocketOptions: option.V2RayWebsocketOptions{
Path: wsOpts.Path,
Headers: headers,
MaxEarlyData: uint32(wsOpts.MaxEarlyData),
EarlyDataHeaderName: wsOpts.EarlyDataHeaderName,
},
}
default:
return nil
}
}
func clashTLSOptions(server string, tlsOptions *TLSOptions) option.OutboundTLSOptionsContainer {
if tlsOptions != nil && tlsOptions.SNI == "" {
tlsOptions.SNI = server
}
return option.OutboundTLSOptionsContainer{
TLS: tlsOptions.Build(),
}
}
func trimStringArray(array []string) []string {
return common.Filter(array, func(it string) bool {
return strings.TrimSpace(it) != ""
})
}

49
parser/clash/vless.go Normal file
View File

@@ -0,0 +1,49 @@
package clash
import "github.com/sagernet/sing-box/option"
type VlessOption struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
*TLSOptions `yaml:",inline"`
UUID string `yaml:"uuid"`
Flow string `yaml:"flow,omitempty"`
UDP bool `yaml:"udp,omitempty"`
PacketAddr bool `yaml:"packet-addr,omitempty"`
XUDP bool `yaml:"xudp,omitempty"`
PacketEncoding string `yaml:"packet-encoding,omitempty"`
Network string `yaml:"network,omitempty"`
ServerName string `yaml:"servername,omitempty"`
HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
MuxOpts *MuxOptions `yaml:"smux,omitempty"`
}
func (v *VlessOption) Build() any {
if v.TLSOptions != nil {
v.SNI = v.ServerName
}
switch v.PacketEncoding {
case "":
if v.PacketAddr {
v.PacketEncoding = "packetaddr"
} else {
v.PacketEncoding = "xudp"
}
case "packet":
v.PacketEncoding = "packetaddr"
}
return &option.VLESSOutboundOptions{
DialerOptions: v.DialerOptions.Build(),
ServerOptions: v.ServerOptions.Build(),
UUID: v.UUID,
Flow: v.Flow,
Network: clashNetworks(v.UDP),
OutboundTLSOptionsContainer: clashTLSOptions(v.Server, v.TLSOptions),
Multiplex: v.MuxOpts.Build(),
Transport: clashTransport(v.Network, v.HTTPOpts, v.HTTP2Opts, v.GrpcOpts, v.WSOpts),
PacketEncoding: &v.PacketEncoding,
}
}

55
parser/clash/vmess.go Normal file
View File

@@ -0,0 +1,55 @@
package clash
import "github.com/sagernet/sing-box/option"
type VmessOption struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
*TLSOptions `yaml:",inline"`
UUID string `yaml:"uuid"`
AlterID int `yaml:"alterId"`
Cipher string `yaml:"cipher"`
UDP bool `yaml:"udp,omitempty"`
Network string `yaml:"network,omitempty"`
ServerName string `yaml:"servername,omitempty"`
HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
PacketAddr bool `yaml:"packet-addr,omitempty"`
XUDP bool `yaml:"xudp,omitempty"`
PacketEncoding string `yaml:"packet-encoding,omitempty"`
GlobalPadding bool `yaml:"global-padding,omitempty"`
AuthenticatedLength bool `yaml:"authenticated-length,omitempty"`
MuxOpts *MuxOptions `yaml:"smux,omitempty"`
}
func (v *VmessOption) Build() any {
if v.TLSOptions != nil {
v.SNI = v.ServerName
}
switch v.PacketEncoding {
case "":
if v.XUDP {
v.PacketEncoding = "xudp"
} else if v.PacketAddr {
v.PacketEncoding = "packetaddr"
}
case "packet":
v.PacketEncoding = "packetaddr"
}
return &option.VMessOutboundOptions{
DialerOptions: v.DialerOptions.Build(),
ServerOptions: v.ServerOptions.Build(),
UUID: v.UUID,
Security: v.Cipher,
AlterId: v.AlterID,
GlobalPadding: v.GlobalPadding,
AuthenticatedLength: v.AuthenticatedLength,
Network: clashNetworks(v.UDP),
OutboundTLSOptionsContainer: clashTLSOptions(v.Server, v.TLSOptions),
PacketEncoding: v.PacketEncoding,
Multiplex: v.MuxOpts.Build(),
Transport: clashTransport(v.Network, v.HTTPOpts, v.HTTP2Opts, v.GrpcOpts, v.WSOpts),
}
}

71
parser/link/hysteria.go Normal file
View File

@@ -0,0 +1,71 @@
package link
import (
"net/url"
"strconv"
"strings"
"github.com/sagernet/sing-box/common"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/byteformats"
)
func parseHysteriaLink(link string) (option.Outbound, error) {
linkURL, err := url.Parse(link)
if err != nil {
return option.Outbound{}, err
}
var options option.HysteriaOutboundOptions
TLSOptions := option.OutboundTLSOptions{
Enabled: true,
ECH: &option.OutboundECHOptions{},
UTLS: &option.OutboundUTLSOptions{},
Reality: &option.OutboundRealityOptions{},
}
options.Server = linkURL.Hostname()
TLSOptions.ServerName = linkURL.Hostname()
options.ServerPort = common.StringToType[uint16](linkURL.Port())
for key, values := range linkURL.Query() {
value := values[0]
switch key {
case "auth":
options.AuthString = value
case "peer", "sni":
TLSOptions.ServerName = value
case "alpn":
TLSOptions.ALPN = strings.Split(value, ",")
case "ca":
TLSOptions.CertificatePath = value
case "ca_str":
TLSOptions.Certificate = strings.Split(value, "\n")
case "up":
options.Up = &byteformats.NetworkBytesCompat{}
options.Up.UnmarshalJSON([]byte(value))
case "up_mbps":
options.UpMbps, _ = strconv.Atoi(value)
case "down":
options.Down = &byteformats.NetworkBytesCompat{}
options.Down.UnmarshalJSON([]byte(value))
case "down_mbps":
options.DownMbps, _ = strconv.Atoi(value)
case "obfs", "obfsParam":
options.Obfs = value
case "insecure", "skip-cert-verify":
if value == "1" || value == "true" {
TLSOptions.Insecure = true
}
case "tfo", "tcp-fast-open", "tcp_fast_open":
if value == "1" || value == "true" {
options.TCPFastOpen = true
}
}
}
outbound := option.Outbound{
Type: C.TypeHysteria,
Tag: linkURL.Fragment,
}
options.TLS = &TLSOptions
outbound.Options = &options
return outbound, nil
}

61
parser/link/hysteria2.go Normal file
View File

@@ -0,0 +1,61 @@
package link
import (
"net/url"
"strconv"
"github.com/sagernet/sing-box/common"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
)
func parseHysteria2Link(link string) (option.Outbound, error) {
linkURL, err := url.Parse(link)
if err != nil {
return option.Outbound{}, err
}
var options option.Hysteria2OutboundOptions
TLSOptions := option.OutboundTLSOptions{
Enabled: true,
ECH: &option.OutboundECHOptions{},
UTLS: &option.OutboundUTLSOptions{},
Reality: &option.OutboundRealityOptions{},
}
Obfs := &option.Hysteria2Obfs{}
options.ServerPort = uint16(443)
options.Server = linkURL.Hostname()
TLSOptions.ServerName = linkURL.Hostname()
if linkURL.User != nil {
options.Password = linkURL.User.Username()
}
if linkURL.Port() != "" {
options.ServerPort = common.StringToType[uint16](linkURL.Port())
}
for key, values := range linkURL.Query() {
value := values[0]
switch key {
case "up":
options.UpMbps, _ = strconv.Atoi(value)
case "down":
options.DownMbps, _ = strconv.Atoi(value)
case "obfs":
if value == "salamander" {
Obfs.Type = "salamander"
options.Obfs = Obfs
}
case "obfs-password":
Obfs.Password = value
case "insecure", "skip-cert-verify":
if value == "1" || value == "true" {
TLSOptions.Insecure = true
}
}
}
outbound := option.Outbound{
Type: C.TypeHysteria2,
Tag: linkURL.Fragment,
}
options.TLS = &TLSOptions
outbound.Options = &options
return outbound, nil
}

42
parser/link/parser.go Normal file
View File

@@ -0,0 +1,42 @@
package link
import (
"regexp"
"strings"
"github.com/sagernet/sing-box/common"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func ParseSubscriptionLink(link string) (option.Outbound, error) {
reg := regexp.MustCompile(`^(.*?)(://)(.*?)([@?#].*)?$`)
result := reg.FindStringSubmatch(link)
if result == nil {
return option.Outbound{}, E.New("invalid link")
}
scheme := result[1]
switch scheme {
case "tuic":
return parseTuicLink(link)
case "trojan":
return parseTrojanLink(link)
case "vless":
return parseVLESSLink(link)
case "hysteria":
return parseHysteriaLink(link)
case "hy2", "hysteria2":
return parseHysteria2Link(link)
}
result[3], _ = common.DecodeBase64URLSafe(result[3])
link = strings.Join(result[1:], "")
switch scheme {
case "ss":
return parseShadowsocksLink(link)
case "vmess":
return parseVMessLink(link)
default:
return option.Outbound{}, E.New("unsupported scheme: ", scheme)
}
}

View File

@@ -0,0 +1,39 @@
package link
import (
"net/url"
"github.com/sagernet/sing-box/common"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func parseShadowsocksLink(link string) (option.Outbound, error) {
linkURL, err := url.Parse(link)
if err != nil {
return option.Outbound{}, err
}
if linkURL.User == nil || linkURL.User.Username() == "" {
return option.Outbound{}, E.New("missing user info")
}
var options option.ShadowsocksOutboundOptions
options.ServerOptions.Server = linkURL.Hostname()
options.ServerOptions.ServerPort = common.StringToType[uint16](linkURL.Port())
password, _ := linkURL.User.Password()
if password == "" {
return option.Outbound{}, E.New("bad user info")
}
options.Method = linkURL.User.Username()
options.Password = password
plugin := linkURL.Query().Get("plugin")
options.Plugin = shadowsocksPluginName(plugin)
options.PluginOptions = shadowsocksPluginOptions(plugin)
outbound := option.Outbound{
Type: C.TypeShadowsocks,
Tag: linkURL.Fragment,
}
outbound.Options = &options
return outbound, nil
}

89
parser/link/trojan.go Normal file
View File

@@ -0,0 +1,89 @@
package link
import (
"net/url"
"strings"
"github.com/sagernet/sing-box/common"
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/json/badoption"
)
func parseTrojanLink(link string) (option.Outbound, error) {
linkURL, err := url.Parse(link)
if err != nil {
return option.Outbound{}, err
}
if linkURL.User == nil || linkURL.User.Username() == "" {
return option.Outbound{}, E.New("missing password")
}
var options option.TrojanOutboundOptions
TLSOptions := option.OutboundTLSOptions{
Enabled: true,
ECH: &option.OutboundECHOptions{},
UTLS: &option.OutboundUTLSOptions{},
Reality: &option.OutboundRealityOptions{},
}
options.Server = linkURL.Hostname()
TLSOptions.ServerName = linkURL.Hostname()
options.ServerPort = common.StringToType[uint16](linkURL.Port())
options.Password = linkURL.User.Username()
proxy := map[string]string{}
for key, values := range linkURL.Query() {
value := values[0]
proxy[key] = value
}
for key, value := range proxy {
switch key {
case "insecure", "allowInsecure", "skip-cert-verify":
if value == "1" || value == "true" {
TLSOptions.Insecure = true
}
case "serviceName", "sni", "peer":
TLSOptions.ServerName = value
case "alpn":
TLSOptions.ALPN = strings.Split(value, ",")
case "fp":
TLSOptions.UTLS.Enabled = true
TLSOptions.UTLS.Fingerprint = value
case "type":
Transport := option.V2RayTransportOptions{
Type: "",
WebsocketOptions: option.V2RayWebsocketOptions{
Headers: map[string]badoption.Listable[string]{},
},
HTTPOptions: option.V2RayHTTPOptions{
Host: badoption.Listable[string]{},
Headers: map[string]badoption.Listable[string]{},
},
GRPCOptions: option.V2RayGRPCOptions{},
}
switch value {
case "ws":
Transport.Type = C.V2RayTransportTypeWebsocket
Transport.WebsocketOptions = v2rayTransportWs(proxy["host"], proxy["path"])
case "grpc":
Transport.Type = C.V2RayTransportTypeGRPC
if serviceName, exists := proxy["grpc-service-name"]; exists && serviceName != "" {
Transport.GRPCOptions.ServiceName = serviceName
}
default:
continue
}
options.Transport = &Transport
case "tfo", "tcp-fast-open", "tcp_fast_open":
if value == "1" || value == "true" {
options.TCPFastOpen = true
}
}
}
outbound := option.Outbound{
Type: C.TypeTrojan,
Tag: linkURL.Fragment,
}
options.TLS = &TLSOptions
outbound.Options = &options
return outbound, nil
}

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