mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-05-14 00:51:12 +03:00
Add MTProxy, MASQUE, VPN, Link parser. Update AmneziaWG. Remove Tunneling
This commit is contained in:
@@ -20,6 +20,8 @@ builds:
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOTOOLCHAIN=local
|
||||
@@ -61,6 +63,8 @@ builds:
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
env:
|
||||
@@ -97,6 +101,8 @@ builds:
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
targets:
|
||||
|
||||
39
README.md
39
README.md
@@ -4,34 +4,37 @@ Sing-box with extended features.
|
||||
|
||||
## 🔥 Features
|
||||
|
||||
### 🌐 Outbounds
|
||||
- **WARP** — Cloudflare WARP integration through WireGuard
|
||||
- **Tunnel** — Protocol for creating tunnels across nodes
|
||||
- **Bond** — Link aggregation for increased throughput
|
||||
- **Mieru** — Secure, hard to classify, hard to probe network protocol
|
||||
- **Failover** — Automatic outbound switching for high availability
|
||||
### 🌐 Protocols
|
||||
- **WARP**
|
||||
- **Masque**
|
||||
- **MTProxy**
|
||||
- **Mieru**
|
||||
- **VPN**
|
||||
- **Bond**
|
||||
- **Fallback**
|
||||
|
||||
### 🚦 Limiters
|
||||
- **Bandwidth Limiter** — Upload / download rate limiting
|
||||
- **Connection Limiter** — Concurrent connection control
|
||||
- **Bandwidth Limiter**
|
||||
- **Connection Limiter**
|
||||
|
||||
### 🛡 Encryption & Obfuscation
|
||||
- **Amnezia 1.5** — WireGuard traffic obfuscation
|
||||
- **VLESS encryption** — XRAY encryption for VLESS protocol
|
||||
- **Amnezia 2.0**
|
||||
- **VLESS encryption**
|
||||
|
||||
### 🔄 Transports
|
||||
- **mKCP** — Reliable UDP-based transport
|
||||
- **XHTTP** — Modern XRAY transport
|
||||
- **mKCP**
|
||||
- **XHTTP**
|
||||
|
||||
### 🛠 Services
|
||||
- **Admin Panel** — Web-based management interface
|
||||
- **Manager** — Management service for configuring squads, nodes, users, limiters
|
||||
- **Node Manager** — Service for connecting nodes to remote manager
|
||||
- **Admin Panel**
|
||||
- **Manager**
|
||||
- **Node Manager**
|
||||
|
||||
### ⚙ Miscellaneous
|
||||
- **SDNS (DNSCrypt)** — Encrypted DNS queries for enhanced privacy
|
||||
- **Extended WireGuard options** — Advanced configuration capabilities
|
||||
- **Unified Delay** — Unified latency measurement
|
||||
- **Link parser**
|
||||
- **SDNS (DNSCrypt)**
|
||||
- **Extended WireGuard options**
|
||||
- **Unified Delay**
|
||||
|
||||
## 📚 Examples
|
||||
|
||||
|
||||
@@ -68,6 +68,8 @@ type DNSTransport interface {
|
||||
Type() string
|
||||
Tag() 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()
|
||||
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ type CacheFile interface {
|
||||
RDRCStore
|
||||
|
||||
StoreWARPConfig() bool
|
||||
StoreMASQUEConfig() bool
|
||||
|
||||
LoadMode() string
|
||||
StoreMode(mode string) error
|
||||
@@ -59,6 +60,10 @@ type CacheFile interface {
|
||||
SaveRuleSet(tag string, set *SavedBinary) error
|
||||
LoadWARPConfig(tag string) *SavedBinary
|
||||
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 {
|
||||
|
||||
@@ -42,16 +42,15 @@ type InboundManager interface {
|
||||
}
|
||||
|
||||
type InboundContext struct {
|
||||
Inbound string
|
||||
InboundType string
|
||||
IPVersion uint8
|
||||
Network string
|
||||
Source M.Socksaddr
|
||||
Destination M.Socksaddr
|
||||
TunnelSource string
|
||||
TunnelDestination string
|
||||
User string
|
||||
Outbound string
|
||||
Inbound string
|
||||
InboundType string
|
||||
IPVersion uint8
|
||||
Network string
|
||||
Source M.Socksaddr
|
||||
Destination M.Socksaddr
|
||||
Gateway *netip.Addr
|
||||
User string
|
||||
Outbound string
|
||||
|
||||
// sniffer
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
@@ -36,6 +38,8 @@ type PlatformInterface interface {
|
||||
|
||||
UsePlatformNotification() bool
|
||||
SendNotification(notification *Notification) error
|
||||
|
||||
MyInterfaceAddress() []netip.Addr
|
||||
}
|
||||
|
||||
type FindConnectionOwnerRequest struct {
|
||||
|
||||
51
adapter/provider.go
Normal file
51
adapter/provider.go
Normal 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
267
adapter/provider/adapter.go
Normal 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
157
adapter/provider/manager.go
Normal 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
|
||||
}
|
||||
72
adapter/provider/registry.go
Normal file
72
adapter/provider/registry.go
Normal 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
47
box.go
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/adapter/provider"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/certificate"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
@@ -44,6 +45,7 @@ type Box struct {
|
||||
endpoint *endpoint.Manager
|
||||
inbound *inbound.Manager
|
||||
outbound *outbound.Manager
|
||||
provider *provider.Manager
|
||||
service *boxService.Manager
|
||||
dnsTransport *dns.TransportManager
|
||||
dnsRouter *dns.Router
|
||||
@@ -64,6 +66,7 @@ func Context(
|
||||
inboundRegistry adapter.InboundRegistry,
|
||||
outboundRegistry adapter.OutboundRegistry,
|
||||
endpointRegistry adapter.EndpointRegistry,
|
||||
providerRegistry adapter.ProviderRegistry,
|
||||
dnsTransportRegistry adapter.DNSTransportRegistry,
|
||||
serviceRegistry adapter.ServiceRegistry,
|
||||
) context.Context {
|
||||
@@ -82,6 +85,11 @@ func Context(
|
||||
ctx = service.ContextWith[option.EndpointOptionsRegistry](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 {
|
||||
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](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)
|
||||
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
|
||||
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
||||
providerRegistry := service.FromContext[adapter.ProviderRegistry](ctx)
|
||||
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
|
||||
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
|
||||
|
||||
@@ -116,6 +125,9 @@ func New(options Options) (*Box, error) {
|
||||
if outboundRegistry == nil {
|
||||
return nil, E.New("missing outbound registry in context")
|
||||
}
|
||||
if providerRegistry == nil {
|
||||
return nil, E.New("missing provider registry in context")
|
||||
}
|
||||
if dnsTransportRegistry == nil {
|
||||
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)
|
||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||
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)
|
||||
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
|
||||
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
||||
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
||||
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
||||
service.MustRegister[adapter.ProviderManager](ctx, providerManager)
|
||||
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
||||
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
||||
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
@@ -276,6 +290,10 @@ func New(options Options) (*Box, error) {
|
||||
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 {
|
||||
var tag string
|
||||
if outboundOptions.Tag != "" {
|
||||
@@ -302,6 +320,25 @@ func New(options Options) (*Box, error) {
|
||||
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 {
|
||||
var tag string
|
||||
if serviceOptions.Tag != "" {
|
||||
@@ -392,6 +429,7 @@ func New(options Options) (*Box, error) {
|
||||
endpoint: endpointManager,
|
||||
inbound: inboundManager,
|
||||
outbound: outboundManager,
|
||||
provider: providerManager,
|
||||
dnsTransport: dnsTransportManager,
|
||||
service: serviceManager,
|
||||
dnsRouter: dnsRouter,
|
||||
@@ -455,11 +493,11 @@ func (s *Box) preStart() error {
|
||||
if err != nil {
|
||||
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 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -479,7 +517,7 @@ func (s *Box) start() error {
|
||||
if err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -487,7 +525,7 @@ func (s *Box) start() error {
|
||||
if err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -513,6 +551,7 @@ func (s *Box) Close() error {
|
||||
{"service", s.service},
|
||||
{"endpoint", s.endpoint},
|
||||
{"inbound", s.inbound},
|
||||
{"provider", s.provider},
|
||||
{"outbound", s.outbound},
|
||||
{"router", s.router},
|
||||
{"connection", s.connection},
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
package cloudflare
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type CloudflareApi struct {
|
||||
@@ -25,50 +22,93 @@ func NewCloudflareApi(opts ...CloudflareApiOption) *CloudflareApi {
|
||||
}
|
||||
|
||||
func (api *CloudflareApi) CreateProfile(ctx context.Context, publicKey string) (*CloudflareProfile, error) {
|
||||
request, err := http.NewRequest("POST", "https://api.cloudflareclient.com/v0i1909051800/reg", strings.NewReader(
|
||||
fmt.Sprintf(
|
||||
"{\"install_id\":\"\",\"tos\":\"%s\",\"key\":\"%s\",\"fcm_token\":\"\",\"type\":\"ios\",\"locale\":\"en_US\"}",
|
||||
time.Now().Format("2006-01-02T15:04:05.000Z"),
|
||||
publicKey,
|
||||
),
|
||||
))
|
||||
serial, err := GenerateRandomAndroidSerial()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate serial: %v", err)
|
||||
}
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range Headers {
|
||||
request.Header.Set(k, v)
|
||||
}
|
||||
response, err := api.client.Do(request.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("status code is not 200")
|
||||
}
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to register: %v", response.StatusCode)
|
||||
}
|
||||
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) {
|
||||
request, err := http.NewRequest("GET", "https://api.cloudflareclient.com/v0i1909051800/reg/"+id, nil)
|
||||
func (api *CloudflareApi) EnrollKey(ctx context.Context, authToken string, id string, keyType, tunType, publicKey string) (*CloudflareProfile, error) {
|
||||
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 {
|
||||
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 != 200 {
|
||||
return nil, fmt.Errorf("status code is not 200")
|
||||
if response.StatusCode != http.StatusOK {
|
||||
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 {
|
||||
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)
|
||||
return profile, json.NewDecoder(strings.NewReader(gjson.Get(string(content), "result").Raw)).Decode(profile)
|
||||
return profile, json.NewDecoder(response.Body).Decode(profile)
|
||||
}
|
||||
|
||||
25
common/cloudflare/constant.go
Normal file
25
common/cloudflare/constant.go
Normal 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
132
common/cloudflare/models.go
Normal 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, ",")
|
||||
}
|
||||
@@ -4,12 +4,14 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CloudflareApiOption func(api *CloudflareApi)
|
||||
|
||||
func WithDialContext(dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) CloudflareApiOption {
|
||||
return func(api *CloudflareApi) {
|
||||
api.client.Timeout = 30 * time.Second
|
||||
api.client.Transport = &http.Transport{
|
||||
DialContext: dialContext,
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
19
common/cloudflare/utils.go
Normal file
19
common/cloudflare/utils.go
Normal 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")
|
||||
}
|
||||
@@ -11,3 +11,13 @@ func ContextWithIsExternalConnection(ctx context.Context) context.Context {
|
||||
func IsExternalConnectionFromContext(ctx context.Context) bool {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -17,30 +17,31 @@ type Group struct {
|
||||
type groupConnItem struct {
|
||||
conn io.Closer
|
||||
isExternal bool
|
||||
isProvider bool
|
||||
}
|
||||
|
||||
func NewGroup() *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()
|
||||
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}
|
||||
}
|
||||
|
||||
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()
|
||||
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}
|
||||
}
|
||||
|
||||
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()
|
||||
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}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ type klock struct {
|
||||
ref uint64
|
||||
}
|
||||
|
||||
// Create new Kmutex
|
||||
func New[T comparable]() *Kmutex[T] {
|
||||
l := sync.Mutex{}
|
||||
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) {
|
||||
km.l.Lock()
|
||||
defer km.l.Unlock()
|
||||
@@ -37,7 +35,6 @@ func (km *Kmutex[T]) Unlock(key T) {
|
||||
kl.cond.Signal()
|
||||
}
|
||||
|
||||
// Lock Kmutex by unique ID
|
||||
func (km *Kmutex[T]) Lock(key T) {
|
||||
km.l.Lock()
|
||||
defer km.l.Unlock()
|
||||
|
||||
74
common/tls/masque_client.go
Normal file
74
common/tls/masque_client.go
Normal 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
68
common/utils.go
Normal 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
20
constant/provider.go
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,9 @@ const (
|
||||
TypeNaive = "naive"
|
||||
TypeWireGuard = "wireguard"
|
||||
TypeWARP = "warp"
|
||||
TypeMASQUE = "masque"
|
||||
TypeMTProxy = "mtproxy"
|
||||
TypeParser = "parser"
|
||||
TypeHysteria = "hysteria"
|
||||
TypeTor = "tor"
|
||||
TypeSSH = "ssh"
|
||||
@@ -27,8 +30,8 @@ const (
|
||||
TypeTUIC = "tuic"
|
||||
TypeHysteria2 = "hysteria2"
|
||||
TypeBond = "bond"
|
||||
TypeTunnelServer = "tunnel-server"
|
||||
TypeTunnelClient = "tunnel-client"
|
||||
TypeVPNServer = "vpn-server"
|
||||
TypeVPNClient = "vpn-client"
|
||||
TypeTailscale = "tailscale"
|
||||
TypeConnectionLimiter = "connection-limiter"
|
||||
TypeBandwidthLimiter = "bandwidth-limiter"
|
||||
@@ -47,7 +50,7 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
TypeFailover = "failover"
|
||||
TypeFallback = "fallback"
|
||||
TypeSelector = "selector"
|
||||
TypeURLTest = "urltest"
|
||||
)
|
||||
@@ -84,6 +87,12 @@ func ProxyDisplayName(proxyType string) string {
|
||||
return "WireGuard"
|
||||
case TypeWARP:
|
||||
return "WARP"
|
||||
case TypeMASQUE:
|
||||
return "MASQUE"
|
||||
case TypeMTProxy:
|
||||
return "MTProxy"
|
||||
case TypeParser:
|
||||
return "Parser"
|
||||
case TypeHysteria:
|
||||
return "Hysteria"
|
||||
case TypeTor:
|
||||
@@ -106,18 +115,18 @@ func ProxyDisplayName(proxyType string) string {
|
||||
return "Mieru"
|
||||
case TypeAnyTLS:
|
||||
return "AnyTLS"
|
||||
case TypeFailover:
|
||||
return "Failover"
|
||||
case TypeFallback:
|
||||
return "Fallback"
|
||||
case TypeTailscale:
|
||||
return "Tailscale"
|
||||
case TypeSelector:
|
||||
return "Selector"
|
||||
case TypeURLTest:
|
||||
return "URLTest"
|
||||
case TypeTunnelClient:
|
||||
return "Tunnel client"
|
||||
case TypeTunnelServer:
|
||||
return "Tunnel server"
|
||||
case TypeVPNClient:
|
||||
return "VPN Client"
|
||||
case TypeVPNServer:
|
||||
return "VPN Server"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
547
dns/transport/conn_pool.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -31,14 +31,13 @@ func RegisterTransport(registry *dns.TransportRegistry) {
|
||||
}
|
||||
|
||||
type Transport struct {
|
||||
*transport.BaseTransport
|
||||
dns.TransportAdapter
|
||||
|
||||
ctx context.Context
|
||||
dialer N.Dialer
|
||||
serverAddr M.Socksaddr
|
||||
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) {
|
||||
@@ -63,93 +62,76 @@ func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
return nil, E.New("invalid server address: ", serverAddr)
|
||||
}
|
||||
|
||||
t := &Transport{
|
||||
BaseTransport: transport.NewBaseTransport(
|
||||
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions),
|
||||
logger,
|
||||
),
|
||||
ctx: ctx,
|
||||
dialer: transportDialer,
|
||||
serverAddr: serverAddr,
|
||||
tlsConfig: tlsConfig,
|
||||
}
|
||||
|
||||
t.connector = transport.NewConnector(t.CloseContext(), t.dial, transport.ConnectorCallbacks[*quic.Conn]{
|
||||
IsClosed: func(connection *quic.Conn) bool {
|
||||
return common.Done(connection.Context())
|
||||
},
|
||||
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
|
||||
return &Transport{
|
||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions),
|
||||
dialer: transportDialer,
|
||||
serverAddr: serverAddr,
|
||||
tlsConfig: tlsConfig,
|
||||
connection: transport.NewConnPool(transport.ConnPoolOptions[*quic.Conn]{
|
||||
Mode: transport.ConnPoolSingle,
|
||||
IsAlive: func(conn *quic.Conn) bool {
|
||||
return conn != nil && !common.Done(conn.Context())
|
||||
},
|
||||
Close: func(conn *quic.Conn, _ error) {
|
||||
conn.CloseWithError(0, "")
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Transport) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
err := t.SetStarted()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dialer.InitializeDetour(t.dialer)
|
||||
}
|
||||
|
||||
func (t *Transport) Close() error {
|
||||
return E.Errors(t.BaseTransport.Close(), t.connector.Close())
|
||||
return t.connection.Close()
|
||||
}
|
||||
|
||||
func (t *Transport) Reset() {
|
||||
t.connector.Reset()
|
||||
t.connection.Reset()
|
||||
}
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
if !t.BeginQuery() {
|
||||
return nil, transport.ErrTransportClosed
|
||||
}
|
||||
defer t.EndQuery()
|
||||
|
||||
var (
|
||||
conn *quic.Conn
|
||||
err error
|
||||
response *mDNS.Msg
|
||||
)
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
response, err = t.exchange(ctx, message, conn)
|
||||
if err == nil {
|
||||
t.connection.Release(conn, true)
|
||||
return response, nil
|
||||
} else if !isQUICRetryError(err) {
|
||||
t.connection.Release(conn, true)
|
||||
return nil, err
|
||||
} else {
|
||||
t.connector.Reset()
|
||||
t.connection.Release(conn, true)
|
||||
t.Reset()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -17,7 +16,6 @@ import (
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
@@ -29,13 +27,13 @@ func RegisterTLS(registry *dns.TransportRegistry) {
|
||||
}
|
||||
|
||||
type TLSTransport struct {
|
||||
*BaseTransport
|
||||
dns.TransportAdapter
|
||||
logger logger.ContextLogger
|
||||
|
||||
dialer tls.Dialer
|
||||
serverAddr M.Socksaddr
|
||||
tlsConfig tls.Config
|
||||
access sync.Mutex
|
||||
connections list.List[*tlsDNSConn]
|
||||
connections *ConnPool[*tlsDNSConn]
|
||||
}
|
||||
|
||||
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 {
|
||||
return &TLSTransport{
|
||||
BaseTransport: NewBaseTransport(adapter, logger),
|
||||
dialer: tls.NewDialer(dialer, tlsConfig),
|
||||
serverAddr: serverAddr,
|
||||
tlsConfig: tlsConfig,
|
||||
TransportAdapter: adapter,
|
||||
logger: logger,
|
||||
dialer: tls.NewDialer(dialer, 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 {
|
||||
return nil
|
||||
}
|
||||
err := t.SetStarted()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dialer.InitializeDetour(t.dialer)
|
||||
}
|
||||
|
||||
func (t *TLSTransport) Close() error {
|
||||
t.access.Lock()
|
||||
for connection := t.connections.Front(); connection != nil; connection = connection.Next() {
|
||||
connection.Value.Close()
|
||||
}
|
||||
t.connections.Init()
|
||||
t.access.Unlock()
|
||||
return t.BaseTransport.Close()
|
||||
return t.connections.Close()
|
||||
}
|
||||
|
||||
func (t *TLSTransport) Reset() {
|
||||
t.access.Lock()
|
||||
defer t.access.Unlock()
|
||||
for connection := t.connections.Front(); connection != nil; connection = connection.Next() {
|
||||
connection.Value.Close()
|
||||
}
|
||||
t.connections.Init()
|
||||
t.connections.Reset()
|
||||
}
|
||||
|
||||
func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
if !t.BeginQuery() {
|
||||
return nil, ErrTransportClosed
|
||||
}
|
||||
defer t.EndQuery()
|
||||
|
||||
t.access.Lock()
|
||||
conn := t.connections.PopFront()
|
||||
t.access.Unlock()
|
||||
if conn != nil {
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
conn, created, err := t.connections.Acquire(ctx, func(ctx context.Context) (*tlsDNSConn, error) {
|
||||
tlsConn, err := t.dialer.DialTLSContext(ctx, t.serverAddr)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "dial TLS connection")
|
||||
}
|
||||
return &tlsDNSConn{Conn: tlsConn}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response, err := t.exchange(ctx, message, conn)
|
||||
if err == nil {
|
||||
t.connections.Release(conn, true)
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "dial TLS connection")
|
||||
}
|
||||
return t.exchange(ctx, message, &tlsDNSConn{Conn: tlsConn})
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
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++
|
||||
err := WriteMessage(conn, conn.queryId, message)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, E.Cause(err, "write request")
|
||||
}
|
||||
response, err := ReadMessage(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
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{})
|
||||
t.connections.PushBack(conn)
|
||||
t.access.Unlock()
|
||||
return response, nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
@@ -27,13 +28,14 @@ func RegisterUDP(registry *dns.TransportRegistry) {
|
||||
}
|
||||
|
||||
type UDPTransport struct {
|
||||
*BaseTransport
|
||||
dns.TransportAdapter
|
||||
logger logger.ContextLogger
|
||||
|
||||
dialer N.Dialer
|
||||
serverAddr M.Socksaddr
|
||||
udpSize atomic.Int32
|
||||
|
||||
connector *Connector[*Connection]
|
||||
connection *ConnPool[net.Conn]
|
||||
|
||||
callbackAccess sync.RWMutex
|
||||
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 {
|
||||
t := &UDPTransport{
|
||||
BaseTransport: NewBaseTransport(adapter, logger),
|
||||
dialer: dialerInstance,
|
||||
serverAddr: serverAddr,
|
||||
callbacks: make(map[uint16]*udpCallback),
|
||||
TransportAdapter: adapter,
|
||||
logger: logger,
|
||||
dialer: dialerInstance,
|
||||
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.connector = NewSingleflightConnector(t.CloseContext(), t.dial)
|
||||
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 {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
err := t.SetStarted()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dialer.InitializeDetour(t.dialer)
|
||||
}
|
||||
|
||||
func (t *UDPTransport) Close() error {
|
||||
return E.Errors(t.BaseTransport.Close(), t.connector.Close())
|
||||
return t.connection.Close()
|
||||
}
|
||||
|
||||
func (t *UDPTransport) Reset() {
|
||||
t.connector.Reset()
|
||||
t.connection.Reset()
|
||||
}
|
||||
|
||||
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) {
|
||||
if !t.BeginQuery() {
|
||||
return nil, ErrTransportClosed
|
||||
}
|
||||
defer t.EndQuery()
|
||||
|
||||
response, err := t.exchange(ctx, message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 response, nil
|
||||
@@ -158,16 +150,25 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
|
||||
break
|
||||
}
|
||||
if t.udpSize.CompareAndSwap(current, udpSize) {
|
||||
t.connector.Reset()
|
||||
t.Reset()
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
if created {
|
||||
go t.recvLoop(conn)
|
||||
}
|
||||
|
||||
callback := &udpCallback{
|
||||
done: make(chan struct{}),
|
||||
@@ -177,6 +178,7 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
|
||||
queryId, err := t.nextAvailableQueryId()
|
||||
if err != nil {
|
||||
t.callbackAccess.Unlock()
|
||||
t.connection.Release(conn, true)
|
||||
return nil, err
|
||||
}
|
||||
t.callbacks[queryId] = callback
|
||||
@@ -203,30 +205,30 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
|
||||
|
||||
_, err = conn.Write(rawMessage)
|
||||
if err != nil {
|
||||
conn.CloseWithError(err)
|
||||
t.connection.Invalidate(conn, err)
|
||||
return nil, E.Cause(err, "write request")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-callback.done:
|
||||
t.connection.Release(conn, true)
|
||||
callback.response.Id = originalId
|
||||
return callback.response, nil
|
||||
case <-conn.Done():
|
||||
return nil, conn.CloseError()
|
||||
case <-t.CloseContext().Done():
|
||||
return nil, ErrTransportClosed
|
||||
case <-connCtx.Done():
|
||||
return nil, context.Cause(connCtx)
|
||||
case <-ctx.Done():
|
||||
t.connection.Release(conn, true)
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *UDPTransport) recvLoop(conn *Connection) {
|
||||
func (t *UDPTransport) recvLoop(conn net.Conn) {
|
||||
for {
|
||||
buffer := buf.NewSize(int(t.udpSize.Load()))
|
||||
_, err := buffer.ReadOnceFrom(conn)
|
||||
if err != nil {
|
||||
buffer.Release()
|
||||
conn.CloseWithError(err)
|
||||
t.connection.Invalidate(conn, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -234,7 +236,7 @@ func (t *UDPTransport) recvLoop(conn *Connection) {
|
||||
err = message.Unpack(buffer.Bytes())
|
||||
buffer.Release()
|
||||
if err != nil {
|
||||
t.Logger.Debug("discarded malformed UDP response: ", err)
|
||||
t.logger.Debug("discarded malformed UDP response: ", err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.13.11
|
||||
|
||||
* Fix process searcher failure introduced in 1.13.9
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.10
|
||||
|
||||
* Fix process searcher failure introduced in 1.13.9
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
"uuid": "257f20d0-294a-4f07-9f2c-9efee9a37400"
|
||||
},
|
||||
{
|
||||
"type": "failover",
|
||||
"tag": "failover-out",
|
||||
"type": "fallback",
|
||||
"tag": "fallback-out",
|
||||
"outbounds": [
|
||||
"vless-1-out",
|
||||
"vless-2-out",
|
||||
@@ -54,7 +54,7 @@
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "failover-out",
|
||||
"final": "fallback-out",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
@@ -15,22 +15,14 @@
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"port": 53,
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
"action": "hijack-dns"
|
||||
}
|
||||
],
|
||||
"final": "direct-out"
|
||||
},
|
||||
|
||||
@@ -26,10 +26,6 @@
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
},
|
||||
{
|
||||
"type": "bandwidth-limiter",
|
||||
"tag": "bandwidth-limiter",
|
||||
@@ -51,11 +47,7 @@
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"port": 53,
|
||||
"outbound": "dns-out"
|
||||
"action": "hijack-dns"
|
||||
}
|
||||
],
|
||||
"final": "connection-limiter"
|
||||
|
||||
58
examples/masque/client.json
Normal file
58
examples/masque/client.json
Normal 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
|
||||
}
|
||||
}
|
||||
83
examples/mtproxy/server.json
Normal file
83
examples/mtproxy/server.json
Normal 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
|
||||
}
|
||||
}
|
||||
37
examples/parser/client.json
Normal file
37
examples/parser/client.json
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "info"
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
@@ -12,9 +12,9 @@
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel-client",
|
||||
"tag": "tunnel",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"type": "vpn-client",
|
||||
"tag": "vpn",
|
||||
"address": "10.0.0.2",
|
||||
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe",
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
@@ -30,33 +30,26 @@
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 10000
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
},
|
||||
{
|
||||
"type": "failover",
|
||||
"tag": "f",
|
||||
"outbounds": ["tunnel", "direct-out"],
|
||||
"interrupt_exist_connections": false,
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"outbound": "f",
|
||||
"override_tunnel_destination": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13"
|
||||
"protocol": "dns",
|
||||
"action": "hijack-dns"
|
||||
},
|
||||
{
|
||||
"outbound": "vpn",
|
||||
}
|
||||
],
|
||||
"final": "f",
|
||||
"final": "direct-out",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
51
examples/vpn/client-server/server.json
Normal file
51
examples/vpn/client-server/server.json
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,9 @@
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel-client",
|
||||
"tag": "tunnel",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"type": "vpn-client",
|
||||
"tag": "vpn",
|
||||
"address": "10.0.0.2",
|
||||
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe",
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
@@ -42,8 +42,8 @@
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"outbound": "tunnel",
|
||||
"override_tunnel_destination": "487f6073-3300-4819-a07d-39652e45fb4d"
|
||||
"outbound": "vpn",
|
||||
"override_gateway": "10.0.0.3"
|
||||
}
|
||||
],
|
||||
"final": "direct-out",
|
||||
@@ -12,9 +12,9 @@
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel-client",
|
||||
"tag": "tunnel",
|
||||
"uuid": "487f6073-3300-4819-a07d-39652e45fb4d",
|
||||
"type": "vpn-client",
|
||||
"tag": "vpn",
|
||||
"address": "10.0.0.3",
|
||||
"key": "3d74d616-2502-4c17-9cc3-92c366550f4f",
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
61
examples/vpn/client1-server-client2/server.json
Normal file
61
examples/vpn/client1-server-client2/server.json
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -29,10 +29,6 @@
|
||||
"server_port": 8000,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"network": "tcp"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
@@ -12,12 +12,12 @@
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel-server",
|
||||
"tag": "tunnel",
|
||||
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13",
|
||||
"type": "vpn-server",
|
||||
"tag": "vpn",
|
||||
"address": "10.0.0.1",
|
||||
"users": [
|
||||
{
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"address": "10.0.0.2",
|
||||
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe"
|
||||
}
|
||||
],
|
||||
@@ -45,8 +45,8 @@
|
||||
"rules": [
|
||||
{
|
||||
"inbound": "vless-in",
|
||||
"outbound": "tunnel",
|
||||
"override_tunnel_destination": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
"outbound": "vpn",
|
||||
"override_gateway": "10.0.0.2"
|
||||
}
|
||||
],
|
||||
"final": "direct-out",
|
||||
@@ -12,9 +12,9 @@
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel-client",
|
||||
"tag": "tunnel",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"type": "vpn-client",
|
||||
"tag": "vpn",
|
||||
"address": "10.0.0.2",
|
||||
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe",
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
@@ -12,9 +12,9 @@
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel-client",
|
||||
"tag": "tunnel",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"type": "vpn-client",
|
||||
"tag": "vpn",
|
||||
"address": "10.0.0.2",
|
||||
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe",
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
@@ -12,12 +12,12 @@
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel-server",
|
||||
"tag": "tunnel",
|
||||
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13",
|
||||
"type": "vpn-server",
|
||||
"tag": "vpn",
|
||||
"address": "10.0.0.1",
|
||||
"users": [
|
||||
{
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"address": "10.0.0.2",
|
||||
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe"
|
||||
}
|
||||
],
|
||||
@@ -51,8 +51,8 @@
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"outbound": "tunnel",
|
||||
"override_tunnel_destination": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
"outbound": "vpn",
|
||||
"override_gateway": "10.0.0.2"
|
||||
}
|
||||
],
|
||||
"final": "direct-out",
|
||||
@@ -44,6 +44,7 @@ type CacheFile struct {
|
||||
storeFakeIP bool
|
||||
storeRDRC bool
|
||||
storeWARPConfig bool
|
||||
storeMASQUEConfig bool
|
||||
rdrcTimeout time.Duration
|
||||
DB *bbolt.DB
|
||||
resetAccess sync.Mutex
|
||||
@@ -82,17 +83,18 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
|
||||
}
|
||||
}
|
||||
return &CacheFile{
|
||||
ctx: ctx,
|
||||
path: filemanager.BasePath(ctx, path),
|
||||
cacheID: cacheIDBytes,
|
||||
storeFakeIP: options.StoreFakeIP,
|
||||
storeRDRC: options.StoreRDRC,
|
||||
storeWARPConfig: options.StoreWARPConfig,
|
||||
rdrcTimeout: rdrcTimeout,
|
||||
saveDomain: make(map[netip.Addr]string),
|
||||
saveAddress4: make(map[string]netip.Addr),
|
||||
saveAddress6: make(map[string]netip.Addr),
|
||||
saveRDRC: make(map[saveRDRCCacheKey]bool),
|
||||
ctx: ctx,
|
||||
path: filemanager.BasePath(ctx, path),
|
||||
cacheID: cacheIDBytes,
|
||||
storeFakeIP: options.StoreFakeIP,
|
||||
storeRDRC: options.StoreRDRC,
|
||||
storeWARPConfig: options.StoreWARPConfig,
|
||||
storeMASQUEConfig: options.StoreMASQUEConfig,
|
||||
rdrcTimeout: rdrcTimeout,
|
||||
saveDomain: make(map[netip.Addr]string),
|
||||
saveAddress4: make(map[string]netip.Addr),
|
||||
saveAddress6: make(map[string]netip.Addr),
|
||||
saveRDRC: make(map[saveRDRCCacheKey]bool),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,6 +368,10 @@ func (c *CacheFile) StoreWARPConfig() bool {
|
||||
return c.storeWARPConfig
|
||||
}
|
||||
|
||||
func (c *CacheFile) StoreMASQUEConfig() bool {
|
||||
return c.storeMASQUEConfig
|
||||
}
|
||||
|
||||
func (c *CacheFile) LoadWARPConfig(tag string) *adapter.SavedBinary {
|
||||
var savedConfig adapter.SavedBinary
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,48 +4,78 @@ import (
|
||||
"context"
|
||||
"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/render"
|
||||
)
|
||||
|
||||
func proxyProviderRouter() http.Handler {
|
||||
func proxyProviderRouter(server *Server) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/", getProviders)
|
||||
r.Get("/", getProviders(server))
|
||||
|
||||
r.Route("/{name}", func(r chi.Router) {
|
||||
r.Use(parseProviderName, findProviderByName)
|
||||
r.Get("/", getProvider)
|
||||
r.Use(parseProviderName, findProviderByName(server))
|
||||
r.Get("/", getProvider(server))
|
||||
r.Put("/", updateProvider)
|
||||
r.Get("/healthcheck", healthCheckProvider)
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
func getProviders(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, render.M{
|
||||
"providers": render.M{},
|
||||
})
|
||||
func getProviders(server *Server) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
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) {
|
||||
/*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider)
|
||||
render.JSON(w, r, provider)*/
|
||||
render.NoContent(w, r)
|
||||
func getProvider(server *Server) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
provider := r.Context().Value(CtxKeyProvider).(adapter.Provider)
|
||||
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) {
|
||||
/*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider)
|
||||
if err := provider.Update(); err != nil {
|
||||
render.Status(r, http.StatusServiceUnavailable)
|
||||
render.JSON(w, r, newError(err.Error()))
|
||||
return
|
||||
}*/
|
||||
provider := r.Context().Value(CtxKeyProvider).(adapter.Provider)
|
||||
if provider, isUpdater := provider.(adapter.ProviderUpdater); isUpdater {
|
||||
if err := provider.Update(); err != nil {
|
||||
render.Status(r, http.StatusServiceUnavailable)
|
||||
render.JSON(w, r, newError(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
render.NoContent(w, r)
|
||||
}
|
||||
|
||||
func healthCheckProvider(w http.ResponseWriter, r *http.Request) {
|
||||
/*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider)
|
||||
provider.HealthCheck()*/
|
||||
provider := r.Context().Value(CtxKeyProvider).(adapter.Provider)
|
||||
provider.HealthCheck(r.Context())
|
||||
render.NoContent(w, r)
|
||||
}
|
||||
|
||||
@@ -57,18 +87,19 @@ func parseProviderName(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func findProviderByName(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
/*name := r.Context().Value(CtxKeyProviderName).(string)
|
||||
providers := tunnel.ProxyProviders()
|
||||
provider, exist := providers[name]
|
||||
if !exist {*/
|
||||
render.Status(r, http.StatusNotFound)
|
||||
render.JSON(w, r, ErrNotFound)
|
||||
//return
|
||||
//}
|
||||
func findProviderByName(server *Server) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.Context().Value(CtxKeyProviderName).(string)
|
||||
provider, exist := server.provider.Get(name)
|
||||
if !exist {
|
||||
render.Status(r, http.StatusNotFound)
|
||||
render.JSON(w, r, ErrNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// ctx := context.WithValue(r.Context(), CtxKeyProvider, provider)
|
||||
// next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
ctx := context.WithValue(r.Context(), CtxKeyProvider, provider)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ type Server struct {
|
||||
dnsRouter adapter.DNSRouter
|
||||
outbound adapter.OutboundManager
|
||||
endpoint adapter.EndpointManager
|
||||
provider adapter.ProviderManager
|
||||
logger log.Logger
|
||||
httpServer *http.Server
|
||||
trafficManager *trafficontrol.Manager
|
||||
@@ -71,6 +72,7 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
|
||||
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
|
||||
outbound: service.FromContext[adapter.OutboundManager](ctx),
|
||||
endpoint: service.FromContext[adapter.EndpointManager](ctx),
|
||||
provider: service.FromContext[adapter.ProviderManager](ctx),
|
||||
logger: logFactory.NewLogger("clash-api"),
|
||||
httpServer: &http.Server{
|
||||
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("/rules", ruleRouter(s.router))
|
||||
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("/script", scriptRouter())
|
||||
r.Mount("/profile", profileRouter())
|
||||
|
||||
@@ -3,6 +3,7 @@ package libbox
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/netip"
|
||||
"os"
|
||||
|
||||
box "github.com/sagernet/sing-box"
|
||||
@@ -33,7 +34,7 @@ func baseContext(platformInterface PlatformInterface) context.Context {
|
||||
}
|
||||
ctx := context.Background()
|
||||
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) {
|
||||
@@ -144,6 +145,10 @@ func (s *platformInterfaceStub) SendNotification(notification *adapter.Notificat
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) MyInterfaceAddress() []netip.Addr {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) UsePlatformLocalDNSTransport() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ type platformInterfaceWrapper struct {
|
||||
useProcFS bool
|
||||
networkManager adapter.NetworkManager
|
||||
myTunName string
|
||||
myTunAddress []netip.Addr
|
||||
defaultInterfaceAccess sync.Mutex
|
||||
defaultInterface *control.Interface
|
||||
isExpensive bool
|
||||
@@ -78,9 +79,25 @@ func (w *platformInterfaceWrapper) OpenInterface(options *tun.Options, platformO
|
||||
}
|
||||
options.FileDescriptor = dupFd
|
||||
w.myTunName = options.Name
|
||||
w.myTunAddress = myTunAddress(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 {
|
||||
return true
|
||||
}
|
||||
|
||||
31
go.mod
31
go.mod
@@ -1,8 +1,9 @@
|
||||
module github.com/sagernet/sing-box
|
||||
|
||||
go 1.25.5
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
github.com/Diniboy1123/connect-ip-go v0.0.0-20260409225322-8d7bb0a858a2
|
||||
github.com/GoAdminGroup/go-admin v1.2.26
|
||||
github.com/GoAdminGroup/themes v0.0.48
|
||||
github.com/anthropics/anthropic-sdk-go v1.26.0
|
||||
@@ -33,7 +34,7 @@ require (
|
||||
github.com/miekg/dns v1.1.72
|
||||
github.com/openai/openai-go/v3 v3.26.0
|
||||
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/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
|
||||
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/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c
|
||||
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/stretchr/testify v1.11.1
|
||||
github.com/vishvananda/netns v0.0.5
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2
|
||||
go.uber.org/zap v1.27.1
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/crypto v0.49.0
|
||||
@@ -72,7 +75,12 @@ 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
|
||||
)
|
||||
|
||||
@@ -95,6 +103,7 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // 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/florianl/go-nfqueue/v2 v2.0.2 // 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/jsimonetti/rtnetlink v1.4.0 // 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/leodido/go-urn v1.4.0 // 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/nxadm/tail v1.4.11 // 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/prometheus-community/pro-bing v0.4.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/peercred v0.0.0-20250107143737-35a0c7bd7edc // 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/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
@@ -210,13 +219,13 @@ require (
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
lukechampine.com/blake3 v1.4.1
|
||||
xorm.io/builder v0.3.7 // indirect
|
||||
xorm.io/xorm v1.0.2 // indirect
|
||||
)
|
||||
|
||||
replace github.com/sagernet/wireguard-go => github.com/shtorm-7/wireguard-go v0.0.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
|
||||
|
||||
@@ -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/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
55
go.sum
@@ -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/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/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/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
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/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
|
||||
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/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc=
|
||||
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/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
|
||||
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
|
||||
github.com/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/go.mod h1:LNlYVipaYkQArRFDNNJ02VkNV+My9A5XR/IGS7sIBQc=
|
||||
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-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54=
|
||||
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-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=
|
||||
@@ -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/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/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/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
|
||||
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/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||
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.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@@ -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/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
|
||||
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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||
github.com/panjf2000/ants/v2 v2.12.0 h1:u9JhESo83i/GkZnhfTNuFMMWcNt7mnV1bGJ6FT4wXH8=
|
||||
github.com/panjf2000/ants/v2 v2.12.0/go.mod h1:tSQuaNQ6r6NRhPt+IZVUevvDyFMTs+eS4ztZc52uJTY=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pierrec/lz4 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/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
|
||||
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -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/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/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/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/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/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
|
||||
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/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
|
||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
|
||||
github.com/shtorm-7/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/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/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/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.3.4/go.mod h1:Me2JlCDYHxnd0mnuX7L5LXAeDHCltI7vSKq3eTE6SVE=
|
||||
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.4.0/go.mod h1:Me2JlCDYHxnd0mnuX7L5LXAeDHCltI7vSKq3eTE6SVE=
|
||||
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/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
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/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.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.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
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/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
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/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
||||
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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
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/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
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.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
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/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
|
||||
12
include/masque.go
Normal file
12
include/masque.go
Normal 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
20
include/masque_stub.go
Normal 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
12
include/mtproxy.go
Normal 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
20
include/mtproxy_stub.go
Normal 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`)
|
||||
})
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/adapter/provider"
|
||||
"github.com/sagernet/sing-box/adapter/service"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
@@ -28,6 +29,7 @@ import (
|
||||
"github.com/sagernet/sing-box/protocol/mieru"
|
||||
"github.com/sagernet/sing-box/protocol/mixed"
|
||||
"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/shadowsocks"
|
||||
"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/trojan"
|
||||
"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/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/manager"
|
||||
"github.com/sagernet/sing-box/service/node"
|
||||
@@ -50,7 +54,7 @@ import (
|
||||
)
|
||||
|
||||
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 {
|
||||
@@ -77,6 +81,7 @@ func InboundRegistry() *inbound.Registry {
|
||||
|
||||
registerQUICInbounds(registry)
|
||||
registerStubForRemovedInbounds(registry)
|
||||
registerMTProxyInbound(registry)
|
||||
|
||||
return registry
|
||||
}
|
||||
@@ -88,7 +93,7 @@ func OutboundRegistry() *outbound.Registry {
|
||||
|
||||
block.RegisterOutbound(registry)
|
||||
|
||||
group.RegisterFailover(registry)
|
||||
group.RegisterFallback(registry)
|
||||
group.RegisterSelector(registry)
|
||||
group.RegisterURLTest(registry)
|
||||
|
||||
@@ -104,12 +109,15 @@ func OutboundRegistry() *outbound.Registry {
|
||||
vless.RegisterOutbound(registry)
|
||||
mieru.RegisterOutbound(registry)
|
||||
anytls.RegisterOutbound(registry)
|
||||
registerMASQUEOutbound(registry)
|
||||
|
||||
bond.RegisterOutbound(registry)
|
||||
|
||||
bandwidth.RegisterOutbound(registry)
|
||||
connection.RegisterOutbound(registry)
|
||||
|
||||
parser.RegisterOutbound(registry)
|
||||
|
||||
registerQUICOutbounds(registry)
|
||||
registerStubForRemovedOutbounds(registry)
|
||||
|
||||
@@ -119,8 +127,8 @@ func OutboundRegistry() *outbound.Registry {
|
||||
func EndpointRegistry() *endpoint.Registry {
|
||||
registry := endpoint.NewRegistry()
|
||||
|
||||
tunnel.RegisterServerEndpoint(registry)
|
||||
tunnel.RegisterClientEndpoint(registry)
|
||||
vpn.RegisterServerEndpoint(registry)
|
||||
vpn.RegisterClientEndpoint(registry)
|
||||
|
||||
registerWireGuardEndpoint(registry)
|
||||
registerTailscaleEndpoint(registry)
|
||||
@@ -128,6 +136,16 @@ func EndpointRegistry() *endpoint.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 {
|
||||
registry := dns.NewTransportRegistry()
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@ package include
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/protocol/warp"
|
||||
"github.com/sagernet/sing-box/protocol/wireguard"
|
||||
)
|
||||
|
||||
func registerWireGuardEndpoint(registry *endpoint.Registry) {
|
||||
wireguard.RegisterEndpoint(registry)
|
||||
wireguard.RegisterWARPEndpoint(registry)
|
||||
warp.RegisterEndpoint(registry)
|
||||
}
|
||||
|
||||
9
option/cloudflare.go
Normal file
9
option/cloudflare.go
Normal 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"`
|
||||
}
|
||||
@@ -11,13 +11,14 @@ type ExperimentalOptions struct {
|
||||
}
|
||||
|
||||
type CacheFileOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
CacheID string `json:"cache_id,omitempty"`
|
||||
StoreFakeIP bool `json:"store_fakeip,omitempty"`
|
||||
StoreRDRC bool `json:"store_rdrc,omitempty"`
|
||||
StoreWARPConfig bool `json:"store_warp_config,omitempty"`
|
||||
RDRCTimeout badoption.Duration `json:"rdrc_timeout,omitempty"`
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
CacheID string `json:"cache_id,omitempty"`
|
||||
StoreFakeIP bool `json:"store_fakeip,omitempty"`
|
||||
StoreRDRC bool `json:"store_rdrc,omitempty"`
|
||||
StoreWARPConfig bool `json:"store_warp_config,omitempty"`
|
||||
StoreMASQUEConfig bool `json:"store_masque_config,omitempty"`
|
||||
RDRCTimeout badoption.Duration `json:"rdrc_timeout,omitempty"`
|
||||
}
|
||||
|
||||
type ClashAPIOptions struct {
|
||||
|
||||
9
option/failover.go
Normal file
9
option/failover.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package option
|
||||
|
||||
type FailoverInboundOptions struct {
|
||||
Inbounds []Inbound `json:"inbounds"`
|
||||
}
|
||||
|
||||
type FailoverOutboundOptions struct {
|
||||
Outbounds []Outbound `json:"outbounds"`
|
||||
}
|
||||
@@ -3,13 +3,13 @@ package option
|
||||
import "github.com/sagernet/sing/common/json/badoption"
|
||||
|
||||
type SelectorOutboundOptions struct {
|
||||
Outbounds []string `json:"outbounds"`
|
||||
Default string `json:"default,omitempty"`
|
||||
InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"`
|
||||
GroupCommonOption
|
||||
Default string `json:"default,omitempty"`
|
||||
InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"`
|
||||
}
|
||||
|
||||
type URLTestOutboundOptions struct {
|
||||
Outbounds []string `json:"outbounds"`
|
||||
GroupCommonOption
|
||||
URL string `json:"url,omitempty"`
|
||||
Interval badoption.Duration `json:"interval,omitempty"`
|
||||
Tolerance uint16 `json:"tolerance,omitempty"`
|
||||
@@ -17,6 +17,14 @@ type URLTestOutboundOptions struct {
|
||||
InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"`
|
||||
}
|
||||
|
||||
type FailoverOutboundOptions struct {
|
||||
type FallbackOutboundOptions struct {
|
||||
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
32
option/masque.go
Normal 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
89
option/mtproxy.go
Normal 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"`
|
||||
}
|
||||
@@ -19,6 +19,7 @@ type _Options struct {
|
||||
Endpoints []Endpoint `json:"endpoints,omitempty"`
|
||||
Inbounds []Inbound `json:"inbounds,omitempty"`
|
||||
Outbounds []Outbound `json:"outbounds,omitempty"`
|
||||
Providers []Provider `json:"providers,omitempty"`
|
||||
Route *RouteOptions `json:"route,omitempty"`
|
||||
Services []Service `json:"services,omitempty"`
|
||||
Experimental *ExperimentalOptions `json:"experimental,omitempty"`
|
||||
|
||||
6
option/parser.go
Normal file
6
option/parser.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package option
|
||||
|
||||
type ParserOutboundOptions struct {
|
||||
DialerOptions
|
||||
Link string `json:"link"`
|
||||
}
|
||||
75
option/provider.go
Normal file
75
option/provider.go
Normal 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"`
|
||||
}
|
||||
@@ -88,8 +88,6 @@ type RawDefaultRule struct {
|
||||
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
||||
Port badoption.Listable[uint16] `json:"port,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"`
|
||||
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
||||
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
||||
|
||||
@@ -155,9 +155,10 @@ type RouteActionOptions struct {
|
||||
}
|
||||
|
||||
type RawRouteOptionsActionOptions struct {
|
||||
OverrideAddress string `json:"override_address,omitempty"`
|
||||
OverridePort uint16 `json:"override_port,omitempty"`
|
||||
OverrideTunnelDestination string `json:"override_tunnel_destination,omitempty"`
|
||||
OverrideAddress string `json:"override_address,omitempty"`
|
||||
OverridePort uint16 `json:"override_port,omitempty"`
|
||||
|
||||
OverrideGateway string `json:"override_gateway,omitempty"`
|
||||
|
||||
NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"`
|
||||
FallbackDelay uint32 `json:"fallback_delay,omitempty"`
|
||||
|
||||
@@ -90,8 +90,6 @@ type RawDefaultDNSRule struct {
|
||||
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
||||
Port badoption.Listable[uint16] `json:"port,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"`
|
||||
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
||||
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
||||
|
||||
@@ -194,8 +194,6 @@ type DefaultHeadlessRule struct {
|
||||
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
||||
Port badoption.Listable[uint16] `json:"port,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"`
|
||||
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
||||
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
||||
|
||||
@@ -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
25
option/vpn.go
Normal 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
18
option/warp.go
Normal 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
|
||||
}
|
||||
@@ -33,29 +33,6 @@ type WireGuardPeer struct {
|
||||
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 {
|
||||
JC int `json:"jc,omitempty"`
|
||||
JMin int `json:"jmin,omitempty"`
|
||||
|
||||
30
parser/clash/anytls.go
Normal file
30
parser/clash/anytls.go
Normal 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
181
parser/clash/base.go
Normal 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
23
parser/clash/http.go
Normal 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
47
parser/clash/hysteria.go
Normal 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
34
parser/clash/hysteria2.go
Normal 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
106
parser/clash/parser.go
Normal 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
|
||||
}
|
||||
51
parser/clash/shadowsocks.go
Normal file
51
parser/clash/shadowsocks.go
Normal 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
21
parser/clash/socks5.go
Normal 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
36
parser/clash/ssh.go
Normal 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
28
parser/clash/trojan.go
Normal 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
47
parser/clash/tuic.go
Normal 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
205
parser/clash/utils.go
Normal 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
49
parser/clash/vless.go
Normal 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
55
parser/clash/vmess.go
Normal 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
71
parser/link/hysteria.go
Normal 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
61
parser/link/hysteria2.go
Normal 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
42
parser/link/parser.go
Normal 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)
|
||||
}
|
||||
}
|
||||
39
parser/link/shadowsocks.go
Normal file
39
parser/link/shadowsocks.go
Normal 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
89
parser/link/trojan.go
Normal 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
Reference in New Issue
Block a user