Compare commits

..

11 Commits

Author SHA1 Message Date
世界
47736b27ba refactor: Modular inbounds 2024-11-03 20:24:41 +08:00
世界
0b2c7ec35c refactor: Modular outbounds 2024-11-03 19:56:44 +08:00
世界
e537c56b6b Implement dns-hijack 2024-11-03 19:47:17 +08:00
世界
b456aff4ac Implement resolve(server) 2024-11-03 19:47:15 +08:00
世界
4da652ff02 Implement TCP and ICMP rejects 2024-11-03 19:47:11 +08:00
世界
262727ec6c Crazy sekai overturns the small pond 2024-11-03 19:47:09 +08:00
世界
81dc9e7698 documentation: Bump version 2024-11-02 21:40:38 +08:00
世界
d876077281 Update dependencies 2024-11-02 21:40:28 +08:00
世界
0df81c297b Update quic-go to v0.48.0 2024-11-02 21:40:28 +08:00
世界
b460484e43 Fix "Fix metadata context" 2024-11-02 21:37:04 +08:00
世界
0e511791e8 platform: Add openURL event 2024-11-02 17:13:13 +08:00
211 changed files with 3137 additions and 5207 deletions

View File

@@ -4,28 +4,28 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/binary" "encoding/binary"
"net"
"time" "time"
"github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-dns" "github.com/sagernet/sing-dns"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/varbin" "github.com/sagernet/sing/common/varbin"
) )
type ClashServer interface { type ClashServer interface {
LifecycleService Service
ConnectionTracker PreStarter
Mode() string Mode() string
ModeList() []string ModeList() []string
HistoryStorage() *urltest.HistoryStorage HistoryStorage() *urltest.HistoryStorage
} RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker)
type V2RayServer interface {
LifecycleService
StatsService() ConnectionTracker
} }
type CacheFile interface { type CacheFile interface {
LifecycleService Service
PreStarter
StoreFakeIP() bool StoreFakeIP() bool
FakeIPStorage FakeIPStorage
@@ -94,6 +94,10 @@ func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
return nil return nil
} }
type Tracker interface {
Leave()
}
type OutboundGroup interface { type OutboundGroup interface {
Outbound Outbound
Now() string Now() string
@@ -111,3 +115,13 @@ func OutboundTag(detour Outbound) string {
} }
return detour.Tag() return detour.Tag()
} }
type V2RayServer interface {
Service
StatsService() V2RayStatsService
}
type V2RayStatsService interface {
RoutedConnection(inbound string, outbound string, user string, conn net.Conn) net.Conn
RoutedPacketConnection(inbound string, outbound string, user string, conn N.PacketConn) N.PacketConn
}

View File

@@ -28,15 +28,7 @@ type UDPInjectableInbound interface {
type InboundRegistry interface { type InboundRegistry interface {
option.InboundOptionsRegistry option.InboundOptionsRegistry
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error) CreateInbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Inbound, error)
}
type InboundManager interface {
Lifecycle
Inbounds() []Inbound
Get(tag string) (Inbound, bool)
Remove(tag string) error
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) error
} }
type InboundContext struct { type InboundContext struct {
@@ -65,8 +57,6 @@ type InboundContext struct {
// Deprecated // Deprecated
InboundOptions option.InboundOptions InboundOptions option.InboundOptions
UDPDisableDomainUnmapping bool UDPDisableDomainUnmapping bool
UDPConnect bool
DNSServer string DNSServer string
DestinationAddresses []netip.Addr DestinationAddresses []netip.Addr

View File

@@ -1,143 +0,0 @@
package inbound
import (
"context"
"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"
)
var _ adapter.InboundManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.InboundRegistry
access sync.Mutex
started bool
stage adapter.StartStage
inbounds []adapter.Inbound
inboundByTag map[string]adapter.Inbound
}
func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
inboundByTag: make(map[string]adapter.Inbound),
}
}
func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock()
defer m.access.Unlock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
for _, inbound := range m.inbounds {
err := adapter.LegacyStart(inbound, stage)
if err != nil {
return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]")
}
}
return nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
if !m.started {
return nil
}
m.started = false
inbounds := m.inbounds
m.inbounds = nil
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, inbound := range inbounds {
monitor.Start("close inbound/", inbound.Type(), "[", inbound.Tag(), "]")
err = E.Append(err, inbound.Close(), func(err error) error {
return E.Cause(err, "close inbound/", inbound.Type(), "[", inbound.Tag(), "]")
})
monitor.Finish()
}
return nil
}
func (m *Manager) Inbounds() []adapter.Inbound {
m.access.Lock()
defer m.access.Unlock()
return m.inbounds
}
func (m *Manager) Get(tag string) (adapter.Inbound, bool) {
m.access.Lock()
defer m.access.Unlock()
inbound, found := m.inboundByTag[tag]
return inbound, found
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
inbound, found := m.inboundByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.inboundByTag, tag)
index := common.Index(m.inbounds, func(it adapter.Inbound) bool {
return it == inbound
})
if index == -1 {
panic("invalid inbound index")
}
m.inbounds = append(m.inbounds[:index], m.inbounds[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return inbound.Close()
}
return nil
}
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error {
inbound, err := m.registry.Create(ctx, router, logger, tag, outboundType, options)
if err != nil {
return err
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
for _, stage := range adapter.ListStartStages {
err = adapter.LegacyStart(inbound, stage)
if err != nil {
return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]")
}
}
}
if existsInbound, loaded := m.inboundByTag[tag]; loaded {
if m.started {
err = existsInbound.Close()
if err != nil {
return E.Cause(err, "close inbound/", existsInbound.Type(), "[", existsInbound.Tag(), "]")
}
}
existsIndex := common.Index(m.inbounds, func(it adapter.Inbound) bool {
return it == existsInbound
})
if existsIndex == -1 {
panic("invalid inbound index")
}
m.inbounds = append(m.inbounds[:existsIndex], m.inbounds[existsIndex+1:]...)
}
m.inbounds = append(m.inbounds, inbound)
m.inboundByTag[tag] = inbound
return nil
}

View File

@@ -30,39 +30,39 @@ type (
type Registry struct { type Registry struct {
access sync.Mutex access sync.Mutex
optionsType map[string]optionsConstructorFunc optionsType map[string]optionsConstructorFunc
constructor map[string]constructorFunc constructors map[string]constructorFunc
} }
func NewRegistry() *Registry { func NewRegistry() *Registry {
return &Registry{ return &Registry{
optionsType: make(map[string]optionsConstructorFunc), optionsType: make(map[string]optionsConstructorFunc),
constructor: make(map[string]constructorFunc), constructors: make(map[string]constructorFunc),
} }
} }
func (m *Registry) CreateOptions(outboundType string) (any, bool) { func (r *Registry) CreateOptions(outboundType string) (any, bool) {
m.access.Lock() r.access.Lock()
defer m.access.Unlock() defer r.access.Unlock()
optionsConstructor, loaded := m.optionsType[outboundType] optionsConstructor, loaded := r.optionsType[outboundType]
if !loaded { if !loaded {
return nil, false return nil, false
} }
return optionsConstructor(), true return optionsConstructor(), true
} }
func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) { func (r *Registry) CreateInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
m.access.Lock() r.access.Lock()
defer m.access.Unlock() defer r.access.Unlock()
constructor, loaded := m.constructor[outboundType] constructor, loaded := r.constructors[outboundType]
if !loaded { if !loaded {
return nil, E.New("outbound type not found: " + outboundType) return nil, E.New("outbound type not found: " + outboundType)
} }
return constructor(ctx, router, logger, tag, options) return constructor(ctx, router, logger, tag, options)
} }
func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) { func (r *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
m.access.Lock() r.access.Lock()
defer m.access.Unlock() defer r.access.Unlock()
m.optionsType[outboundType] = optionsConstructor r.optionsType[outboundType] = optionsConstructor
m.constructor[outboundType] = constructor r.constructors[outboundType] = constructor
} }

View File

@@ -1,64 +0,0 @@
package adapter
import E "github.com/sagernet/sing/common/exceptions"
type StartStage uint8
const (
StartStateInitialize StartStage = iota
StartStateStart
StartStatePostStart
StartStateStarted
)
var ListStartStages = []StartStage{
StartStateInitialize,
StartStateStart,
StartStatePostStart,
StartStateStarted,
}
func (s StartStage) String() string {
switch s {
case StartStateInitialize:
return "initialize"
case StartStateStart:
return "start"
case StartStatePostStart:
return "post-start"
case StartStateStarted:
return "finish-start"
default:
panic("unknown stage")
}
}
type Lifecycle interface {
Start(stage StartStage) error
Close() error
}
type LifecycleService interface {
Name() string
Lifecycle
}
func Start(stage StartStage, services ...Lifecycle) error {
for _, service := range services {
err := service.Start(stage)
if err != nil {
return err
}
}
return nil
}
func StartNamed(stage StartStage, services []LifecycleService) error {
for _, service := range services {
err := service.Start(stage)
if err != nil {
return E.Cause(err, stage.String(), " ", service.Name())
}
}
return nil
}

View File

@@ -1,49 +0,0 @@
package adapter
func LegacyStart(starter any, stage StartStage) error {
switch stage {
case StartStateInitialize:
if preStarter, isPreStarter := starter.(interface {
PreStart() error
}); isPreStarter {
return preStarter.PreStart()
}
case StartStateStart:
if starter, isStarter := starter.(interface {
Start() error
}); isStarter {
return starter.Start()
}
case StartStateStarted:
if postStarter, isPostStarter := starter.(interface {
PostStart() error
}); isPostStarter {
return postStarter.PostStart()
}
}
return nil
}
type lifecycleServiceWrapper struct {
Service
name string
}
func NewLifecycleService(service Service, name string) LifecycleService {
return &lifecycleServiceWrapper{
Service: service,
name: name,
}
}
func (l *lifecycleServiceWrapper) Name() string {
return l.name
}
func (l *lifecycleServiceWrapper) Start(stage StartStage) error {
return LegacyStart(l.Service, stage)
}
func (l *lifecycleServiceWrapper) Close() error {
return l.Service.Close()
}

View File

@@ -1,42 +0,0 @@
package adapter
import (
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
)
type NetworkManager interface {
Lifecycle
InterfaceFinder() control.InterfaceFinder
UpdateInterfaces() error
DefaultNetworkInterface() *NetworkInterface
NetworkInterfaces() []NetworkInterface
DefaultInterface() string
AutoDetectInterface() bool
AutoDetectInterfaceFunc() control.Func
DefaultMark() uint32
RegisterAutoRedirectOutputMark(mark uint32) error
AutoRedirectOutputMark() uint32
NetworkMonitor() tun.NetworkUpdateMonitor
InterfaceMonitor() tun.DefaultInterfaceMonitor
PackageManager() tun.PackageManager
WIFIState() WIFIState
ResetNetwork()
}
type InterfaceUpdateListener interface {
InterfaceUpdated()
}
type WIFIState struct {
SSID string
BSSID string
}
type NetworkInterface struct {
control.Interface
Type string
DNSServers []string
Expensive bool
Constrained bool
}

View File

@@ -22,12 +22,3 @@ type OutboundRegistry interface {
option.OutboundOptionsRegistry option.OutboundOptionsRegistry
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error) CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
} }
type OutboundManager interface {
Lifecycle
Outbounds() []Outbound
Outbound(tag string) (Outbound, bool)
Default() Outbound
Remove(tag string) error
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) error
}

View File

@@ -20,7 +20,6 @@ import (
) )
func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext) error { func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext) error {
defer conn.Close()
ctx = adapter.WithContext(ctx, &metadata) ctx = adapter.WithContext(ctx, &metadata)
var outConn net.Conn var outConn net.Conn
var err error var err error
@@ -41,7 +40,6 @@ func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata a
} }
func NewDirectConnection(ctx context.Context, router adapter.Router, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, domainStrategy dns.DomainStrategy) error { func NewDirectConnection(ctx context.Context, router adapter.Router, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, domainStrategy dns.DomainStrategy) error {
defer conn.Close()
ctx = adapter.WithContext(ctx, &metadata) ctx = adapter.WithContext(ctx, &metadata)
var outConn net.Conn var outConn net.Conn
var err error var err error
@@ -69,49 +67,29 @@ func NewDirectConnection(ctx context.Context, router adapter.Router, this N.Dial
} }
func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata adapter.InboundContext) error { func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata adapter.InboundContext) error {
defer conn.Close()
ctx = adapter.WithContext(ctx, &metadata) ctx = adapter.WithContext(ctx, &metadata)
var ( var outConn net.PacketConn
outPacketConn net.PacketConn var destinationAddress netip.Addr
outConn net.Conn var err error
destinationAddress netip.Addr
err error
)
if metadata.UDPConnect {
if len(metadata.DestinationAddresses) > 0 { if len(metadata.DestinationAddresses) > 0 {
outConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses) outConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, metadata.DestinationAddresses)
} else { } else {
outConn, err = this.DialContext(ctx, N.NetworkUDP, metadata.Destination) outConn, err = this.ListenPacket(ctx, metadata.Destination)
} }
if err != nil { if err != nil {
return N.ReportHandshakeFailure(conn, err) return N.ReportHandshakeFailure(conn, err)
} }
outPacketConn = bufio.NewUnbindPacketConn(outConn) err = N.ReportPacketConnHandshakeSuccess(conn, outConn)
connRemoteAddr := M.AddrFromNet(outConn.RemoteAddr())
if connRemoteAddr != metadata.Destination.Addr {
destinationAddress = connRemoteAddr
}
} else {
if len(metadata.DestinationAddresses) > 0 {
outPacketConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, metadata.DestinationAddresses)
} else {
outPacketConn, err = this.ListenPacket(ctx, metadata.Destination)
}
if err != nil { if err != nil {
return N.ReportHandshakeFailure(conn, err) outConn.Close()
}
}
err = N.ReportPacketConnHandshakeSuccess(conn, outPacketConn)
if err != nil {
outPacketConn.Close()
return err return err
} }
if destinationAddress.IsValid() { if destinationAddress.IsValid() {
if metadata.Destination.IsFqdn() { if metadata.Destination.IsFqdn() {
if metadata.UDPDisableDomainUnmapping { if metadata.UDPDisableDomainUnmapping {
outPacketConn = bufio.NewUnidirectionalNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) outConn = bufio.NewUnidirectionalNATPacketConn(bufio.NewPacketConn(outConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination)
} else { } else {
outPacketConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) outConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination)
} }
} }
if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded { if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded {
@@ -126,63 +104,37 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn,
case C.ProtocolDNS: case C.ProtocolDNS:
ctx, conn = canceler.NewPacketConn(ctx, conn, C.DNSTimeout) ctx, conn = canceler.NewPacketConn(ctx, conn, C.DNSTimeout)
} }
return bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(outPacketConn)) return bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(outConn))
} }
func NewDirectPacketConnection(ctx context.Context, router adapter.Router, this N.Dialer, conn N.PacketConn, metadata adapter.InboundContext, domainStrategy dns.DomainStrategy) error { func NewDirectPacketConnection(ctx context.Context, router adapter.Router, this N.Dialer, conn N.PacketConn, metadata adapter.InboundContext, domainStrategy dns.DomainStrategy) error {
defer conn.Close()
ctx = adapter.WithContext(ctx, &metadata) ctx = adapter.WithContext(ctx, &metadata)
var ( var outConn net.PacketConn
outPacketConn net.PacketConn var destinationAddress netip.Addr
outConn net.Conn var err error
destinationAddress netip.Addr
err error
)
if metadata.UDPConnect {
if len(metadata.DestinationAddresses) > 0 { if len(metadata.DestinationAddresses) > 0 {
outConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses) outConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, metadata.DestinationAddresses)
} else if metadata.Destination.IsFqdn() { } else if metadata.Destination.IsFqdn() {
var destinationAddresses []netip.Addr var destinationAddresses []netip.Addr
destinationAddresses, err = router.Lookup(ctx, metadata.Destination.Fqdn, domainStrategy) destinationAddresses, err = router.Lookup(ctx, metadata.Destination.Fqdn, domainStrategy)
if err != nil { if err != nil {
return N.ReportHandshakeFailure(conn, err) return N.ReportHandshakeFailure(conn, err)
} }
outConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, destinationAddresses) outConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, destinationAddresses)
} else { } else {
outConn, err = this.DialContext(ctx, N.NetworkUDP, metadata.Destination) outConn, err = this.ListenPacket(ctx, metadata.Destination)
} }
if err != nil { if err != nil {
return N.ReportHandshakeFailure(conn, err) return N.ReportHandshakeFailure(conn, err)
} }
connRemoteAddr := M.AddrFromNet(outConn.RemoteAddr()) err = N.ReportPacketConnHandshakeSuccess(conn, outConn)
if connRemoteAddr != metadata.Destination.Addr {
destinationAddress = connRemoteAddr
}
} else {
if len(metadata.DestinationAddresses) > 0 {
outPacketConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, metadata.DestinationAddresses)
} else if metadata.Destination.IsFqdn() {
var destinationAddresses []netip.Addr
destinationAddresses, err = router.Lookup(ctx, metadata.Destination.Fqdn, domainStrategy)
if err != nil { if err != nil {
return N.ReportHandshakeFailure(conn, err) outConn.Close()
}
outPacketConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, destinationAddresses)
} else {
outPacketConn, err = this.ListenPacket(ctx, metadata.Destination)
}
if err != nil {
return N.ReportHandshakeFailure(conn, err)
}
}
err = N.ReportPacketConnHandshakeSuccess(conn, outPacketConn)
if err != nil {
outPacketConn.Close()
return err return err
} }
if destinationAddress.IsValid() { if destinationAddress.IsValid() {
if metadata.Destination.IsFqdn() { if metadata.Destination.IsFqdn() {
outPacketConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) outConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination)
} }
if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded { if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded {
natConn.UpdateDestination(destinationAddress) natConn.UpdateDestination(destinationAddress)
@@ -196,7 +148,7 @@ func NewDirectPacketConnection(ctx context.Context, router adapter.Router, this
case C.ProtocolDNS: case C.ProtocolDNS:
ctx, conn = canceler.NewPacketConn(ctx, conn, C.DNSTimeout) ctx, conn = canceler.NewPacketConn(ctx, conn, C.DNSTimeout)
} }
return bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(outPacketConn)) return bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(outConn))
} }
func CopyEarlyConn(ctx context.Context, conn net.Conn, serverConn net.Conn) error { func CopyEarlyConn(ctx context.Context, conn net.Conn, serverConn net.Conn) error {

View File

@@ -1,269 +0,0 @@
package outbound
import (
"context"
"io"
"os"
"strings"
"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.OutboundManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.OutboundRegistry
defaultTag string
access sync.Mutex
started bool
stage adapter.StartStage
outbounds []adapter.Outbound
outboundByTag map[string]adapter.Outbound
dependByTag map[string][]string
defaultOutbound adapter.Outbound
defaultOutboundFallback adapter.Outbound
}
func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, defaultTag string) *Manager {
return &Manager{
logger: logger,
registry: registry,
defaultTag: defaultTag,
outboundByTag: make(map[string]adapter.Outbound),
dependByTag: make(map[string][]string),
}
}
func (m *Manager) Initialize(defaultOutboundFallback adapter.Outbound) {
m.defaultOutboundFallback = defaultOutboundFallback
}
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
outbounds := m.outbounds
m.access.Unlock()
if stage == adapter.StartStateStart {
return m.startOutbounds(outbounds)
} else {
for _, outbound := range outbounds {
err := adapter.LegacyStart(outbound, stage)
if err != nil {
return E.Cause(err, stage, " outbound/", outbound.Type(), "[", outbound.Tag(), "]")
}
}
}
return nil
}
func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
monitor := taskmonitor.New(m.logger, C.StartTimeout)
started := make(map[string]bool)
for {
canContinue := false
startOne:
for _, outboundToStart := range outbounds {
outboundTag := outboundToStart.Tag()
if started[outboundTag] {
continue
}
dependencies := outboundToStart.Dependencies()
for _, dependency := range dependencies {
if !started[dependency] {
continue startOne
}
}
started[outboundTag] = true
canContinue = true
if starter, isStarter := outboundToStart.(interface {
Start() error
}); isStarter {
monitor.Start("start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
err := starter.Start()
monitor.Finish()
if err != nil {
return E.Cause(err, "start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
}
}
}
if len(started) == len(outbounds) {
break
}
if canContinue {
continue
}
currentOutbound := common.Find(outbounds, func(it adapter.Outbound) bool {
return !started[it.Tag()]
})
var lintOutbound func(oTree []string, oCurrent adapter.Outbound) error
lintOutbound = func(oTree []string, oCurrent adapter.Outbound) error {
problemOutboundTag := common.Find(oCurrent.Dependencies(), func(it string) bool {
return !started[it]
})
if common.Contains(oTree, problemOutboundTag) {
return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag)
}
m.access.Lock()
problemOutbound := m.outboundByTag[problemOutboundTag]
m.access.Unlock()
if problemOutbound == nil {
return E.New("dependency[", problemOutboundTag, "] not found for outbound[", oCurrent.Tag(), "]")
}
return lintOutbound(append(oTree, problemOutboundTag), problemOutbound)
}
return lintOutbound([]string{currentOutbound.Tag()}, currentOutbound)
}
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
outbounds := m.outbounds
m.outbounds = nil
m.access.Unlock()
var err error
for _, outbound := range outbounds {
if closer, isCloser := outbound.(io.Closer); isCloser {
monitor.Start("close outbound/", outbound.Type(), "[", outbound.Tag(), "]")
err = E.Append(err, closer.Close(), func(err error) error {
return E.Cause(err, "close outbound/", outbound.Type(), "[", outbound.Tag(), "]")
})
monitor.Finish()
}
}
return nil
}
func (m *Manager) Outbounds() []adapter.Outbound {
m.access.Lock()
defer m.access.Unlock()
return m.outbounds
}
func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
m.access.Lock()
defer m.access.Unlock()
outbound, found := m.outboundByTag[tag]
return outbound, found
}
func (m *Manager) Default() adapter.Outbound {
m.access.Lock()
defer m.access.Unlock()
if m.defaultOutbound != nil {
return m.defaultOutbound
} else {
return m.defaultOutboundFallback
}
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
outbound, found := m.outboundByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.outboundByTag, tag)
index := common.Index(m.outbounds, func(it adapter.Outbound) bool {
return it == outbound
})
if index == -1 {
panic("invalid inbound index")
}
m.outbounds = append(m.outbounds[:index], m.outbounds[index+1:]...)
started := m.started
if m.defaultOutbound == outbound {
if len(m.outbounds) > 0 {
m.defaultOutbound = m.outbounds[0]
m.logger.Info("updated default outbound to ", m.defaultOutbound.Tag())
} else {
m.defaultOutbound = nil
}
}
dependBy := m.dependByTag[tag]
if len(dependBy) > 0 {
return E.New("outbound[", tag, "] is depended by ", strings.Join(dependBy, ", "))
}
dependencies := outbound.Dependencies()
for _, dependency := range dependencies {
if len(m.dependByTag[dependency]) == 1 {
delete(m.dependByTag, dependency)
} else {
m.dependByTag[dependency] = common.Filter(m.dependByTag[dependency], func(it string) bool {
return it != tag
})
}
}
m.access.Unlock()
if started {
return common.Close(outbound)
}
return nil
}
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, inboundType string, options any) error {
if tag == "" {
return os.ErrInvalid
}
outbound, err := m.registry.CreateOutbound(ctx, router, logger, tag, inboundType, options)
if err != nil {
return err
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
for _, stage := range adapter.ListStartStages {
err = adapter.LegacyStart(outbound, stage)
if err != nil {
return E.Cause(err, stage, " outbound/", outbound.Type(), "[", outbound.Tag(), "]")
}
}
}
if existsOutbound, loaded := m.outboundByTag[tag]; loaded {
if m.started {
err = common.Close(existsOutbound)
if err != nil {
return E.Cause(err, "close outbound/", existsOutbound.Type(), "[", existsOutbound.Tag(), "]")
}
}
existsIndex := common.Index(m.outbounds, func(it adapter.Outbound) bool {
return it == existsOutbound
})
if existsIndex == -1 {
panic("invalid inbound index")
}
m.outbounds = append(m.outbounds[:existsIndex], m.outbounds[existsIndex+1:]...)
}
m.outbounds = append(m.outbounds, outbound)
m.outboundByTag[tag] = outbound
dependencies := outbound.Dependencies()
for _, dependency := range dependencies {
m.dependByTag[dependency] = append(m.dependByTag[dependency], tag)
}
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) {
m.defaultOutbound = outbound
if m.started {
m.logger.Info("updated default outbound to ", outbound.Tag())
}
}
return nil
}

View File

@@ -1 +1,9 @@
package adapter package adapter
type PreStarter interface {
PreStart() error
}
type PostStarter interface {
PostStart() error
}

View File

@@ -10,16 +10,26 @@ import (
"github.com/sagernet/sing-box/common/geoip" "github.com/sagernet/sing-box/common/geoip"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-dns" "github.com/sagernet/sing-dns"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service"
mdns "github.com/miekg/dns" mdns "github.com/miekg/dns"
"go4.org/netipx" "go4.org/netipx"
) )
type Router interface { type Router interface {
Lifecycle Service
PreStarter
PostStarter
Cleanup() error
Outbounds() []Outbound
Outbound(tag string) (Outbound, bool)
DefaultOutbound(network string) (Outbound, error)
FakeIPStore() FakeIPStore FakeIPStore() FakeIPStore
@@ -29,23 +39,37 @@ type Router interface {
GeoIPReader() *geoip.Reader GeoIPReader() *geoip.Reader
LoadGeosite(code string) (Rule, error) LoadGeosite(code string) (Rule, error)
RuleSet(tag string) (RuleSet, bool) RuleSet(tag string) (RuleSet, bool)
NeedWIFIState() bool NeedWIFIState() bool
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error) Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error) LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
ClearDNSCache() ClearDNSCache()
InterfaceFinder() control.InterfaceFinder
UpdateInterfaces() error
DefaultInterface() string
AutoDetectInterface() bool
AutoDetectInterfaceFunc() control.Func
DefaultMark() uint32
RegisterAutoRedirectOutputMark(mark uint32) error
AutoRedirectOutputMark() uint32
NetworkMonitor() tun.NetworkUpdateMonitor
InterfaceMonitor() tun.DefaultInterfaceMonitor
PackageManager() tun.PackageManager
WIFIState() WIFIState
Rules() []Rule Rules() []Rule
SetTracker(tracker ConnectionTracker) ClashServer() ClashServer
SetClashServer(server ClashServer)
ResetNetwork() V2RayServer() V2RayServer
} SetV2RayServer(server V2RayServer)
type ConnectionTracker interface { ResetNetwork() error
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) net.Conn
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) N.PacketConn
} }
// Deprecated: Use ConnectionRouterEx instead. // Deprecated: Use ConnectionRouterEx instead.
@@ -60,6 +84,14 @@ type ConnectionRouterEx interface {
RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
} }
func ContextWithRouter(ctx context.Context, router Router) context.Context {
return service.ContextWith(ctx, router)
}
func RouterFromContext(ctx context.Context) Router {
return service.FromContext[Router](ctx)
}
type RuleSet interface { type RuleSet interface {
Name() string Name() string
StartContext(ctx context.Context, startContext *HTTPStartContext) error StartContext(ctx context.Context, startContext *HTTPStartContext) error
@@ -119,3 +151,12 @@ func (c *HTTPStartContext) Close() {
client.CloseIdleConnections() client.CloseIdleConnections()
} }
} }
type InterfaceUpdateListener interface {
InterfaceUpdated()
}
type WIFIState struct {
SSID string
BSSID string
}

304
box.go
View File

@@ -9,9 +9,6 @@ import (
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/taskmonitor" "github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental" "github.com/sagernet/sing-box/experimental"
@@ -24,7 +21,6 @@ import (
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/pause" "github.com/sagernet/sing/service/pause"
) )
@@ -33,13 +29,14 @@ var _ adapter.Service = (*Box)(nil)
type Box struct { type Box struct {
createdAt time.Time createdAt time.Time
router adapter.Router
inbounds []adapter.Inbound
outbounds []adapter.Outbound
logFactory log.Factory logFactory log.Factory
logger log.ContextLogger logger log.ContextLogger
network *route.NetworkManager preServices1 map[string]adapter.Service
router *route.Router preServices2 map[string]adapter.Service
inbound *inbound.Manager postServices map[string]adapter.Service
outbound *outbound.Manager
services []adapter.LifecycleService
done chan struct{} done chan struct{}
} }
@@ -49,11 +46,7 @@ type Options struct {
PlatformLogWriter log.PlatformWriter PlatformLogWriter log.PlatformWriter
} }
func Context( func Context(ctx context.Context, inboundRegistry adapter.InboundRegistry, outboundRegistry adapter.OutboundRegistry) context.Context {
ctx context.Context,
inboundRegistry adapter.InboundRegistry,
outboundRegistry adapter.OutboundRegistry,
) context.Context {
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil || if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.InboundRegistry](ctx) == nil { service.FromContext[adapter.InboundRegistry](ctx) == nil {
ctx = service.ContextWith[option.InboundOptionsRegistry](ctx, inboundRegistry) ctx = service.ContextWith[option.InboundOptionsRegistry](ctx, inboundRegistry)
@@ -73,18 +66,15 @@ func New(options Options) (*Box, error) {
if ctx == nil { if ctx == nil {
ctx = context.Background() ctx = context.Background()
} }
ctx = service.ContextWithDefaultRegistry(ctx)
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx) inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
if inboundRegistry == nil { if inboundRegistry == nil {
return nil, E.New("missing inbound registry in context") return nil, E.New("missing inbound registry in context")
} }
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx) outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
if outboundRegistry == nil { if outboundRegistry == nil {
return nil, E.New("missing outbound registry in context") return nil, E.New("missing outbound registry in context")
} }
ctx = service.ContextWithDefaultRegistry(ctx)
ctx = pause.WithDefaultManager(ctx) ctx = pause.WithDefaultManager(ctx)
experimentalOptions := common.PtrValueOrDefault(options.Experimental) experimentalOptions := common.PtrValueOrDefault(options.Experimental)
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug)) applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
@@ -116,21 +106,16 @@ func New(options Options) (*Box, error) {
if err != nil { if err != nil {
return nil, E.Cause(err, "create log factory") return nil, E.Cause(err, "create log factory")
} }
router, err := route.NewRouter(
routeOptions := common.PtrValueOrDefault(options.Route) ctx,
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry) logFactory,
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, routeOptions.Final) common.PtrValueOrDefault(options.Route),
service.MustRegister[adapter.InboundManager](ctx, inboundManager) common.PtrValueOrDefault(options.DNS),
service.MustRegister[adapter.OutboundManager](ctx, outboundManager) common.PtrValueOrDefault(options.NTP),
options.Inbounds,
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions) )
if err != nil { if err != nil {
return nil, E.Cause(err, "initialize network manager") return nil, E.Cause(err, "parse route options")
}
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
router, err := route.NewRouter(ctx, logFactory, routeOptions, common.PtrValueOrDefault(options.DNS))
if err != nil {
return nil, E.Cause(err, "initialize router")
} }
//nolint:staticcheck //nolint:staticcheck
if len(options.LegacyInbounds) > 0 { if len(options.LegacyInbounds) > 0 {
@@ -142,6 +127,7 @@ func New(options Options) (*Box, error) {
}) })
} }
} }
inbounds := make([]adapter.Inbound, 0, len(options.Inbounds))
//nolint:staticcheck //nolint:staticcheck
if len(options.LegacyOutbounds) > 0 { if len(options.LegacyOutbounds) > 0 {
for _, legacyOutbound := range options.LegacyOutbounds { for _, legacyOutbound := range options.LegacyOutbounds {
@@ -152,14 +138,17 @@ func New(options Options) (*Box, error) {
}) })
} }
} }
outbounds := make([]adapter.Outbound, 0, len(options.Outbounds))
for i, inboundOptions := range options.Inbounds { for i, inboundOptions := range options.Inbounds {
var currentInbound adapter.Inbound
var tag string var tag string
if inboundOptions.Tag != "" { if inboundOptions.Tag != "" {
tag = inboundOptions.Tag tag = inboundOptions.Tag
} else { } else {
tag = F.ToString(i) tag = F.ToString(i)
} }
err = inboundManager.Create(ctx, currentInbound, err = inboundRegistry.CreateInbound(
ctx,
router, router,
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")), logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
tag, tag,
@@ -167,10 +156,12 @@ func New(options Options) (*Box, error) {
inboundOptions.Options, inboundOptions.Options,
) )
if err != nil { if err != nil {
return nil, E.Cause(err, "initialize inbound[", i, "]") return nil, E.Cause(err, "parse inbound[", i, "]")
} }
inbounds = append(inbounds, currentInbound)
} }
for i, outboundOptions := range options.Outbounds { for i, outboundOptions := range options.Outbounds {
var currentOutbound adapter.Outbound
var tag string var tag string
if outboundOptions.Tag != "" { if outboundOptions.Tag != "" {
tag = outboundOptions.Tag tag = outboundOptions.Tag
@@ -184,7 +175,7 @@ func New(options Options) (*Box, error) {
Outbound: tag, Outbound: tag,
}) })
} }
err = outboundManager.Create( currentOutbound, err = outboundRegistry.CreateOutbound(
outboundCtx, outboundCtx,
router, router,
logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")), logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
@@ -193,78 +184,64 @@ func New(options Options) (*Box, error) {
outboundOptions.Options, outboundOptions.Options,
) )
if err != nil { if err != nil {
return nil, E.Cause(err, "initialize outbound[", i, "]") return nil, E.Cause(err, "parse outbound[", i, "]")
} }
outbounds = append(outbounds, currentOutbound)
}
err = router.Initialize(inbounds, outbounds, func() adapter.Outbound {
defaultOutbound, cErr := direct.NewOutbound(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.DirectOutboundOptions{})
common.Must(cErr)
outbounds = append(outbounds, defaultOutbound)
return defaultOutbound
})
if err != nil {
return nil, err
} }
outboundManager.Initialize(common.Must1(
direct.NewOutbound(
ctx,
router,
logFactory.NewLogger("outbound/direct"),
"direct",
option.DirectOutboundOptions{},
),
))
if platformInterface != nil { if platformInterface != nil {
err = platformInterface.Initialize(networkManager) err = platformInterface.Initialize(ctx, router)
if err != nil { if err != nil {
return nil, E.Cause(err, "initialize platform interface") return nil, E.Cause(err, "initialize platform interface")
} }
} }
var services []adapter.LifecycleService preServices1 := make(map[string]adapter.Service)
preServices2 := make(map[string]adapter.Service)
postServices := make(map[string]adapter.Service)
if needCacheFile { if needCacheFile {
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile)) cacheFile := service.FromContext[adapter.CacheFile](ctx)
if cacheFile == nil {
cacheFile = cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
service.MustRegister[adapter.CacheFile](ctx, cacheFile) service.MustRegister[adapter.CacheFile](ctx, cacheFile)
services = append(services, cacheFile) }
preServices1["cache file"] = cacheFile
} }
if needClashAPI { if needClashAPI {
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI) clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options) clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), clashAPIOptions) clashServer, err := experimental.NewClashServer(ctx, router, logFactory.(log.ObservableFactory), clashAPIOptions)
if err != nil { if err != nil {
return nil, E.Cause(err, "create clash-server") return nil, E.Cause(err, "create clash api server")
} }
router.SetTracker(clashServer) router.SetClashServer(clashServer)
service.MustRegister[adapter.ClashServer](ctx, clashServer) preServices2["clash api"] = clashServer
services = append(services, clashServer)
} }
if needV2RayAPI { if needV2RayAPI {
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI)) v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
if err != nil { if err != nil {
return nil, E.Cause(err, "create v2ray-server") return nil, E.Cause(err, "create v2ray api server")
} }
if v2rayServer.StatsService() != nil { router.SetV2RayServer(v2rayServer)
router.SetTracker(v2rayServer.StatsService()) preServices2["v2ray api"] = v2rayServer
services = append(services, v2rayServer)
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
}
}
ntpOptions := common.PtrValueOrDefault(options.NTP)
if ntpOptions.Enabled {
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions)
if err != nil {
return nil, E.Cause(err, "create NTP service")
}
timeService := ntp.NewService(ntp.Options{
Context: ctx,
Dialer: ntpDialer,
Logger: logFactory.NewLogger("ntp"),
Server: ntpOptions.ServerOptions.Build(),
Interval: time.Duration(ntpOptions.Interval),
WriteToSystem: ntpOptions.WriteToSystem,
})
service.MustRegister[ntp.TimeService](ctx, timeService)
services = append(services, adapter.NewLifecycleService(timeService, "ntp service"))
} }
return &Box{ return &Box{
network: networkManager,
router: router, router: router,
inbound: inboundManager, inbounds: inbounds,
outbound: outboundManager, outbounds: outbounds,
createdAt: createdAt, createdAt: createdAt,
logFactory: logFactory, logFactory: logFactory,
logger: logFactory.Logger(), logger: logFactory.Logger(),
services: services, preServices1: preServices1,
preServices2: preServices2,
postServices: postServices,
done: make(chan struct{}), done: make(chan struct{}),
}, nil }, nil
} }
@@ -315,19 +292,35 @@ func (s *Box) preStart() error {
if err != nil { if err != nil {
return E.Cause(err, "start logger") return E.Cause(err, "start logger")
} }
err = adapter.StartNamed(adapter.StartStateInitialize, s.services) // cache-file clash-api v2ray-api for serviceName, service := range s.preServices1 {
if preService, isPreService := service.(adapter.PreStarter); isPreService {
monitor.Start("pre-start ", serviceName)
err := preService.PreStart()
monitor.Finish()
if err != nil {
return E.Cause(err, "pre-start ", serviceName)
}
}
}
for serviceName, service := range s.preServices2 {
if preService, isPreService := service.(adapter.PreStarter); isPreService {
monitor.Start("pre-start ", serviceName)
err := preService.PreStart()
monitor.Finish()
if err != nil {
return E.Cause(err, "pre-start ", serviceName)
}
}
}
err = s.router.PreStart()
if err != nil {
return E.Cause(err, "pre-start router")
}
err = s.startOutbounds()
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateInitialize, s.network, s.router, s.outbound, s.inbound) return s.router.Start()
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateStart, s.outbound, s.network, s.router)
if err != nil {
return err
}
return nil
} }
func (s *Box) start() error { func (s *Box) start() error {
@@ -335,29 +328,64 @@ func (s *Box) start() error {
if err != nil { if err != nil {
return err return err
} }
err = adapter.StartNamed(adapter.StartStateStart, s.services) for serviceName, service := range s.preServices1 {
err = service.Start()
if err != nil {
return E.Cause(err, "start ", serviceName)
}
}
for serviceName, service := range s.preServices2 {
err = service.Start()
if err != nil {
return E.Cause(err, "start ", serviceName)
}
}
for i, in := range s.inbounds {
var tag string
if in.Tag() == "" {
tag = F.ToString(i)
} else {
tag = in.Tag()
}
err = in.Start()
if err != nil {
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
}
}
err = s.postStart()
if err != nil { if err != nil {
return err return err
} }
err = s.inbound.Start(adapter.StartStateStart) return s.router.Cleanup()
}
func (s *Box) postStart() error {
for serviceName, service := range s.postServices {
err := service.Start()
if err != nil {
return E.Cause(err, "start ", serviceName)
}
}
// TODO: reorganize ALL start order
for _, out := range s.outbounds {
if lateOutbound, isLateOutbound := out.(adapter.PostStarter); isLateOutbound {
err := lateOutbound.PostStart()
if err != nil {
return E.Cause(err, "post-start outbound/", out.Tag())
}
}
}
err := s.router.PostStart()
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.router, s.inbound) for _, in := range s.inbounds {
if lateInbound, isLateInbound := in.(adapter.PostStarter); isLateInbound {
err = lateInbound.PostStart()
if err != nil { if err != nil {
return err return E.Cause(err, "post-start inbound/", in.Tag())
} }
err = adapter.StartNamed(adapter.StartStatePostStart, s.services)
if err != nil {
return err
} }
err = adapter.Start(adapter.StartStateStarted, s.network, s.router, s.outbound, s.inbound)
if err != nil {
return err
}
err = adapter.StartNamed(adapter.StartStateStarted, s.services)
if err != nil {
return err
} }
return nil return nil
} }
@@ -369,32 +397,58 @@ func (s *Box) Close() error {
default: default:
close(s.done) close(s.done)
} }
err := common.Close( monitor := taskmonitor.New(s.logger, C.StopTimeout)
s.inbound, s.outbound, s.router, s.network, var errors error
) for serviceName, service := range s.postServices {
for _, lifecycleService := range s.services { monitor.Start("close ", serviceName)
err = E.Append(err, lifecycleService.Close(), func(err error) error { errors = E.Append(errors, service.Close(), func(err error) error {
return E.Cause(err, "close ", lifecycleService.Name()) return E.Cause(err, "close ", serviceName)
})
monitor.Finish()
}
for i, in := range s.inbounds {
monitor.Start("close inbound/", in.Type(), "[", i, "]")
errors = E.Append(errors, in.Close(), func(err error) error {
return E.Cause(err, "close inbound/", in.Type(), "[", i, "]")
})
monitor.Finish()
}
for i, out := range s.outbounds {
monitor.Start("close outbound/", out.Type(), "[", i, "]")
errors = E.Append(errors, common.Close(out), func(err error) error {
return E.Cause(err, "close outbound/", out.Type(), "[", i, "]")
})
monitor.Finish()
}
monitor.Start("close router")
if err := common.Close(s.router); err != nil {
errors = E.Append(errors, err, func(err error) error {
return E.Cause(err, "close router")
}) })
} }
err = E.Append(err, s.logFactory.Close(), func(err error) error { monitor.Finish()
for serviceName, service := range s.preServices1 {
monitor.Start("close ", serviceName)
errors = E.Append(errors, service.Close(), func(err error) error {
return E.Cause(err, "close ", serviceName)
})
monitor.Finish()
}
for serviceName, service := range s.preServices2 {
monitor.Start("close ", serviceName)
errors = E.Append(errors, service.Close(), func(err error) error {
return E.Cause(err, "close ", serviceName)
})
monitor.Finish()
}
if err := common.Close(s.logFactory); err != nil {
errors = E.Append(errors, err, func(err error) error {
return E.Cause(err, "close logger") return E.Cause(err, "close logger")
}) })
return err }
} return errors
func (s *Box) Network() adapter.NetworkManager {
return s.network
} }
func (s *Box) Router() adapter.Router { func (s *Box) Router() adapter.Router {
return s.router return s.router
} }
func (s *Box) Inbound() adapter.InboundManager {
return s.inbound
}
func (s *Box) Outbound() adapter.OutboundManager {
return s.outbound
}

85
box_outbound.go Normal file
View File

@@ -0,0 +1,85 @@
package box
import (
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
)
func (s *Box) startOutbounds() error {
monitor := taskmonitor.New(s.logger, C.StartTimeout)
outboundTags := make(map[adapter.Outbound]string)
outbounds := make(map[string]adapter.Outbound)
for i, outboundToStart := range s.outbounds {
var outboundTag string
if outboundToStart.Tag() == "" {
outboundTag = F.ToString(i)
} else {
outboundTag = outboundToStart.Tag()
}
if _, exists := outbounds[outboundTag]; exists {
return E.New("outbound tag ", outboundTag, " duplicated")
}
outboundTags[outboundToStart] = outboundTag
outbounds[outboundTag] = outboundToStart
}
started := make(map[string]bool)
for {
canContinue := false
startOne:
for _, outboundToStart := range s.outbounds {
outboundTag := outboundTags[outboundToStart]
if started[outboundTag] {
continue
}
dependencies := outboundToStart.Dependencies()
for _, dependency := range dependencies {
if !started[dependency] {
continue startOne
}
}
started[outboundTag] = true
canContinue = true
if starter, isStarter := outboundToStart.(interface {
Start() error
}); isStarter {
monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
err := starter.Start()
monitor.Finish()
if err != nil {
return E.Cause(err, "initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
}
}
}
if len(started) == len(s.outbounds) {
break
}
if canContinue {
continue
}
currentOutbound := common.Find(s.outbounds, func(it adapter.Outbound) bool {
return !started[outboundTags[it]]
})
var lintOutbound func(oTree []string, oCurrent adapter.Outbound) error
lintOutbound = func(oTree []string, oCurrent adapter.Outbound) error {
problemOutboundTag := common.Find(oCurrent.Dependencies(), func(it string) bool {
return !started[it]
})
if common.Contains(oTree, problemOutboundTag) {
return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag)
}
problemOutbound := outbounds[problemOutboundTag]
if problemOutbound == nil {
return E.New("dependency[", problemOutboundTag, "] not found for outbound[", outboundTags[oCurrent], "]")
}
return lintOutbound(append(oTree, problemOutboundTag), problemOutbound)
}
return lintOutbound([]string{outboundTags[currentOutbound]}, currentOutbound)
}
return nil
}

View File

@@ -58,7 +58,7 @@ func FindSDK() {
} }
func findNDK() bool { func findNDK() bool {
const fixedVersion = "26.3.11579264" const fixedVersion = "27.2.12479018"
const versionFile = "source.properties" const versionFile = "source.properties"
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) { if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
androidNDKPath = fixedPath androidNDKPath = fixedPath

View File

@@ -68,6 +68,6 @@ func preRun(cmd *cobra.Command, args []string) {
if len(configPaths) == 0 && len(configDirectories) == 0 { if len(configPaths) == 0 && len(configDirectories) == 0 {
configPaths = append(configPaths, "config.json") configPaths = append(configPaths, "config.json")
} }
globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger())) globalCtx = service.ContextWith(globalCtx, deprecated.NewEnvManager(log.StdLogger()))
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry()) globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry())
} }

View File

@@ -30,7 +30,7 @@ func check() error {
if err != nil { if err != nil {
return err return err
} }
ctx, cancel := context.WithCancel(globalCtx) ctx, cancel := context.WithCancel(context.Background())
instance, err := box.New(box.Options{ instance, err := box.New(box.Options{
Context: ctx, Context: ctx,
Options: options, Options: options,

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"context"
"os" "os"
"path/filepath" "path/filepath"
@@ -38,7 +39,7 @@ func format() error {
return err return err
} }
for _, optionsEntry := range optionsList { for _, optionsEntry := range optionsList {
optionsEntry.options, err = badjson.Omitempty(globalCtx, optionsEntry.options) optionsEntry.options, err = badjson.Omitempty(context.TODO(), optionsEntry.options)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -6,6 +6,7 @@ import (
"strings" "strings"
"github.com/sagernet/sing-box/common/srs" "github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
@@ -55,6 +56,10 @@ func compileRuleSet(sourcePath string) error {
if err != nil { if err != nil {
return err return err
} }
ruleSet, err := plainRuleSet.Upgrade()
if err != nil {
return err
}
var outputPath string var outputPath string
if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput { if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".json") { if strings.HasSuffix(sourcePath, ".json") {
@@ -69,7 +74,7 @@ func compileRuleSet(sourcePath string) error {
if err != nil { if err != nil {
return err return err
} }
err = srs.Write(outputFile, plainRuleSet.Options, plainRuleSet.Version) err = srs.Write(outputFile, ruleSet, plainRuleSet.Version == C.RuleSetVersion2)
if err != nil { if err != nil {
outputFile.Close() outputFile.Close()
os.Remove(outputPath) os.Remove(outputPath)

View File

@@ -7,7 +7,6 @@ import (
"github.com/sagernet/sing-box/cmd/sing-box/internal/convertor/adguard" "github.com/sagernet/sing-box/cmd/sing-box/internal/convertor/adguard"
"github.com/sagernet/sing-box/common/srs" "github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@@ -78,7 +77,7 @@ func convertRuleSet(sourcePath string) error {
return err return err
} }
defer outputFile.Close() defer outputFile.Close()
err = srs.Write(outputFile, option.PlainRuleSet{Rules: rules}, C.RuleSetVersion2) err = srs.Write(outputFile, option.PlainRuleSet{Rules: rules}, true)
if err != nil { if err != nil {
outputFile.Close() outputFile.Close()
os.Remove(outputPath) os.Remove(outputPath)

View File

@@ -6,7 +6,9 @@ import (
"strings" "strings"
"github.com/sagernet/sing-box/common/srs" "github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -46,10 +48,14 @@ func decompileRuleSet(sourcePath string) error {
return err return err
} }
} }
ruleSet, err := srs.Read(reader, true) plainRuleSet, err := srs.Read(reader, true)
if err != nil { if err != nil {
return err return err
} }
ruleSet := option.PlainRuleSetCompat{
Version: C.RuleSetVersion1,
Options: plainRuleSet,
}
var outputPath string var outputPath string
if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput { if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".srs") { if strings.HasSuffix(sourcePath, ".srs") {

View File

@@ -2,7 +2,6 @@ package main
import ( import (
"bytes" "bytes"
"context"
"io" "io"
"os" "os"
@@ -56,25 +55,26 @@ func ruleSetMatch(sourcePath string, domain string) error {
if err != nil { if err != nil {
return E.Cause(err, "read rule-set") return E.Cause(err, "read rule-set")
} }
var ruleSet option.PlainRuleSetCompat var plainRuleSet option.PlainRuleSet
switch flagRuleSetMatchFormat { switch flagRuleSetMatchFormat {
case C.RuleSetFormatSource: case C.RuleSetFormatSource:
ruleSet, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content) var compat option.PlainRuleSetCompat
compat, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content)
if err != nil {
return err
}
plainRuleSet, err = compat.Upgrade()
if err != nil { if err != nil {
return err return err
} }
case C.RuleSetFormatBinary: case C.RuleSetFormatBinary:
ruleSet, err = srs.Read(bytes.NewReader(content), false) plainRuleSet, err = srs.Read(bytes.NewReader(content), false)
if err != nil { if err != nil {
return err return err
} }
default: default:
return E.New("unknown rule-set format: ", flagRuleSetMatchFormat) return E.New("unknown rule-set format: ", flagRuleSetMatchFormat)
} }
plainRuleSet, err := ruleSet.Upgrade()
if err != nil {
return err
}
ipAddress := M.ParseAddr(domain) ipAddress := M.ParseAddr(domain)
var metadata adapter.InboundContext var metadata adapter.InboundContext
if ipAddress.IsValid() { if ipAddress.IsValid() {
@@ -84,7 +84,7 @@ func ruleSetMatch(sourcePath string, domain string) error {
} }
for i, ruleOptions := range plainRuleSet.Rules { for i, ruleOptions := range plainRuleSet.Rules {
var currentRule adapter.HeadlessRule var currentRule adapter.HeadlessRule
currentRule, err = rule.NewHeadlessRule(context.Background(), ruleOptions) currentRule, err = rule.NewHeadlessRule(nil, ruleOptions)
if err != nil { if err != nil {
return E.Cause(err, "parse rule_set.rules.[", i, "]") return E.Cause(err, "parse rule_set.rules.[", i, "]")
} }

View File

@@ -41,11 +41,11 @@ func createPreStartedClient() (*box.Box, error) {
return instance, nil return instance, nil
} }
func createDialer(instance *box.Box, outboundTag string) (N.Dialer, error) { func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) {
if outboundTag == "" { if outboundTag == "" {
return instance.Outbound().Default(), nil return instance.Router().DefaultOutbound(N.NetworkName(network))
} else { } else {
outbound, loaded := instance.Outbound().Outbound(outboundTag) outbound, loaded := instance.Router().Outbound(outboundTag)
if !loaded { if !loaded {
return nil, E.New("outbound not found: ", outboundTag) return nil, E.New("outbound not found: ", outboundTag)
} }

View File

@@ -45,7 +45,7 @@ func connect(address string) error {
return err return err
} }
defer instance.Close() defer instance.Close()
dialer, err := createDialer(instance, commandToolsFlagOutbound) dialer, err := createDialer(instance, commandConnectFlagNetwork, commandToolsFlagOutbound)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -48,7 +48,7 @@ func fetch(args []string) error {
httpClient = &http.Client{ httpClient = &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
dialer, err := createDialer(instance, commandToolsFlagOutbound) dialer, err := createDialer(instance, network, commandToolsFlagOutbound)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -16,7 +16,7 @@ import (
) )
func initializeHTTP3Client(instance *box.Box) error { func initializeHTTP3Client(instance *box.Box) error {
dialer, err := createDialer(instance, commandToolsFlagOutbound) dialer, err := createDialer(instance, N.NetworkUDP, commandToolsFlagOutbound)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -9,6 +9,7 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -44,7 +45,7 @@ func syncTime() error {
if err != nil { if err != nil {
return err return err
} }
dialer, err := createDialer(instance, commandToolsFlagOutbound) dialer, err := createDialer(instance, N.NetworkUDP, commandToolsFlagOutbound)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -1,7 +1,6 @@
package adguard package adguard
import ( import (
"context"
"strings" "strings"
"testing" "testing"
@@ -27,7 +26,7 @@ example.arpa
`)) `))
require.NoError(t, err) require.NoError(t, err)
require.Len(t, rules, 1) require.Len(t, rules, 1)
rule, err := rule.NewHeadlessRule(context.Background(), rules[0]) rule, err := rule.NewHeadlessRule(nil, rules[0])
require.NoError(t, err) require.NoError(t, err)
matchDomain := []string{ matchDomain := []string{
"example.org", "example.org",
@@ -86,7 +85,7 @@ func TestHosts(t *testing.T) {
`)) `))
require.NoError(t, err) require.NoError(t, err)
require.Len(t, rules, 1) require.Len(t, rules, 1)
rule, err := rule.NewHeadlessRule(context.Background(), rules[0]) rule, err := rule.NewHeadlessRule(nil, rules[0])
require.NoError(t, err) require.NoError(t, err)
matchDomain := []string{ matchDomain := []string{
"google.com", "google.com",
@@ -116,7 +115,7 @@ www.example.org
`)) `))
require.NoError(t, err) require.NoError(t, err)
require.Len(t, rules, 1) require.Len(t, rules, 1)
rule, err := rule.NewHeadlessRule(context.Background(), rules[0]) rule, err := rule.NewHeadlessRule(nil, rules[0])
require.NoError(t, err) require.NoError(t, err)
matchDomain := []string{ matchDomain := []string{
"example.com", "example.com",

View File

@@ -3,7 +3,6 @@ package dialer
import ( import (
"context" "context"
"net" "net"
"net/netip"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -29,31 +28,31 @@ type DefaultDialer struct {
isWireGuardListener bool isWireGuardListener bool
} }
func NewDefault(networkManager adapter.NetworkManager, options option.DialerOptions) (*DefaultDialer, error) { func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDialer, error) {
var dialer net.Dialer var dialer net.Dialer
var listener net.ListenConfig var listener net.ListenConfig
if options.BindInterface != "" { if options.BindInterface != "" {
var interfaceFinder control.InterfaceFinder var interfaceFinder control.InterfaceFinder
if networkManager != nil { if router != nil {
interfaceFinder = networkManager.InterfaceFinder() interfaceFinder = router.InterfaceFinder()
} else { } else {
interfaceFinder = control.NewDefaultInterfaceFinder() interfaceFinder = control.NewDefaultInterfaceFinder()
} }
bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1) bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1)
dialer.Control = control.Append(dialer.Control, bindFunc) dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)
} else if networkManager != nil && networkManager.AutoDetectInterface() { } else if router != nil && router.AutoDetectInterface() {
bindFunc := networkManager.AutoDetectInterfaceFunc() bindFunc := router.AutoDetectInterfaceFunc()
dialer.Control = control.Append(dialer.Control, bindFunc) dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)
} else if networkManager != nil && networkManager.DefaultInterface() != "" { } else if router != nil && router.DefaultInterface() != "" {
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), networkManager.DefaultInterface(), -1) bindFunc := control.BindToInterface(router.InterfaceFinder(), router.DefaultInterface(), -1)
dialer.Control = control.Append(dialer.Control, bindFunc) dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)
} }
var autoRedirectOutputMark uint32 var autoRedirectOutputMark uint32
if networkManager != nil { if router != nil {
autoRedirectOutputMark = networkManager.AutoRedirectOutputMark() autoRedirectOutputMark = router.AutoRedirectOutputMark()
} }
if autoRedirectOutputMark > 0 { if autoRedirectOutputMark > 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark)) dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark))
@@ -65,9 +64,9 @@ func NewDefault(networkManager adapter.NetworkManager, options option.DialerOpti
if autoRedirectOutputMark > 0 { if autoRedirectOutputMark > 0 {
return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `routing_mark`") return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `routing_mark`")
} }
} else if networkManager != nil && networkManager.DefaultMark() > 0 { } else if router != nil && router.DefaultMark() > 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(networkManager.DefaultMark())) dialer.Control = control.Append(dialer.Control, control.RoutingMark(router.DefaultMark()))
listener.Control = control.Append(listener.Control, control.RoutingMark(networkManager.DefaultMark())) listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark()))
if autoRedirectOutputMark > 0 { if autoRedirectOutputMark > 0 {
return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `default_mark`") return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `default_mark`")
} }
@@ -103,7 +102,7 @@ func NewDefault(networkManager adapter.NetworkManager, options option.DialerOpti
udpAddr4 string udpAddr4 string
) )
if options.Inet4BindAddress != nil { if options.Inet4BindAddress != nil {
bindAddr := options.Inet4BindAddress.Build(netip.IPv4Unspecified()) bindAddr := options.Inet4BindAddress.Build()
dialer4.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()} dialer4.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
udpDialer4.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()} udpDialer4.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
udpAddr4 = M.SocksaddrFrom(bindAddr, 0).String() udpAddr4 = M.SocksaddrFrom(bindAddr, 0).String()
@@ -114,7 +113,7 @@ func NewDefault(networkManager adapter.NetworkManager, options option.DialerOpti
udpAddr6 string udpAddr6 string
) )
if options.Inet6BindAddress != nil { if options.Inet6BindAddress != nil {
bindAddr := options.Inet6BindAddress.Build(netip.IPv6Unspecified()) bindAddr := options.Inet6BindAddress.Build()
dialer6.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()} dialer6.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()} udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String() udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()

View File

@@ -12,15 +12,15 @@ import (
) )
type DetourDialer struct { type DetourDialer struct {
outboundManager adapter.OutboundManager router adapter.Router
detour string detour string
dialer N.Dialer dialer N.Dialer
initOnce sync.Once initOnce sync.Once
initErr error initErr error
} }
func NewDetour(outboundManager adapter.OutboundManager, detour string) N.Dialer { func NewDetour(router adapter.Router, detour string) N.Dialer {
return &DetourDialer{outboundManager: outboundManager, detour: detour} return &DetourDialer{router: router, detour: detour}
} }
func (d *DetourDialer) Start() error { func (d *DetourDialer) Start() error {
@@ -31,7 +31,7 @@ func (d *DetourDialer) Start() error {
func (d *DetourDialer) Dialer() (N.Dialer, error) { func (d *DetourDialer) Dialer() (N.Dialer, error) {
d.initOnce.Do(func() { d.initOnce.Do(func() {
var loaded bool var loaded bool
d.dialer, loaded = d.outboundManager.Outbound(d.detour) d.dialer, loaded = d.router.Outbound(d.detour)
if !loaded { if !loaded {
d.initErr = E.New("outbound detour not found: ", d.detour) d.initErr = E.New("outbound detour not found: ", d.detour)
} }

View File

@@ -1,44 +1,34 @@
package dialer package dialer
import ( import (
"context"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-dns" "github.com/sagernet/sing-dns"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
) )
func New(ctx context.Context, options option.DialerOptions) (N.Dialer, error) { func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error) {
networkManager := service.FromContext[adapter.NetworkManager](ctx)
if options.IsWireGuardListener { if options.IsWireGuardListener {
return NewDefault(networkManager, options) return NewDefault(router, options)
}
if router == nil {
return NewDefault(nil, options)
} }
var ( var (
dialer N.Dialer dialer N.Dialer
err error err error
) )
if options.Detour == "" { if options.Detour == "" {
dialer, err = NewDefault(networkManager, options) dialer, err = NewDefault(router, options)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else { } else {
outboundManager := service.FromContext[adapter.OutboundManager](ctx) dialer = NewDetour(router, options.Detour)
if outboundManager == nil {
return nil, E.New("missing outbound manager")
}
dialer = NewDetour(outboundManager, options.Detour)
}
if networkManager == nil {
return NewDefault(networkManager, options)
} }
if options.Detour == "" { if options.Detour == "" {
router := service.FromContext[adapter.Router](ctx)
if router != nil {
dialer = NewResolveDialer( dialer = NewResolveDialer(
router, router,
dialer, dialer,
@@ -46,6 +36,5 @@ func New(ctx context.Context, options option.DialerOptions) (N.Dialer, error) {
dns.DomainStrategy(options.DomainStrategy), dns.DomainStrategy(options.DomainStrategy),
time.Duration(options.FallbackDelay)) time.Duration(options.FallbackDelay))
} }
}
return dialer, nil return dialer, nil
} }

View File

@@ -9,22 +9,30 @@ import (
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
type DefaultOutboundDialer struct { type RouterDialer struct {
outboundManager adapter.OutboundManager router adapter.Router
} }
func NewDefaultOutbound(outboundManager adapter.OutboundManager) N.Dialer { func NewRouter(router adapter.Router) N.Dialer {
return &DefaultOutboundDialer{outboundManager: outboundManager} return &RouterDialer{router: router}
} }
func (d *DefaultOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { func (d *RouterDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
return d.outboundManager.Default().DialContext(ctx, network, destination) dialer, err := d.router.DefaultOutbound(network)
if err != nil {
return nil, err
}
return dialer.DialContext(ctx, network, destination)
} }
func (d *DefaultOutboundDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { func (d *RouterDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return d.outboundManager.Default().ListenPacket(ctx, destination) dialer, err := d.router.DefaultOutbound(N.NetworkUDP)
if err != nil {
return nil, err
}
return dialer.ListenPacket(ctx, destination)
} }
func (d *DefaultOutboundDialer) Upstream() any { func (d *RouterDialer) Upstream() any {
return d.outboundManager.Default() return d.router
} }

View File

@@ -3,7 +3,6 @@ package listener
import ( import (
"context" "context"
"net" "net"
"net/netip"
"sync/atomic" "sync/atomic"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -93,7 +92,7 @@ func (l *Listener) Start() error {
if l.setSystemProxy { if l.setSystemProxy {
listenPort := M.SocksaddrFromNet(l.tcpListener.Addr()).Port listenPort := M.SocksaddrFromNet(l.tcpListener.Addr()).Port
var listenAddrString string var listenAddrString string
listenAddr := l.listenOptions.Listen.Build(netip.IPv4Unspecified()) listenAddr := l.listenOptions.Listen.Build()
if listenAddr.IsUnspecified() { if listenAddr.IsUnspecified() {
listenAddrString = "127.0.0.1" listenAddrString = "127.0.0.1"
} else { } else {

View File

@@ -2,7 +2,6 @@ package listener
import ( import (
"net" "net"
"net/netip"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -17,7 +16,7 @@ import (
func (l *Listener) ListenTCP() (net.Listener, error) { func (l *Listener) ListenTCP() (net.Listener, error) {
var err error var err error
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort) bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(), l.listenOptions.ListenPort)
var tcpListener net.Listener var tcpListener net.Listener
var listenConfig net.ListenConfig var listenConfig net.ListenConfig
if l.listenOptions.TCPKeepAlive >= 0 { if l.listenOptions.TCPKeepAlive >= 0 {

View File

@@ -2,7 +2,6 @@ package listener
import ( import (
"net" "net"
"net/netip"
"os" "os"
"time" "time"
@@ -14,7 +13,7 @@ import (
) )
func (l *Listener) ListenUDP() (net.PacketConn, error) { func (l *Listener) ListenUDP() (net.PacketConn, error) {
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort) bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(), l.listenOptions.ListenPort)
var lc net.ListenConfig var lc net.ListenConfig
var udpFragment bool var udpFragment bool
if l.listenOptions.UDPFragment != nil { if l.listenOptions.UDPFragment != nil {
@@ -49,9 +48,9 @@ func (l *Listener) loopUDPIn() {
if !l.threadUnsafePacketWriter { if !l.threadUnsafePacketWriter {
buffer = buf.NewPacket() buffer = buf.NewPacket()
defer buffer.Release() defer buffer.Release()
}
buffer.IncRef() buffer.IncRef()
defer buffer.DecRef() defer buffer.DecRef()
}
if l.oobPacketHandler != nil { if l.oobPacketHandler != nil {
oob := make([]byte, 1024) oob := make([]byte, 1024)
for { for {

View File

@@ -2,6 +2,7 @@ package settings
import ( import (
"context" "context"
"net/netip"
"strconv" "strconv"
"strings" "strings"
@@ -11,7 +12,6 @@ import (
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/shell" "github.com/sagernet/sing/common/shell"
"github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service"
) )
type DarwinSystemProxy struct { type DarwinSystemProxy struct {
@@ -24,7 +24,7 @@ type DarwinSystemProxy struct {
} }
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*DarwinSystemProxy, error) { func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*DarwinSystemProxy, error) {
interfaceMonitor := service.FromContext[adapter.NetworkManager](ctx).InterfaceMonitor() interfaceMonitor := adapter.RouterFromContext(ctx).InterfaceMonitor()
if interfaceMonitor == nil { if interfaceMonitor == nil {
return nil, E.New("missing interface monitor") return nil, E.New("missing interface monitor")
} }
@@ -76,14 +76,14 @@ func (p *DarwinSystemProxy) update(event int) {
} }
func (p *DarwinSystemProxy) update0() error { func (p *DarwinSystemProxy) update0() error {
newInterface := p.monitor.DefaultInterface() newInterfaceName := p.monitor.DefaultInterfaceName(netip.IPv4Unspecified())
if p.interfaceName == newInterface.Name { if p.interfaceName == newInterfaceName {
return nil return nil
} }
if p.interfaceName != "" { if p.interfaceName != "" {
_ = p.Disable() _ = p.Disable()
} }
p.interfaceName = newInterface.Name p.interfaceName = newInterfaceName
interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName) interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName)
if err != nil { if err != nil {
return err return err

View File

@@ -38,13 +38,10 @@ const (
ruleItemWIFIBSSID ruleItemWIFIBSSID
ruleItemAdGuardDomain ruleItemAdGuardDomain
ruleItemProcessPathRegex ruleItemProcessPathRegex
ruleItemNetworkType
ruleItemNetworkIsExpensive
ruleItemNetworkIsConstrained
ruleItemFinal uint8 = 0xFF ruleItemFinal uint8 = 0xFF
) )
func Read(reader io.Reader, recover bool) (ruleSetCompat option.PlainRuleSetCompat, err error) { func Read(reader io.Reader, recover bool) (ruleSet option.PlainRuleSet, err error) {
var magicBytes [3]byte var magicBytes [3]byte
_, err = io.ReadFull(reader, magicBytes[:]) _, err = io.ReadFull(reader, magicBytes[:])
if err != nil { if err != nil {
@@ -57,10 +54,10 @@ func Read(reader io.Reader, recover bool) (ruleSetCompat option.PlainRuleSetComp
var version uint8 var version uint8
err = binary.Read(reader, binary.BigEndian, &version) err = binary.Read(reader, binary.BigEndian, &version)
if err != nil { if err != nil {
return ruleSetCompat, err return ruleSet, err
} }
if version > C.RuleSetVersionCurrent { if version > C.RuleSetVersion2 {
return ruleSetCompat, E.New("unsupported version: ", version) return ruleSet, E.New("unsupported version: ", version)
} }
compressReader, err := zlib.NewReader(reader) compressReader, err := zlib.NewReader(reader)
if err != nil { if err != nil {
@@ -71,10 +68,9 @@ func Read(reader io.Reader, recover bool) (ruleSetCompat option.PlainRuleSetComp
if err != nil { if err != nil {
return return
} }
ruleSetCompat.Version = version ruleSet.Rules = make([]option.HeadlessRule, length)
ruleSetCompat.Options.Rules = make([]option.HeadlessRule, length)
for i := uint64(0); i < length; i++ { for i := uint64(0); i < length; i++ {
ruleSetCompat.Options.Rules[i], err = readRule(bReader, recover) ruleSet.Rules[i], err = readRule(bReader, recover)
if err != nil { if err != nil {
err = E.Cause(err, "read rule[", i, "]") err = E.Cause(err, "read rule[", i, "]")
return return
@@ -83,12 +79,18 @@ func Read(reader io.Reader, recover bool) (ruleSetCompat option.PlainRuleSetComp
return return
} }
func Write(writer io.Writer, ruleSet option.PlainRuleSet, generateVersion uint8) error { func Write(writer io.Writer, ruleSet option.PlainRuleSet, generateUnstable bool) error {
_, err := writer.Write(MagicBytes[:]) _, err := writer.Write(MagicBytes[:])
if err != nil { if err != nil {
return err return err
} }
err = binary.Write(writer, binary.BigEndian, generateVersion) var version uint8
if generateUnstable {
version = C.RuleSetVersion2
} else {
version = C.RuleSetVersion1
}
err = binary.Write(writer, binary.BigEndian, version)
if err != nil { if err != nil {
return err return err
} }
@@ -102,7 +104,7 @@ func Write(writer io.Writer, ruleSet option.PlainRuleSet, generateVersion uint8)
return err return err
} }
for _, rule := range ruleSet.Rules { for _, rule := range ruleSet.Rules {
err = writeRule(bWriter, rule, generateVersion) err = writeRule(bWriter, rule, generateUnstable)
if err != nil { if err != nil {
return err return err
} }
@@ -133,12 +135,12 @@ func readRule(reader varbin.Reader, recover bool) (rule option.HeadlessRule, err
return return
} }
func writeRule(writer varbin.Writer, rule option.HeadlessRule, generateVersion uint8) error { func writeRule(writer varbin.Writer, rule option.HeadlessRule, generateUnstable bool) error {
switch rule.Type { switch rule.Type {
case C.RuleTypeDefault: case C.RuleTypeDefault:
return writeDefaultRule(writer, rule.DefaultOptions, generateVersion) return writeDefaultRule(writer, rule.DefaultOptions, generateUnstable)
case C.RuleTypeLogical: case C.RuleTypeLogical:
return writeLogicalRule(writer, rule.LogicalOptions, generateVersion) return writeLogicalRule(writer, rule.LogicalOptions, generateUnstable)
default: default:
panic("unknown rule type: " + rule.Type) panic("unknown rule type: " + rule.Type)
} }
@@ -225,12 +227,6 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
return return
} }
rule.AdGuardDomainMatcher = matcher rule.AdGuardDomainMatcher = matcher
case ruleItemNetworkType:
rule.NetworkType, err = readRuleItemString(reader)
case ruleItemNetworkIsExpensive:
rule.NetworkIsExpensive = true
case ruleItemNetworkIsConstrained:
rule.NetworkIsConstrained = true
case ruleItemFinal: case ruleItemFinal:
err = binary.Read(reader, binary.BigEndian, &rule.Invert) err = binary.Read(reader, binary.BigEndian, &rule.Invert)
return return
@@ -244,7 +240,7 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
} }
} }
func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, generateVersion uint8) error { func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, generateUnstable bool) error {
err := binary.Write(writer, binary.BigEndian, uint8(0)) err := binary.Write(writer, binary.BigEndian, uint8(0))
if err != nil { if err != nil {
return err return err
@@ -268,7 +264,7 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
if err != nil { if err != nil {
return err return err
} }
err = domain.NewMatcher(rule.Domain, rule.DomainSuffix, generateVersion == C.RuleSetVersion1).Write(writer) err = domain.NewMatcher(rule.Domain, rule.DomainSuffix, !generateUnstable).Write(writer)
if err != nil { if err != nil {
return err return err
} }
@@ -345,27 +341,6 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
return err return err
} }
} }
if len(rule.NetworkType) > 0 {
if generateVersion < C.RuleSetVersion3 {
return E.New("network_type rule item is only supported in version 3 or later")
}
err = writeRuleItemString(writer, ruleItemNetworkType, rule.NetworkType)
if err != nil {
return err
}
}
if rule.NetworkIsExpensive {
err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsExpensive)
if err != nil {
return err
}
}
if rule.NetworkIsConstrained {
err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsConstrained)
if err != nil {
return err
}
}
if len(rule.WIFISSID) > 0 { if len(rule.WIFISSID) > 0 {
err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID) err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID)
if err != nil { if err != nil {
@@ -379,9 +354,6 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
} }
} }
if len(rule.AdGuardDomain) > 0 { if len(rule.AdGuardDomain) > 0 {
if generateVersion < C.RuleSetVersion2 {
return E.New("AdGuard rule items is only supported in version 2 or later")
}
err = binary.Write(writer, binary.BigEndian, ruleItemAdGuardDomain) err = binary.Write(writer, binary.BigEndian, ruleItemAdGuardDomain)
if err != nil { if err != nil {
return err return err
@@ -485,7 +457,7 @@ func readLogicalRule(reader varbin.Reader, recovery bool) (logicalRule option.Lo
return return
} }
func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRule, generateVersion uint8) error { func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRule, generateUnstable bool) error {
err := binary.Write(writer, binary.BigEndian, uint8(1)) err := binary.Write(writer, binary.BigEndian, uint8(1))
if err != nil { if err != nil {
return err return err
@@ -506,7 +478,7 @@ func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRu
return err return err
} }
for _, rule := range logicalRule.Rules { for _, rule := range logicalRule.Rules {
err = writeRule(writer, rule, generateVersion) err = writeRule(writer, rule, generateUnstable)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -19,7 +19,6 @@ import (
"github.com/sagernet/sing-dns" "github.com/sagernet/sing-dns"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/service"
mDNS "github.com/miekg/dns" mDNS "github.com/miekg/dns"
) )
@@ -214,7 +213,7 @@ func fetchECHClientConfig(ctx context.Context) func(_ context.Context, serverNam
}, },
}, },
} }
response, err := service.FromContext[adapter.Router](ctx).Exchange(ctx, message) response, err := adapter.RouterFromContext(ctx).Exchange(ctx, message)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/sagernet/reality" "github.com/sagernet/reality"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
@@ -101,7 +102,7 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb
tlsConfig.ShortIds[shortID] = true tlsConfig.ShortIds[shortID] = true
} }
handshakeDialer, err := dialer.New(ctx, options.Reality.Handshake.DialerOptions) handshakeDialer, err := dialer.New(adapter.RouterFromContext(ctx), options.Reality.Handshake.DialerOptions)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -1,8 +0,0 @@
package constant
const (
InterfaceTypeWIFI = "wifi"
InterfaceTypeCellular = "cellular"
InterfaceTypeEthernet = "ethernet"
InterfaceTypeOther = "other"
)

View File

@@ -6,7 +6,7 @@ import (
const IsAndroid = goos.IsAndroid == 1 const IsAndroid = goos.IsAndroid == 1
const IsDarwin = goos.IsDarwin == 1 || goos.IsIos == 1 const IsDarwin = goos.IsDarwin == 1
const IsDragonfly = goos.IsDragonfly == 1 const IsDragonfly = goos.IsDragonfly == 1

View File

@@ -21,14 +21,11 @@ const (
const ( const (
RuleSetVersion1 = 1 + iota RuleSetVersion1 = 1 + iota
RuleSetVersion2 RuleSetVersion2
RuleSetVersion3
RuleSetVersionCurrent = RuleSetVersion3
) )
const ( const (
RuleActionTypeRoute = "route" RuleActionTypeRoute = "route"
RuleActionTypeRouteOptions = "route-options" RuleActionTypeReturn = "return"
RuleActionTypeDirect = "direct"
RuleActionTypeReject = "reject" RuleActionTypeReject = "reject"
RuleActionTypeHijackDNS = "hijack-dns" RuleActionTypeHijackDNS = "hijack-dns"
RuleActionTypeSniff = "sniff" RuleActionTypeSniff = "sniff"
@@ -37,5 +34,9 @@ const (
const ( const (
RuleActionRejectMethodDefault = "default" RuleActionRejectMethodDefault = "default"
RuleActionRejectMethodReset = "reset"
RuleActionRejectMethodNetworkUnreachable = "network-unreachable"
RuleActionRejectMethodHostUnreachable = "host-unreachable"
RuleActionRejectMethodPortUnreachable = "port-unreachable"
RuleActionRejectMethodDrop = "drop" RuleActionRejectMethodDrop = "drop"
) )

View File

@@ -2,78 +2,10 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
#### 1.11.0-alpha.13 #### 1.11.0-alpha.5
* Fixes and improvements * Fixes and improvements
#### 1.11.0-alpha.12
* Merge route options to route actions **1**
* Add `network_type`, `network_is_expensive` and `network_is_constrainted` rule items **2**
* Fixes and improvements
**1**:
Route options in DNS route actions will no longer be considered deprecated,
see [DNS Route Action](/configuration/dns/rule_action/).
Also, now `udp_disable_domain_unmapping` and `udp_connect` can also be configured in route action,
see [Route Action](/configuration/route/rule_action/).
**2**:
When using in graphical clients, new routing rule items allow you to match on
network type (WIFI, cellular, etc.), whether the network is expensive, and whether Low Data Mode is enabled.
See [Route Rule](/configuration/route/rule/), [DNS Route Rule](/configuration/dns/rule/)
and [Headless Rule](/configuration/rule-set/headless-rule/).
#### 1.11.0-alpha.9
* Improve tun compatibility **1**
* Fixes and improvements
**1**:
When `gvisor` tun stack is enabled, even if the request passes routing,
if the outbound connection establishment fails,
the connection still does not need to be established and a TCP RST is replied.
#### 1.11.0-alpha.7
* Introducing rule actions **1**
**1**:
New rule actions replace legacy inbound fields and special outbound fields,
and can be used for pre-matching **2**.
See [Rule](/configuration/route/rule/),
[Rule Action](/configuration/route/rule_action/),
[DNS Rule](/configuration/dns/rule/) and
[DNS Rule Action](/configuration/dns/rule_action/).
For migration, see
[Migrate legacy special outbounds to rule actions](/migration/#migrate-legacy-special-outbounds-to-rule-actions),
[Migrate legacy inbound fields to rule actions](/migration/#migrate-legacy-inbound-fields-to-rule-actions)
and [Migrate legacy DNS route options to rule actions](/migration/#migrate-legacy-dns-route-options-to-rule-actions).
**2**:
Similar to Surge's pre-matching.
Specifically, the new rule actions allow you to reject connections with
TCP RST (for TCP connections) and ICMP port unreachable (for UDP packets)
before connection established to improve tun's compatibility.
See [Rule Action](/configuration/route/rule_action/).
#### 1.11.0-alpha.6
* Update quic-go to v0.48.1
* Set gateway for tun correctly
* Fixes and improvements
#### 1.11.0-alpha.2 #### 1.11.0-alpha.2
* Add warnings for usage of deprecated features * Add warnings for usage of deprecated features

View File

@@ -1,3 +1,7 @@
---
icon: material/new-box
---
!!! quote "Changes in sing-box 1.9.0" !!! quote "Changes in sing-box 1.9.0"
:material-plus: [client_subnet](#client_subnet) :material-plus: [client_subnet](#client_subnet)

View File

@@ -1,3 +1,7 @@
---
icon: material/new-box
---
!!! quote "sing-box 1.9.0 中的更改" !!! quote "sing-box 1.9.0 中的更改"
:material-plus: [client_subnet](#client_subnet) :material-plus: [client_subnet](#client_subnet)

View File

@@ -2,17 +2,6 @@
icon: material/new-box icon: material/new-box
--- ---
!!! quote "Changes in sing-box 1.11.0"
:material-plus: [action](#action)
:material-alert: [server](#server)
:material-alert: [disable_cache](#disable_cache)
:material-alert: [rewrite_ttl](#rewrite_ttl)
:material-alert: [client_subnet](#client_subnet)
:material-plus: [network_type](#network_type)
:material-plus: [network_is_expensive](#network_is_expensive)
:material-plus: [network_is_constrained](#network_is_constrained)
!!! quote "Changes in sing-box 1.10.0" !!! quote "Changes in sing-box 1.10.0"
:material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
@@ -128,11 +117,6 @@ icon: material/new-box
1000 1000
], ],
"clash_mode": "direct", "clash_mode": "direct",
"network_type": [
"wifi"
],
"network_is_expensive": false,
"network_is_constrained": false,
"wifi_ssid": [ "wifi_ssid": [
"My WIFI" "My WIFI"
], ],
@@ -151,15 +135,19 @@ icon: material/new-box
"outbound": [ "outbound": [
"direct" "direct"
], ],
"action": "route", "server": "local",
"server": "local" "disable_cache": false,
"rewrite_ttl": 100,
"client_subnet": "127.0.0.1/24"
}, },
{ {
"type": "logical", "type": "logical",
"mode": "and", "mode": "and",
"rules": [], "rules": [],
"action": "route", "server": "local",
"server": "local" "disable_cache": false,
"rewrite_ttl": 100,
"client_subnet": "127.0.0.1/24"
} }
] ]
} }
@@ -230,7 +218,7 @@ Match domain using regular expression.
!!! failure "Deprecated in sing-box 1.8.0" !!! failure "Deprecated in sing-box 1.8.0"
Geosite is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geosite-to-rule-sets). Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets).
Match geosite. Match geosite.
@@ -238,7 +226,7 @@ Match geosite.
!!! failure "Deprecated in sing-box 1.8.0" !!! failure "Deprecated in sing-box 1.8.0"
GeoIP is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geoip-to-rule-sets). GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets).
Match source geoip. Match source geoip.
@@ -318,39 +306,6 @@ Match user id.
Match Clash mode. Match Clash mode.
#### network_type
!!! question "Since sing-box 1.11.0"
!!! quote ""
Only supported in graphical clients on Android and Apple platforms.
Match network type.
Available values: `wifi`, `cellular`, `ethernet` and `other`.
#### network_is_expensive
!!! question "Since sing-box 1.11.0"
!!! quote ""
Only supported in graphical clients on Android and Apple platforms.
Match if network is considered Metered (on Android) or considered expensive,
such as Cellular or a Personal Hotspot (on Apple platforms).
#### network_is_constrained
!!! question "Since sing-box 1.11.0"
!!! quote ""
Only supported in graphical clients on Apple platforms.
Match if network is in Low Data Mode.
#### wifi_ssid #### wifi_ssid
!!! quote "" !!! quote ""
@@ -399,35 +354,29 @@ Match outbound.
`any` can be used as a value to match any outbound. `any` can be used as a value to match any outbound.
#### action #### server
==Required== ==Required==
See [DNS Rule Actions](../rule_action/) for details. Tag of the target dns server.
#### server
!!! failure "Deprecated in sing-box 1.11.0"
Moved to [DNS Rule Action](../rule_action#route).
#### disable_cache #### disable_cache
!!! failure "Deprecated in sing-box 1.11.0" Disable cache and save cache in this query.
Moved to [DNS Rule Action](../rule_action#route).
#### rewrite_ttl #### rewrite_ttl
!!! failure "Deprecated in sing-box 1.11.0" Rewrite TTL in DNS responses.
Moved to [DNS Rule Action](../rule_action#route).
#### client_subnet #### client_subnet
!!! failure "Deprecated in sing-box 1.11.0" !!! question "Since sing-box 1.9.0"
Moved to [DNS Rule Action](../rule_action#route). Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default.
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
Will overrides `dns.client_subnet` and `servers.[].client_subnet`.
### Address Filter Fields ### Address Filter Fields

View File

@@ -2,17 +2,6 @@
icon: material/new-box icon: material/new-box
--- ---
!!! quote "sing-box 1.11.0 中的更改"
:material-plus: [action](#action)
:material-alert: [server](#server)
:material-alert: [disable_cache](#disable_cache)
:material-alert: [rewrite_ttl](#rewrite_ttl)
:material-alert: [client_subnet](#client_subnet)
:material-plus: [network_type](#network_type)
:material-plus: [network_is_expensive](#network_is_expensive)
:material-plus: [network_is_constrained](#network_is_constrained)
!!! quote "sing-box 1.10.0 中的更改" !!! quote "sing-box 1.10.0 中的更改"
:material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
@@ -128,11 +117,6 @@ icon: material/new-box
1000 1000
], ],
"clash_mode": "direct", "clash_mode": "direct",
"network_type": [
"wifi"
],
"network_is_expensive": false,
"network_is_constrained": false,
"wifi_ssid": [ "wifi_ssid": [
"My WIFI" "My WIFI"
], ],
@@ -151,15 +135,17 @@ icon: material/new-box
"outbound": [ "outbound": [
"direct" "direct"
], ],
"action": "route", "server": "local",
"server": "local" "disable_cache": false,
"client_subnet": "127.0.0.1/24"
}, },
{ {
"type": "logical", "type": "logical",
"mode": "and", "mode": "and",
"rules": [], "rules": [],
"action": "route", "server": "local",
"server": "local" "disable_cache": false,
"client_subnet": "127.0.0.1/24"
} }
] ]
} }
@@ -318,39 +304,6 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
匹配 Clash 模式。 匹配 Clash 模式。
#### network_type
!!! question "自 sing-box 1.11.0 起"
!!! quote ""
仅在 Android 与 Apple 平台图形客户端中支持。
匹配网络类型。
Available values: `wifi`, `cellular`, `ethernet` and `other`.
#### network_is_expensive
!!! question "自 sing-box 1.11.0 起"
!!! quote ""
仅在 Android 与 Apple 平台图形客户端中支持。
匹配如果网络被视为计费 (在 Android) 或被视为昂贵,
像蜂窝网络或个人热点 (在 Apple 平台)。
#### network_is_constrained
!!! question "自 sing-box 1.11.0 起"
!!! quote ""
仅在 Apple 平台图形客户端中支持。
匹配如果网络在低数据模式下。
#### wifi_ssid #### wifi_ssid
!!! quote "" !!! quote ""
@@ -399,35 +352,29 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
`any` 可作为值用于匹配任意出站。 `any` 可作为值用于匹配任意出站。
#### action #### server
==必填== ==必填==
参阅 [规则动作](../rule_action/) 目标 DNS 服务器的标签
#### server
!!! failure "已在 sing-box 1.11.0 废弃"
已移动到 [DNS 规则动作](../rule_action#route).
#### disable_cache #### disable_cache
!!! failure "已在 sing-box 1.11.0 废弃" 在此查询中禁用缓存。
已移动到 [DNS 规则动作](../rule_action#route).
#### rewrite_ttl #### rewrite_ttl
!!! failure "已在 sing-box 1.11.0 废弃" 重写 DNS 回应中的 TTL。
已移动到 [DNS 规则动作](../rule_action#route).
#### client_subnet #### client_subnet
!!! failure "已在 sing-box 1.11.0 废弃" !!! question "自 sing-box 1.9.0 "
已移动到 [DNS 规则动作](../rule_action#route). 默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。
如果值是 IP 地址而不是前缀,则会自动附加 `/32``/128`
将覆盖 `dns.client_subnet``servers.[].client_subnet`
### 地址筛选字段 ### 地址筛选字段
@@ -473,12 +420,8 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
#### mode #### mode
==必填==
`and``or` `and``or`
#### rules #### rules
==必填==
包括的规则。 包括的规则。

View File

@@ -1,79 +0,0 @@
---
icon: material/new-box
---
# DNS Rule Action
!!! question "Since sing-box 1.11.0"
### route
```json
{
"action": "route", // default
"server": "",
"disable_cache": false,
"rewrite_ttl": 0,
"client_subnet": null
}
```
`route` inherits the classic rule behavior of routing DNS requests to the specified server.
#### server
==Required==
Tag of target server.
#### disable_cache
Disable cache and save cache in this query.
#### rewrite_ttl
Rewrite TTL in DNS responses.
#### client_subnet
Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default.
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
Will overrides `dns.client_subnet` and `servers.[].client_subnet`.
### route-options
```json
{
"action": "route-options",
"disable_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
```
`route-options` set options for routing.
### reject
```json
{
"action": "reject",
"method": "default", // default
"no_drop": false
}
```
`reject` reject DNS requests.
#### method
- `default`: Reply with NXDOMAIN.
- `drop`: Drop the request.
#### no_drop
If not enabled, `method` will be temporarily overwritten to `drop` after 50 triggers in 30s.
Not available when `method` is set to drop.

View File

@@ -1,81 +0,0 @@
---
icon: material/new-box
---
# DNS 规则动作
!!! question "自 sing-box 1.11.0 起"
### route
```json
{
"action": "route", // 默认
"server": "",
// 兼容性
"disable_cache": false,
"rewrite_ttl": 0,
"client_subnet": null
}
```
`route` 继承了将 DNS 请求 路由到指定服务器的经典规则动作。
#### server
==必填==
目标 DNS 服务器的标签。
#### disable_cache
在此查询中禁用缓存。
#### rewrite_ttl
重写 DNS 回应中的 TTL。
#### client_subnet
默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。
如果值是 IP 地址而不是前缀,则会自动附加 `/32``/128`
将覆盖 `dns.client_subnet``servers.[].client_subnet`
### route-options
```json
{
"action": "route-options",
"disable_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
```
`route-options` 为路由设置选项。
### reject
```json
{
"action": "reject",
"method": "default", // default
"no_drop": false
}
```
`reject` 拒绝 DNS 请求。
#### method
- `default`: 返回 NXDOMAIN。
- `drop`: 丢弃请求。
#### no_drop
如果未启用,则 30 秒内触发 50 次后,`method` 将被暂时覆盖为 `drop`
`method` 设为 `drop` 时不可用。

View File

@@ -1,3 +1,7 @@
---
icon: material/new-box
---
!!! quote "Changes in sing-box 1.9.0" !!! quote "Changes in sing-box 1.9.0"
:material-plus: [client_subnet](#client_subnet) :material-plus: [client_subnet](#client_subnet)

View File

@@ -1,3 +1,7 @@
---
icon: material/new-box
---
!!! quote "sing-box 1.9.0 中的更改" !!! quote "sing-box 1.9.0 中的更改"
:material-plus: [client_subnet](#client_subnet) :material-plus: [client_subnet](#client_subnet)

View File

@@ -1,3 +1,7 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.8.0" !!! question "Since sing-box 1.8.0"
!!! quote "Changes in sing-box 1.9.0" !!! quote "Changes in sing-box 1.9.0"

View File

@@ -1,3 +1,7 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.8.0 起" !!! question "自 sing-box 1.8.0 起"
!!! quote "sing-box 1.9.0 中的更改" !!! quote "sing-box 1.9.0 中的更改"

View File

@@ -1,14 +1,8 @@
--- `block` outbound closes all incoming requests.
icon: material/delete-clock
---
!!! failure "Deprecated in sing-box 1.11.0"
Legacy special outbounds are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-special-outbounds-to-rule-actions).
### Structure ### Structure
```json F ```json
{ {
"type": "block", "type": "block",
"tag": "block" "tag": "block"

View File

@@ -1,11 +1,3 @@
---
icon: material/delete-clock
---
!!! failure "已在 sing-box 1.11.0 废弃"
旧的特殊出站已被弃用,且将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-special-outbounds-to-rule-actions).
`block` 出站关闭所有传入请求。 `block` 出站关闭所有传入请求。
### 结构 ### 结构

View File

@@ -1,11 +1,3 @@
---
icon: material/delete-clock
---
!!! failure "Deprecated in sing-box 1.11.0"
Legacy special outbounds are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-special-outbounds-to-rule-actions).
`dns` outbound is a internal DNS server. `dns` outbound is a internal DNS server.
### Structure ### Structure

View File

@@ -1,11 +1,3 @@
---
icon: material/delete-clock
---
!!! failure "已在 sing-box 1.11.0 废弃"
旧的特殊出站已被弃用,且将在 sing-box 1.13.0 中被移除, 参阅 [迁移指南](/migration/#migrate-legacy-special-outbounds-to-rule-actions).
`dns` 出站是一个内部 DNS 服务器。 `dns` 出站是一个内部 DNS 服务器。
### 结构 ### 结构

View File

@@ -4,7 +4,7 @@ icon: material/delete-clock
!!! failure "Deprecated in sing-box 1.8.0" !!! failure "Deprecated in sing-box 1.8.0"
GeoIP is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geoip-to-rule-sets). GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets).
### Structure ### Structure

View File

@@ -4,7 +4,7 @@ icon: material/delete-clock
!!! failure "已在 sing-box 1.8.0 废弃" !!! failure "已在 sing-box 1.8.0 废弃"
GeoIP 已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geoip)。 GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。
### 结构 ### 结构

View File

@@ -4,7 +4,7 @@ icon: material/delete-clock
!!! failure "Deprecated in sing-box 1.8.0" !!! failure "Deprecated in sing-box 1.8.0"
Geosite is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geosite-to-rule-sets). Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets).
### Structure ### Structure

View File

@@ -4,7 +4,7 @@ icon: material/delete-clock
!!! failure "已在 sing-box 1.8.0 废弃" !!! failure "已在 sing-box 1.8.0 废弃"
Geosite 已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geosite)。 Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geosite)。
### 结构 ### 结构

View File

@@ -1,15 +1,7 @@
--- ---
icon: material/new-box icon: material/alert-decagram
--- ---
!!! quote "Changes in sing-box 1.11.0"
:material-plus: [action](#action)
:material-alert: [outbound](#outbound)
:material-plus: [network_type](#network_type)
:material-plus: [network_is_expensive](#network_is_expensive)
:material-plus: [network_is_constrained](#network_is_constrained)
!!! quote "Changes in sing-box 1.10.0" !!! quote "Changes in sing-box 1.10.0"
:material-plus: [client](#client) :material-plus: [client](#client)
@@ -123,11 +115,6 @@ icon: material/new-box
1000 1000
], ],
"clash_mode": "direct", "clash_mode": "direct",
"network_type": [
"wifi"
],
"network_is_expensive": false,
"network_is_constrained": false,
"wifi_ssid": [ "wifi_ssid": [
"My WIFI" "My WIFI"
], ],
@@ -142,7 +129,6 @@ icon: material/new-box
"rule_set_ipcidr_match_source": false, "rule_set_ipcidr_match_source": false,
"rule_set_ip_cidr_match_source": false, "rule_set_ip_cidr_match_source": false,
"invert": false, "invert": false,
"action": "route",
"outbound": "direct" "outbound": "direct"
}, },
{ {
@@ -150,7 +136,6 @@ icon: material/new-box
"mode": "and", "mode": "and",
"rules": [], "rules": [],
"invert": false, "invert": false,
"action": "route",
"outbound": "direct" "outbound": "direct"
} }
] ]
@@ -224,7 +209,7 @@ Match domain using regular expression.
!!! failure "Deprecated in sing-box 1.8.0" !!! failure "Deprecated in sing-box 1.8.0"
Geosite is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geosite-to-rule-sets). Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets).
Match geosite. Match geosite.
@@ -232,7 +217,7 @@ Match geosite.
!!! failure "Deprecated in sing-box 1.8.0" !!! failure "Deprecated in sing-box 1.8.0"
GeoIP is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geoip-to-rule-sets). GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets).
Match source geoip. Match source geoip.
@@ -240,7 +225,7 @@ Match source geoip.
!!! failure "Deprecated in sing-box 1.8.0" !!! failure "Deprecated in sing-box 1.8.0"
GeoIP is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geoip-to-rule-sets). GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets).
Match geoip. Match geoip.
@@ -330,39 +315,6 @@ Match user id.
Match Clash mode. Match Clash mode.
#### network_type
!!! question "Since sing-box 1.11.0"
!!! quote ""
Only supported in graphical clients on Android and Apple platforms.
Match network type.
Available values: `wifi`, `cellular`, `ethernet` and `other`.
#### network_is_expensive
!!! question "Since sing-box 1.11.0"
!!! quote ""
Only supported in graphical clients on Android and Apple platforms.
Match if network is considered Metered (on Android) or considered expensive,
such as Cellular or a Personal Hotspot (on Apple platforms).
#### network_is_constrained
!!! question "Since sing-box 1.11.0"
!!! quote ""
Only supported in graphical clients on Apple platforms.
Match if network is in Low Data Mode.
#### wifi_ssid #### wifi_ssid
!!! quote "" !!! quote ""
@@ -405,17 +357,11 @@ Make `ip_cidr` in rule-sets match the source IP.
Invert match result. Invert match result.
#### action #### outbound
==Required== ==Required==
See [Rule Actions](../rule_action/) for details. Tag of the target outbound.
#### outbound
!!! failure "Deprecated in sing-box 1.11.0"
Moved to [Rule Action](../rule_action#route).
### Logical Fields ### Logical Fields

View File

@@ -1,21 +1,14 @@
--- ---
icon: material/new-box icon: material/alert-decagram
--- ---
!!! quote "sing-box 1.11.0 中的更改"
:material-plus: [action](#action)
:material-alert: [outbound](#outbound)
:material-plus: [network_type](#network_type)
:material-plus: [network_is_expensive](#network_is_expensive)
:material-plus: [network_is_constrained](#network_is_constrained)
!!! quote "sing-box 1.10.0 中的更改" !!! quote "sing-box 1.10.0 中的更改"
:material-plus: [client](#client) :material-plus: [client](#client)
:material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
:material-plus: [process_path_regex](#process_path_regex) :material-plus: [process_path_regex](#process_path_regex)
!!! quote "sing-box 1.8.0 中的更改" !!! quote "sing-box 1.8.0 中的更改"
:material-plus: [rule_set](#rule_set) :material-plus: [rule_set](#rule_set)
@@ -120,11 +113,6 @@ icon: material/new-box
1000 1000
], ],
"clash_mode": "direct", "clash_mode": "direct",
"network_type": [
"wifi"
],
"network_is_expensive": false,
"network_is_constrained": false,
"wifi_ssid": [ "wifi_ssid": [
"My WIFI" "My WIFI"
], ],
@@ -139,7 +127,6 @@ icon: material/new-box
"rule_set_ipcidr_match_source": false, "rule_set_ipcidr_match_source": false,
"rule_set_ip_cidr_match_source": false, "rule_set_ip_cidr_match_source": false,
"invert": false, "invert": false,
"action": "route",
"outbound": "direct" "outbound": "direct"
}, },
{ {
@@ -147,7 +134,6 @@ icon: material/new-box
"mode": "and", "mode": "and",
"rules": [], "rules": [],
"invert": false, "invert": false,
"action": "route",
"outbound": "direct" "outbound": "direct"
} }
] ]
@@ -160,7 +146,7 @@ icon: material/new-box
当内容只有一项时,可以忽略 JSON 数组 [] 标签。 当内容只有一项时,可以忽略 JSON 数组 [] 标签。
### 默认字段 ### Default Fields
!!! note "" !!! note ""
@@ -327,39 +313,6 @@ icon: material/new-box
匹配 Clash 模式。 匹配 Clash 模式。
#### network_type
!!! question "自 sing-box 1.11.0 起"
!!! quote ""
仅在 Android 与 Apple 平台图形客户端中支持。
匹配网络类型。
Available values: `wifi`, `cellular`, `ethernet` and `other`.
#### network_is_expensive
!!! question "自 sing-box 1.11.0 起"
!!! quote ""
仅在 Android 与 Apple 平台图形客户端中支持。
匹配如果网络被视为计费 (在 Android) 或被视为昂贵,
像蜂窝网络或个人热点 (在 Apple 平台)。
#### network_is_constrained
!!! question "自 sing-box 1.11.0 起"
!!! quote ""
仅在 Apple 平台图形客户端中支持。
匹配如果网络在低数据模式下。
#### wifi_ssid #### wifi_ssid
!!! quote "" !!! quote ""
@@ -402,17 +355,11 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
反选匹配结果。 反选匹配结果。
#### action #### outbound
==必填== ==必填==
参阅 [规则动作](../rule_action/) 目标出站的标签
#### outbound
!!! failure "已在 sing-box 1.11.0 废弃"
已移动到 [规则动作](../rule_action#route).
### 逻辑字段 ### 逻辑字段

View File

@@ -1,141 +0,0 @@
---
icon: material/new-box
---
# Rule Action
!!! question "Since sing-box 1.11.0"
## Final actions
### route
```json
{
"action": "route", // default
"outbound": "",
"udp_disable_domain_unmapping": false,
"udp_connect": false
}
```
`route` inherits the classic rule behavior of routing connection to the specified outbound.
#### outbound
==Required==
Tag of target outbound.
#### udp_disable_domain_unmapping
If enabled, for UDP proxy requests addressed to a domain,
the original packet address will be sent in the response instead of the mapped domain.
This option is used for compatibility with clients that
do not support receiving UDP packets with domain addresses, such as Surge.
#### udp_connect
If enabled, attempts to connect UDP connection to the destination instead of listen.
### route-options
```json
{
"action": "route-options",
"udp_disable_domain_unmapping": false,
"udp_connect": false
}
```
`route-options` set options for routing.
### reject
```json
{
"action": "reject",
"method": "default", // default
"no_drop": false
}
```
`reject` reject connections
The specified method is used for reject tun connections if `sniff` action has not been performed yet.
For non-tun connections and already established connections, will just be closed.
#### method
- `default`: Reply with TCP RST for TCP connections, and ICMP port unreachable for UDP packets.
- `drop`: Drop packets.
#### no_drop
If not enabled, `method` will be temporarily overwritten to `drop` after 50 triggers in 30s.
Not available when `method` is set to drop.
### hijack-dns
```json
{
"action": "hijack-dns"
}
```
`hijack-dns` hijack DNS requests to the sing-box DNS module.
## Non-final actions
### sniff
```json
{
"action": "sniff",
"sniffer": [],
"timeout": ""
}
```
`sniff` performs protocol sniffing on connections.
For deprecated `inbound.sniff` options, it is considered to `sniff()` performed before routing.
#### sniffer
Enabled sniffers.
All sniffers enabled by default.
Available protocol values an be found on in [Protocol Sniff](../sniff/)
#### timeout
Timeout for sniffing.
`300ms` is used by default.
### resolve
```json
{
"action": "resolve",
"strategy": "",
"server": ""
}
```
`resolve` resolve request destination from domain to IP addresses.
#### strategy
DNS resolution strategy, available values are: `prefer_ipv4`, `prefer_ipv6`, `ipv4_only`, `ipv6_only`.
`dns.strategy` will be used by default.
#### server
Specifies DNS server tag to use instead of selecting through DNS routing.

View File

@@ -1,139 +0,0 @@
---
icon: material/new-box
---
# 规则动作
!!! question "自 sing-box 1.11.0 起"
## 最终动作
### route
```json
{
"action": "route", // 默认
"outbound": "",
"udp_disable_domain_unmapping": false,
"udp_connect": false
}
```
`route` 继承了将连接路由到指定出站的经典规则动作。
#### outbound
==必填==
目标出站的标签。
#### udp_disable_domain_unmapping
如果启用,对于地址为域的 UDP 代理请求,将在响应中发送原始包地址而不是映射的域。
此选项用于兼容不支持接收带有域地址的 UDP 包的客户端,如 Surge。
#### udp_connect
如果启用,将尝试将 UDP 连接 connect 到目标而不是 listen。
### route-options
```json
{
"action": "route-options",
"udp_disable_domain_unmapping": false,
"udp_connect": false
}
```
`route-options` 为路由设置选项。
### reject
```json
{
"action": "reject",
"method": "default", // 默认
"no_drop": false
}
```
`reject` 拒绝连接。
如果尚未执行 `sniff` 操作,则将使用指定方法拒绝 tun 连接。
对于非 tun 连接和已建立的连接,将直接关闭。
#### method
- `default`: 对于 TCP 连接回复 RST对于 UDP 包回复 ICMP 端口不可达。
- `drop`: 丢弃数据包。
#### no_drop
如果未启用,则 30 秒内触发 50 次后,`method` 将被暂时覆盖为 `drop`
`method` 设为 `drop` 时不可用。
### hijack-dns
```json
{
"action": "hijack-dns"
}
```
`hijack-dns` 劫持 DNS 请求至 sing-box DNS 模块。
## 非最终动作
### sniff
```json
{
"action": "sniff",
"sniffer": [],
"timeout": ""
}
```
`sniff` 对连接执行协议嗅探。
对于已弃用的 `inbound.sniff` 选项,被视为在路由之前执行的 `sniff`
#### sniffer
启用的探测器。
默认启用所有探测器。
可用的协议值可以在 [协议嗅探](../sniff/) 中找到。
#### timeout
探测超时时间。
默认使用 300ms。
### resolve
```json
{
"action": "resolve",
"strategy": "",
"server": ""
}
```
`resolve` 将请求的目标从域名解析为 IP 地址。
#### strategy
DNS 解析策略,可用值有:`prefer_ipv4``prefer_ipv6``ipv4_only``ipv6_only`
默认使用 `dns.strategy`
#### server
指定要使用的 DNS 服务器的标签,而不是通过 DNS 路由进行选择。

View File

@@ -1,70 +0,0 @@
---
icon: material/new-box
---
# AdGuard DNS Filter
!!! question "自 sing-box 1.10.0 起"
sing-box 支持其他项目的一些规则集格式,这些格式无法完全转换为 sing-box
目前只有 AdGuard DNS Filter。
这些格式不直接作为源格式支持,
而是需要将它们转换为二进制规则集。
## 转换
使用 `sing-box rule-set convert --type adguard [--output <file-name>.srs] <file-name>.txt` 以转换为二进制规则集。
## 性能
AdGuard 将所有规则保存在内存中并按顺序匹配,
而 sing-box 选择高性能和较小的内存使用量。
作为权衡,您无法知道匹配了哪个规则项。
## 兼容性
[AdGuardSDNSFilter](https://github.com/AdguardTeam/AdGuardSDNSFilter)
中的几乎所有规则以及 [adguard-filter-list](https://github.com/ppfeufer/adguard-filter-list)
中列出的规则集中的规则均受支持。
## 支持的格式
### AdGuard Filter
#### 基本规则语法
| 语法 | 支持 |
|--------|------------------|
| `@@` | :material-check: |
| `\|\|` | :material-check: |
| `\|` | :material-check: |
| `^` | :material-check: |
| `*` | :material-check: |
#### 主机语法
| 语法 | 示例 | 支持 |
|-------------|--------------------------|--------------------------|
| Scheme | `https://` | :material-alert: Ignored |
| Domain Host | `example.org` | :material-check: |
| IP Host | `1.1.1.1`, `10.0.0.` | :material-close: |
| Regexp | `/regexp/` | :material-check: |
| Port | `example.org:80` | :material-close: |
| Path | `example.org/path/ad.js` | :material-close: |
#### 描述符语法
| 描述符 | 支持 |
|-----------------------|--------------------------|
| `$important` | :material-check: |
| `$dnsrewrite=0.0.0.0` | :material-alert: Ignored |
| 任何其他描述符 | :material-close: |
### Hosts
只有 IP 地址为 `0.0.0.0` 的条目将被接受。
### 简易
当所有行都是有效域时,它们被视为简单的逐行域规则, 与 hosts 一样,只匹配完全相同的域。

View File

@@ -1,13 +1,3 @@
---
icon: material/new-box
---
!!! quote "Changes in sing-box 1.11.0"
:material-plus: [network_type](#network_type)
:material-plus: [network_is_expensive](#network_is_expensive)
:material-plus: [network_is_constrained](#network_is_constrained)
### Structure ### Structure
!!! question "Since sing-box 1.8.0" !!! question "Since sing-box 1.8.0"
@@ -73,11 +63,6 @@ icon: material/new-box
"package_name": [ "package_name": [
"com.termux" "com.termux"
], ],
"network_type": [
"wifi"
],
"network_is_expensive": false,
"network_is_constrained": false,
"wifi_ssid": [ "wifi_ssid": [
"My WIFI" "My WIFI"
], ],
@@ -192,39 +177,6 @@ Match process path using regular expression.
Match android package name. Match android package name.
#### network_type
!!! question "Since sing-box 1.11.0"
!!! quote ""
Only supported in graphical clients on Android and Apple platforms.
Match network type.
Available values: `wifi`, `cellular`, `ethernet` and `other`.
#### network_is_expensive
!!! question "Since sing-box 1.11.0"
!!! quote ""
Only supported in graphical clients on Android and Apple platforms.
Match if network is considered Metered (on Android) or considered expensive,
such as Cellular or a Personal Hotspot (on Apple platforms).
#### network_is_constrained
!!! question "Since sing-box 1.11.0"
!!! quote ""
Only supported in graphical clients on Apple platforms.
Match if network is in Low Data Mode.
#### wifi_ssid #### wifi_ssid
!!! quote "" !!! quote ""

View File

@@ -1,258 +0,0 @@
---
icon: material/new-box
---
!!! quote "sing-box 1.11.0 中的更改"
:material-plus: [network_type](#network_type)
:material-alert: [network_is_expensive](#network_is_expensive)
:material-alert: [network_is_constrained](#network_is_constrained)
### 结构
!!! question "自 sing-box 1.8.0 起"
```json
{
"rules": [
{
"query_type": [
"A",
"HTTPS",
32768
],
"network": [
"tcp"
],
"domain": [
"test.com"
],
"domain_suffix": [
".cn"
],
"domain_keyword": [
"test"
],
"domain_regex": [
"^stun\\..+"
],
"source_ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"source_port": [
12345
],
"source_port_range": [
"1000:2000",
":3000",
"4000:"
],
"port": [
80,
443
],
"port_range": [
"1000:2000",
":3000",
"4000:"
],
"process_name": [
"curl"
],
"process_path": [
"/usr/bin/curl"
],
"process_path_regex": [
"^/usr/bin/.+"
],
"package_name": [
"com.termux"
],
"network_type": [
"wifi"
],
"network_is_expensive": false,
"network_is_constrained": false,
"wifi_ssid": [
"My WIFI"
],
"wifi_bssid": [
"00:00:00:00:00:00"
],
"invert": false
},
{
"type": "logical",
"mode": "and",
"rules": [],
"invert": false
}
]
}
```
!!! note ""
当内容只有一项时,可以忽略 JSON 数组 [] 标签。
### Default Fields
!!! note ""
默认规则使用以下匹配逻辑:
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `ip_cidr`) &&
(`port` || `port_range`) &&
(`source_port` || `source_port_range`) &&
`other fields`
#### query_type
DNS 查询类型。值可以为整数或者类型名称字符串。
#### network
`tcp``udp`
#### domain
匹配完整域名。
#### domain_suffix
匹配域名后缀。
#### domain_keyword
匹配域名关键字。
#### domain_regex
匹配域名正则表达式。
#### source_ip_cidr
匹配源 IP CIDR。
#### ip_cidr
匹配 IP CIDR。
#### source_port
匹配源端口。
#### source_port_range
匹配源端口范围。
#### port
匹配端口。
#### port_range
匹配端口范围。
#### process_name
!!! quote ""
仅支持 Linux、Windows 和 macOS。
匹配进程名称。
#### process_path
!!! quote ""
仅支持 Linux、Windows 和 macOS.
匹配进程路径。
#### process_path_regex
!!! question "自 sing-box 1.10.0 起"
!!! quote ""
仅支持 Linux、Windows 和 macOS.
使用正则表达式匹配进程路径。
#### package_name
匹配 Android 应用包名。
#### network_type
!!! question "自 sing-box 1.11.0 起"
!!! quote ""
仅在 Android 与 Apple 平台图形客户端中支持。
匹配网络类型。
Available values: `wifi`, `cellular`, `ethernet` and `other`.
#### network_is_expensive
!!! question "自 sing-box 1.11.0 起"
!!! quote ""
仅在 Android 与 Apple 平台图形客户端中支持。
匹配如果网络被视为计费 (在 Android) 或被视为昂贵,
像蜂窝网络或个人热点 (在 Apple 平台)。
#### network_is_constrained
!!! question "自 sing-box 1.11.0 起"
!!! quote ""
仅在 Apple 平台图形客户端中支持。
匹配如果网络在低数据模式下。
#### wifi_ssid
!!! quote ""
仅在 Android 与 Apple 平台图形客户端中支持。
匹配 WiFi SSID。
#### wifi_bssid
!!! quote ""
仅在 Android 与 Apple 平台图形客户端中支持。
#### invert
反选匹配结果。
### 逻辑字段
#### type
`logical`
#### mode
==必填==
`and``or`
#### rules
==必填==
包括的规则。

View File

@@ -74,7 +74,7 @@ Tag of rule-set.
==Required== ==Required==
List of [Headless Rule](../headless-rule/). List of [Headless Rule](./headless-rule.md/).
### Local or Remote Fields ### Local or Remote Fields

View File

@@ -1,117 +0,0 @@
---
icon: material/new-box
---
!!! quote "sing-box 1.10.0 中的更改"
:material-plus: `type: inline`
# 规则集
!!! question "自 sing-box 1.8.0 起"
### 结构
=== "内联"
!!! question "自 sing-box 1.10.0 起"
```json
{
"type": "inline", // 可选
"tag": "",
"rules": []
}
```
=== "本地文件"
```json
{
"type": "local",
"tag": "",
"format": "source", // or binary
"path": ""
}
```
=== "远程文件"
!!! info ""
远程规则集将被缓存如果 `experimental.cache_file.enabled` 已启用。
```json
{
"type": "remote",
"tag": "",
"format": "source", // or binary
"url": "",
"download_detour": "", // 可选
"update_interval": "" // 可选
}
```
### 字段
#### type
==必填==
规则集类型, `local` 或 `remote`。
#### tag
==必填==
规则集的标签。
### 内联字段
!!! question "自 sing-box 1.10.0 起"
#### rules
==必填==
一组 [无头规则](../headless-rule/).
### 本地或远程字段
#### format
==必填==
规则集格式, `source` 或 `binary`。
### 本地字段
#### path
==必填==
!!! note ""
自 sing-box 1.10.0 起,文件更改时将自动重新加载。
规则集的文件路径。
### 远程字段
#### url
==必填==
规则集的下载 URL。
#### download_detour
用于下载规则集的出站的标签。
如果为空,将使用默认出站。
#### update_interval
规则集的更新间隔。
默认使用 `1d`。

View File

@@ -4,10 +4,6 @@ icon: material/new-box
# Source Format # Source Format
!!! quote "Changes in sing-box 1.11.0"
:material-plus: version `3`
!!! quote "Changes in sing-box 1.10.0" !!! quote "Changes in sing-box 1.10.0"
:material-plus: version `2` :material-plus: version `2`
@@ -18,7 +14,7 @@ icon: material/new-box
```json ```json
{ {
"version": 3, "version": 2,
"rules": [] "rules": []
} }
``` ```
@@ -33,14 +29,19 @@ Use `sing-box rule-set compile [--output <file-name>.srs] <file-name>.json` to c
==Required== ==Required==
Version of rule-set. Version of rule-set, one of `1` or `2`.
* 1: sing-box 1.8.0: Initial rule-set version. * 1: Initial rule-set version, since sing-box 1.8.0.
* 2: sing-box 1.10.0: Optimized memory usages of `domain_suffix` rules in binary rule-sets. * 2: Optimized memory usages of `domain_suffix` rules.
* 3: sing-box 1.11.0: Added `network_type`, `network_is_expensive` and `network_is_constrainted` rule items.
The new rule-set version `2` does not make any changes to the format, only affecting `binary` rule-sets compiled by command `rule-set compile`
Since 1.10.0, the optimization is always applied to `source` rule-sets even if version is set to `1`.
It is recommended to upgrade to `2` after sing-box 1.10.0 becomes a stable version.
#### rules #### rules
==Required== ==Required==
List of [Headless Rule](../headless-rule/). List of [Headless Rule](./headless-rule.md/).

View File

@@ -1,46 +0,0 @@
---
icon: material/new-box
---
# 源文件格式
!!! quote "sing-box 1.11.0 中的更改"
:material-plus: version `3`
!!! quote "sing-box 1.10.0 中的更改"
:material-plus: version `2`
!!! question "自 sing-box 1.8.0 起"
### 结构
```json
{
"version": 3,
"rules": []
}
```
### 编译
使用 `sing-box rule-set compile [--output <file-name>.srs] <file-name>.json` 以编译源文件为二进制规则集。
### 字段
#### version
==必填==
规则集版本。
* 1: sing-box 1.8.0: 初始规则集版本。
* 2: sing-box 1.10.0: 优化了二进制规则集中 `domain_suffix` 规则的内存使用。
* 3: sing-box 1.11.0: 添加了 `network_type``network_is_expensive``network_is_constrainted` 规则项。
#### rules
==必填==
一组 [无头规则](../headless-rule/).

View File

@@ -1,15 +1,3 @@
---
icon: material/delete-clock
---
!!! quote "Changes in sing-box 1.11.0"
:material-delete-clock: [sniff](#sniff)
:material-delete-clock: [sniff_override_destination](#sniff_override_destination)
:material-delete-clock: [sniff_timeout](#sniff_timeout)
:material-delete-clock: [domain_strategy](#domain_strategy)
:material-delete-clock: [udp_disable_domain_unmapping](#udp_disable_domain_unmapping)
### Structure ### Structure
```json ```json
@@ -80,40 +68,24 @@ Requires target inbound support, see [Injectable](/configuration/inbound/#fields
#### sniff #### sniff
!!! failure "Deprecated in sing-box 1.11.0"
Inbound fields are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
Enable sniffing. Enable sniffing.
See [Protocol Sniff](/configuration/route/sniff/) for details. See [Protocol Sniff](/configuration/route/sniff/) for details.
#### sniff_override_destination #### sniff_override_destination
!!! failure "Deprecated in sing-box 1.11.0"
Inbound fields are deprecated and will be removed in sing-box 1.13.0.
Override the connection destination address with the sniffed domain. Override the connection destination address with the sniffed domain.
If the domain name is invalid (like tor), this will not work. If the domain name is invalid (like tor), this will not work.
#### sniff_timeout #### sniff_timeout
!!! failure "Deprecated in sing-box 1.11.0"
Inbound fields are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
Timeout for sniffing. Timeout for sniffing.
`300ms` is used by default. 300ms is used by default.
#### domain_strategy #### domain_strategy
!!! failure "Deprecated in sing-box 1.11.0"
Inbound fields are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
If set, the requested domain name will be resolved to IP before routing. If set, the requested domain name will be resolved to IP before routing.
@@ -122,10 +94,6 @@ If `sniff_override_destination` is in effect, its value will be taken as a fallb
#### udp_disable_domain_unmapping #### udp_disable_domain_unmapping
!!! failure "Deprecated in sing-box 1.11.0"
Inbound fields are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
If enabled, for UDP proxy requests addressed to a domain, If enabled, for UDP proxy requests addressed to a domain,
the original packet address will be sent in the response instead of the mapped domain. the original packet address will be sent in the response instead of the mapped domain.

View File

@@ -1,15 +1,3 @@
---
icon: material/delete-clock
---
!!! quote "sing-box 1.11.0 中的更改"
:material-delete-clock: [sniff](#sniff)
:material-delete-clock: [sniff_override_destination](#sniff_override_destination)
:material-delete-clock: [sniff_timeout](#sniff_timeout)
:material-delete-clock: [domain_strategy](#domain_strategy)
:material-delete-clock: [udp_disable_domain_unmapping](#udp_disable_domain_unmapping)
### 结构 ### 结构
```json ```json
@@ -81,40 +69,24 @@ UDP NAT 过期时间,以秒为单位。
#### sniff #### sniff
!!! failure "已在 sing-box 1.11.0 废弃"
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
启用协议探测。 启用协议探测。
参阅 [协议探测](/zh/configuration/route/sniff/) 参阅 [协议探测](/zh/configuration/route/sniff/)
#### sniff_override_destination #### sniff_override_destination
!!! failure "已在 sing-box 1.11.0 废弃"
入站字段已废弃且将在 sing-box 1.12.0 中被移除。
用探测出的域名覆盖连接目标地址。 用探测出的域名覆盖连接目标地址。
如果域名无效(如 Tor将不生效。 如果域名无效(如 Tor将不生效。
#### sniff_timeout #### sniff_timeout
!!! failure "已在 sing-box 1.11.0 废弃"
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
探测超时时间。 探测超时时间。
默认使用 300ms。 默认使用 300ms。
#### domain_strategy #### domain_strategy
!!! failure "已在 sing-box 1.11.0 废弃"
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
可选值: `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only` 可选值: `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`
如果设置,请求的域名将在路由之前解析为 IP。 如果设置,请求的域名将在路由之前解析为 IP。
@@ -123,10 +95,6 @@ UDP NAT 过期时间,以秒为单位。
#### udp_disable_domain_unmapping #### udp_disable_domain_unmapping
!!! failure "已在 sing-box 1.11.0 废弃"
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
如果启用,对于地址为域的 UDP 代理请求,将在响应中发送原始包地址而不是映射的域。 如果启用,对于地址为域的 UDP 代理请求,将在响应中发送原始包地址而不是映射的域。
此选项用于兼容不支持接收带有域地址的 UDP 包的客户端,如 Surge。 此选项用于兼容不支持接收带有域地址的 UDP 包的客户端,如 Surge。

View File

@@ -4,24 +4,6 @@ icon: material/delete-alert
# Deprecated Feature List # Deprecated Feature List
## 1.11.0
#### Legacy special outbounds
Legacy special outbounds (`block` / `dns`) are deprecated
and can be replaced by rule actions,
check [Migration](../migration/#migrate-legacy-special-outbounds-to-rule-actions).
Old fields will be removed in sing-box 1.13.0.
#### Legacy inbound fields
Legacy inbound fields `inbound.<sniff/domain_strategy/...>` are deprecated
and can be replaced by rule actions,
check [Migration](../migration/#migrate-legacy-inbound-fields-to-rule-actions).
Old fields will be removed in sing-box 1.13.0.
## 1.10.0 ## 1.10.0
#### TUN address fields are merged #### TUN address fields are merged
@@ -30,7 +12,7 @@ Old fields will be removed in sing-box 1.13.0.
`inet4_route_address` and `inet6_route_address` are merged into `route_address`, `inet4_route_address` and `inet6_route_address` are merged into `route_address`,
`inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`. `inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`.
Old fields will be removed in sing-box 1.12.0. Old fields are deprecated and will be removed in sing-box 1.11.0.
#### Match source rule items are renamed #### Match source rule items are renamed
@@ -50,7 +32,7 @@ check [Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-o
#### GeoIP #### GeoIP
GeoIP is deprecated and will be removed in sing-box 1.12.0. GeoIP is deprecated and may be removed in the future.
The maxmind GeoIP National Database, as an IP classification database, The maxmind GeoIP National Database, as an IP classification database,
is not entirely suitable for traffic bypassing, is not entirely suitable for traffic bypassing,
@@ -61,7 +43,7 @@ check [Migration](/migration/#migrate-geoip-to-rule-sets).
#### Geosite #### Geosite
Geosite is deprecated and will be removed in sing-box 1.12.0. Geosite is deprecated and may be removed in the future.
Geosite, the `domain-list-community` project maintained by V2Ray as an early traffic bypassing solution, Geosite, the `domain-list-community` project maintained by V2Ray as an early traffic bypassing solution,
suffers from a number of problems, including lack of maintenance, inaccurate rules, and difficult management. suffers from a number of problems, including lack of maintenance, inaccurate rules, and difficult management.

View File

@@ -4,22 +4,6 @@ icon: material/delete-alert
# 废弃功能列表 # 废弃功能列表
## 1.11.0
#### 旧的特殊出站
旧的特殊出站(`block` / `dns`)已废弃且可以通过规则动作替代,
参阅 [迁移指南](/migration/#migrate-legacy-special-outbounds-to-rule-actions)。
旧字段将在 sing-box 1.13.0 中被移除。
#### 旧的入站字段
旧的入站字段(`inbound.<sniff/domain_strategy/...>`)已废弃且可以通过规则动作替代,
参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions)。
旧字段将在 sing-box 1.13.0 中被移除。
## 1.10.0 ## 1.10.0
#### Match source 规则项已重命名 #### Match source 规则项已重命名
@@ -33,7 +17,7 @@ icon: material/delete-alert
`inet4_route_address``inet6_route_address` 已合并为 `route_address` `inet4_route_address``inet6_route_address` 已合并为 `route_address`
`inet4_route_exclude_address``inet6_route_exclude_address` 已合并为 `route_exclude_address` `inet4_route_exclude_address``inet6_route_exclude_address` 已合并为 `route_exclude_address`
旧字段将在 sing-box 1.11.0 中被移除。 旧字段已废弃,且将在 sing-box 1.11.0 中被移除。
#### 移除对 go1.18 和 go1.19 的支持 #### 移除对 go1.18 和 go1.19 的支持
@@ -48,7 +32,7 @@ Clash API 中的 `cache_file` 及相关功能已废弃且已迁移到独立的 `
#### GeoIP #### GeoIP
GeoIP 已废弃且将在 sing-box 1.12.0 中被移除。 GeoIP 已废弃且可能在不久的将来移除。
maxmind GeoIP 国家数据库作为 IP 分类数据库,不完全适合流量绕过, maxmind GeoIP 国家数据库作为 IP 分类数据库,不完全适合流量绕过,
且现有的实现均存在内存使用大与管理困难的问题。 且现有的实现均存在内存使用大与管理困难的问题。
@@ -58,7 +42,7 @@ sing-box 1.8.0 引入了[规则集](/configuration/rule-set/)
#### Geosite #### Geosite
Geosite 已废弃且将在 sing-box 1.12.0 中被移除。 Geosite 已废弃且可能在不久的将来移除。
Geosite即由 V2Ray 维护的 domain-list-community 项目,作为早期流量绕过解决方案, Geosite即由 V2Ray 维护的 domain-list-community 项目,作为早期流量绕过解决方案,
存在着包括缺少维护、规则不准确和管理困难内的大量问题。 存在着包括缺少维护、规则不准确和管理困难内的大量问题。

View File

@@ -2,160 +2,6 @@
icon: material/arrange-bring-forward icon: material/arrange-bring-forward
--- ---
## 1.11.0
### Migrate legacy special outbounds to rule actions
Legacy special outbounds are deprecated and can be replaced by rule actions.
!!! info "References"
[Rule Action](/configuration/route/rule_action/) /
[Block](/configuration/outbound/block/) /
[DNS](/configuration/outbound/dns)
=== "Block"
=== ":material-card-remove: Deprecated"
```json
{
"outbounds": [
{
"type": "block",
"tag": "block"
}
],
"route": {
"rules": [
{
...,
"outbound": "block"
}
]
}
}
```
=== ":material-card-multiple: New"
```json
{
"route": {
"rules": [
{
...,
"action": "reject"
}
]
}
}
```
=== "DNS"
=== ":material-card-remove: Deprecated"
```json
{
"inbound": [
{
...,
"sniff": true
}
],
"outbounds": [
{
"tag": "dns",
"type": "dns"
}
],
"route": {
"rules": [
{
"protocol": "dns",
"outbound": "dns"
}
]
}
}
```
=== ":material-card-multiple: New"
```json
{
"route": {
"rules": [
{
"action": "sniff"
},
{
"protocol": "dns",
"action": "hijack-dns"
}
]
}
}
```
### Migrate legacy inbound fields to rule actions
Inbound fields are deprecated and can be replaced by rule actions.
!!! info "References"
[Listen Fields](/configuration/inbound/listen/) /
[Rule](/configuration/route/rule/) /
[Rule Action](/configuration/route/rule_action/) /
[DNS Rule](/configuration/dns/rule/) /
[DNS Rule Action](/configuration/dns/rule_action/)
=== ":material-card-remove: Deprecated"
```json
{
"inbounds": [
{
"type": "mixed",
"sniff": true,
"sniff_timeout": "1s",
"domain_strategy": "prefer_ipv4"
}
]
}
```
=== ":material-card-multiple: New"
```json
{
"inbounds": [
{
"type": "mixed",
"tag": "in"
}
],
"route": {
"rules": [
{
"inbound": "in",
"action": "resolve",
"strategy": "prefer_ipv4"
},
{
"inbound": "in",
"action": "sniff",
"timeout": "1s"
}
]
}
}
```
## 1.10.0 ## 1.10.0
### TUN address fields are merged ### TUN address fields are merged
@@ -164,6 +10,8 @@ Inbound fields are deprecated and can be replaced by rule actions.
`inet4_route_address` and `inet6_route_address` are merged into `route_address`, `inet4_route_address` and `inet6_route_address` are merged into `route_address`,
`inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`. `inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`.
Old fields are deprecated and will be removed in sing-box 1.11.0.
!!! info "References" !!! info "References"
[TUN](/configuration/inbound/tun/) [TUN](/configuration/inbound/tun/)

View File

@@ -2,160 +2,6 @@
icon: material/arrange-bring-forward icon: material/arrange-bring-forward
--- ---
## 1.11.0
### 迁移旧的特殊出站到规则动作
旧的特殊出站已被弃用,且可以被规则动作替代。
!!! info "参考"
[规则动作](/zh/configuration/route/rule_action/) /
[Block](/zh/configuration/outbound/block/) /
[DNS](/zh/configuration/outbound/dns)
=== "Block"
=== ":material-card-remove: 弃用的"
```json
{
"outbounds": [
{
"type": "block",
"tag": "block"
}
],
"route": {
"rules": [
{
...,
"outbound": "block"
}
]
}
}
```
=== ":material-card-multiple: 新的"
```json
{
"route": {
"rules": [
{
...,
"action": "reject"
}
]
}
}
```
=== "DNS"
=== ":material-card-remove: 弃用的"
```json
{
"inbound": [
{
...,
"sniff": true
}
],
"outbounds": [
{
"tag": "dns",
"type": "dns"
}
],
"route": {
"rules": [
{
"protocol": "dns",
"outbound": "dns"
}
]
}
}
```
=== ":material-card-multiple: 新的"
```json
{
"route": {
"rules": [
{
"action": "sniff"
},
{
"protocol": "dns",
"action": "hijack-dns"
}
]
}
}
```
### 迁移旧的入站字段到规则动作
入站选项已被弃用,且可以被规则动作替代。
!!! info "参考"
[监听字段](/zh/configuration/shared/listen/) /
[规则](/zh/configuration/route/rule/) /
[规则动作](/zh/configuration/route/rule_action/) /
[DNS 规则](/zh/configuration/dns/rule/) /
[DNS 规则动作](/zh/configuration/dns/rule_action/)
=== ":material-card-remove: 弃用的"
```json
{
"inbounds": [
{
"type": "mixed",
"sniff": true,
"sniff_timeout": "1s",
"domain_strategy": "prefer_ipv4"
}
]
}
```
=== ":material-card-multiple: New"
```json
{
"inbounds": [
{
"type": "mixed",
"tag": "in"
}
],
"route": {
"rules": [
{
"inbound": "in",
"action": "resolve",
"strategy": "prefer_ipv4"
},
{
"inbound": "in",
"action": "sniff",
"timeout": "1s"
}
]
}
}
```
## 1.10.0 ## 1.10.0
### TUN 地址字段已合并 ### TUN 地址字段已合并

View File

@@ -93,18 +93,7 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
} }
} }
func (c *CacheFile) Name() string { func (c *CacheFile) start() error {
return "cache-file"
}
func (c *CacheFile) Dependencies() []string {
return nil
}
func (c *CacheFile) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateInitialize {
return nil
}
const fileMode = 0o666 const fileMode = 0o666
options := bbolt.Options{Timeout: time.Second} options := bbolt.Options{Timeout: time.Second}
var ( var (
@@ -162,6 +151,14 @@ func (c *CacheFile) Start(stage adapter.StartStage) error {
return nil return nil
} }
func (c *CacheFile) PreStart() error {
return c.start()
}
func (c *CacheFile) Start() error {
return nil
}
func (c *CacheFile) Close() error { func (c *CacheFile) Close() error {
if c.DB == nil { if c.DB == nil {
return nil return nil

View File

@@ -12,7 +12,7 @@ import (
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
) )
type ClashServerConstructor = func(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) type ClashServerConstructor = func(ctx context.Context, router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error)
var clashServerConstructor ClashServerConstructor var clashServerConstructor ClashServerConstructor
@@ -20,11 +20,11 @@ func RegisterClashServerConstructor(constructor ClashServerConstructor) {
clashServerConstructor = constructor clashServerConstructor = constructor
} }
func NewClashServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) { func NewClashServer(ctx context.Context, router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
if clashServerConstructor == nil { if clashServerConstructor == nil {
return nil, os.ErrInvalid return nil, os.ErrInvalid
} }
return clashServerConstructor(ctx, logFactory, options) return clashServerConstructor(ctx, router, logFactory, options)
} }
func CalculateClashModeList(options option.Options) []string { func CalculateClashModeList(options option.Options) []string {

View File

@@ -23,7 +23,7 @@ func groupRouter(server *Server) http.Handler {
r := chi.NewRouter() r := chi.NewRouter()
r.Get("/", getGroups(server)) r.Get("/", getGroups(server))
r.Route("/{name}", func(r chi.Router) { r.Route("/{name}", func(r chi.Router) {
r.Use(parseProxyName, findProxyByName(server)) r.Use(parseProxyName, findProxyByName(server.router))
r.Get("/", getGroup(server)) r.Get("/", getGroup(server))
r.Get("/delay", getGroupDelay(server)) r.Get("/delay", getGroupDelay(server))
}) })
@@ -32,7 +32,7 @@ func groupRouter(server *Server) http.Handler {
func getGroups(server *Server) func(w http.ResponseWriter, r *http.Request) { func getGroups(server *Server) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
groups := common.Map(common.Filter(server.outboundManager.Outbounds(), func(it adapter.Outbound) bool { groups := common.Map(common.Filter(server.router.Outbounds(), func(it adapter.Outbound) bool {
_, isGroup := it.(adapter.OutboundGroup) _, isGroup := it.(adapter.OutboundGroup)
return isGroup return isGroup
}), func(it adapter.Outbound) *badjson.JSONObject { }), func(it adapter.Outbound) *badjson.JSONObject {
@@ -86,7 +86,7 @@ func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request)
result, err = urlTestGroup.URLTest(ctx) result, err = urlTestGroup.URLTest(ctx)
} else { } else {
outbounds := common.FilterNotNil(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { outbounds := common.FilterNotNil(common.Map(outboundGroup.All(), func(it string) adapter.Outbound {
itOutbound, _ := server.outboundManager.Outbound(it) itOutbound, _ := server.router.Outbound(it)
return itOutbound return itOutbound
})) }))
b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10)) b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10))
@@ -100,7 +100,7 @@ func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request)
continue continue
} }
checked[realTag] = true checked[realTag] = true
p, loaded := server.outboundManager.Outbound(realTag) p, loaded := server.router.Outbound(realTag)
if !loaded { if !loaded {
continue continue
} }

View File

@@ -23,10 +23,10 @@ import (
func proxyRouter(server *Server, router adapter.Router) http.Handler { func proxyRouter(server *Server, router adapter.Router) http.Handler {
r := chi.NewRouter() r := chi.NewRouter()
r.Get("/", getProxies(server)) r.Get("/", getProxies(server, router))
r.Route("/{name}", func(r chi.Router) { r.Route("/{name}", func(r chi.Router) {
r.Use(parseProxyName, findProxyByName(server)) r.Use(parseProxyName, findProxyByName(router))
r.Get("/", getProxy(server)) r.Get("/", getProxy(server))
r.Get("/delay", getProxyDelay(server)) r.Get("/delay", getProxyDelay(server))
r.Put("/", updateProxy) r.Put("/", updateProxy)
@@ -42,11 +42,11 @@ func parseProxyName(next http.Handler) http.Handler {
}) })
} }
func findProxyByName(server *Server) func(next http.Handler) http.Handler { func findProxyByName(router adapter.Router) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
name := r.Context().Value(CtxKeyProxyName).(string) name := r.Context().Value(CtxKeyProxyName).(string)
proxy, exist := server.outboundManager.Outbound(name) proxy, exist := router.Outbound(name)
if !exist { if !exist {
render.Status(r, http.StatusNotFound) render.Status(r, http.StatusNotFound)
render.JSON(w, r, ErrNotFound) render.JSON(w, r, ErrNotFound)
@@ -83,10 +83,10 @@ func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject {
return &info return &info
} }
func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) { func getProxies(server *Server, router adapter.Router) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
var proxyMap badjson.JSONObject var proxyMap badjson.JSONObject
outbounds := common.Filter(server.outboundManager.Outbounds(), func(detour adapter.Outbound) bool { outbounds := common.Filter(router.Outbounds(), func(detour adapter.Outbound) bool {
return detour.Tag() != "" return detour.Tag() != ""
}) })
@@ -100,7 +100,12 @@ func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) {
allProxies = append(allProxies, detour.Tag()) allProxies = append(allProxies, detour.Tag())
} }
defaultTag := server.outboundManager.Default().Tag() var defaultTag string
if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil {
defaultTag = defaultOutbound.Tag()
} else {
defaultTag = allProxies[0]
}
sort.SliceStable(allProxies, func(i, j int) bool { sort.SliceStable(allProxies, func(i, j int) bool {
return allProxies[i] == defaultTag return allProxies[i] == defaultTag

View File

@@ -42,7 +42,6 @@ var _ adapter.ClashServer = (*Server)(nil)
type Server struct { type Server struct {
ctx context.Context ctx context.Context
router adapter.Router router adapter.Router
outboundManager adapter.OutboundManager
logger log.Logger logger log.Logger
httpServer *http.Server httpServer *http.Server
trafficManager *trafficontrol.Manager trafficManager *trafficontrol.Manager
@@ -57,13 +56,12 @@ type Server struct {
externalUIDownloadDetour string externalUIDownloadDetour string
} }
func NewServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) { func NewServer(ctx context.Context, router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
trafficManager := trafficontrol.NewManager() trafficManager := trafficontrol.NewManager()
chiRouter := chi.NewRouter() chiRouter := chi.NewRouter()
s := &Server{ server := &Server{
ctx: ctx, ctx: ctx,
router: service.FromContext[adapter.Router](ctx), router: router,
outboundManager: service.FromContext[adapter.OutboundManager](ctx),
logger: logFactory.NewLogger("clash-api"), logger: logFactory.NewLogger("clash-api"),
httpServer: &http.Server{ httpServer: &http.Server{
Addr: options.ExternalController, Addr: options.ExternalController,
@@ -75,18 +73,18 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
externalUIDownloadURL: options.ExternalUIDownloadURL, externalUIDownloadURL: options.ExternalUIDownloadURL,
externalUIDownloadDetour: options.ExternalUIDownloadDetour, externalUIDownloadDetour: options.ExternalUIDownloadDetour,
} }
s.urlTestHistory = service.PtrFromContext[urltest.HistoryStorage](ctx) server.urlTestHistory = service.PtrFromContext[urltest.HistoryStorage](ctx)
if s.urlTestHistory == nil { if server.urlTestHistory == nil {
s.urlTestHistory = urltest.NewHistoryStorage() server.urlTestHistory = urltest.NewHistoryStorage()
} }
defaultMode := "Rule" defaultMode := "Rule"
if options.DefaultMode != "" { if options.DefaultMode != "" {
defaultMode = options.DefaultMode defaultMode = options.DefaultMode
} }
if !common.Contains(s.modeList, defaultMode) { if !common.Contains(server.modeList, defaultMode) {
s.modeList = append([]string{defaultMode}, s.modeList...) server.modeList = append([]string{defaultMode}, server.modeList...)
} }
s.mode = defaultMode server.mode = defaultMode
//goland:noinspection GoDeprecation //goland:noinspection GoDeprecation
//nolint:staticcheck //nolint:staticcheck
if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" { if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" {
@@ -110,39 +108,33 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
r.Get("/logs", getLogs(logFactory)) r.Get("/logs", getLogs(logFactory))
r.Get("/traffic", traffic(trafficManager)) r.Get("/traffic", traffic(trafficManager))
r.Get("/version", version) r.Get("/version", version)
r.Mount("/configs", configRouter(s, logFactory)) r.Mount("/configs", configRouter(server, logFactory))
r.Mount("/proxies", proxyRouter(s, s.router)) r.Mount("/proxies", proxyRouter(server, router))
r.Mount("/rules", ruleRouter(s.router)) r.Mount("/rules", ruleRouter(router))
r.Mount("/connections", connectionRouter(s.router, trafficManager)) r.Mount("/connections", connectionRouter(router, trafficManager))
r.Mount("/providers/proxies", proxyProviderRouter()) r.Mount("/providers/proxies", proxyProviderRouter())
r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/providers/rules", ruleProviderRouter())
r.Mount("/script", scriptRouter()) r.Mount("/script", scriptRouter())
r.Mount("/profile", profileRouter()) r.Mount("/profile", profileRouter())
r.Mount("/cache", cacheRouter(ctx)) r.Mount("/cache", cacheRouter(ctx))
r.Mount("/dns", dnsRouter(s.router)) r.Mount("/dns", dnsRouter(router))
s.setupMetaAPI(r) server.setupMetaAPI(r)
}) })
if options.ExternalUI != "" { if options.ExternalUI != "" {
s.externalUI = filemanager.BasePath(ctx, os.ExpandEnv(options.ExternalUI)) server.externalUI = filemanager.BasePath(ctx, os.ExpandEnv(options.ExternalUI))
chiRouter.Group(func(r chi.Router) { chiRouter.Group(func(r chi.Router) {
fs := http.StripPrefix("/ui", http.FileServer(http.Dir(s.externalUI))) fs := http.StripPrefix("/ui", http.FileServer(http.Dir(server.externalUI)))
r.Get("/ui", http.RedirectHandler("/ui/", http.StatusTemporaryRedirect).ServeHTTP) r.Get("/ui", http.RedirectHandler("/ui/", http.StatusTemporaryRedirect).ServeHTTP)
r.Get("/ui/*", func(w http.ResponseWriter, r *http.Request) { r.Get("/ui/*", func(w http.ResponseWriter, r *http.Request) {
fs.ServeHTTP(w, r) fs.ServeHTTP(w, r)
}) })
}) })
} }
return s, nil return server, nil
} }
func (s *Server) Name() string { func (s *Server) PreStart() error {
return "clash server"
}
func (s *Server) Start(stage adapter.StartStage) error {
switch stage {
case adapter.StartStateStart:
cacheFile := service.FromContext[adapter.CacheFile](s.ctx) cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
if cacheFile != nil { if cacheFile != nil {
mode := cacheFile.LoadMode() mode := cacheFile.LoadMode()
@@ -152,7 +144,10 @@ func (s *Server) Start(stage adapter.StartStage) error {
s.mode = mode s.mode = mode
} }
} }
case adapter.StartStateStarted: return nil
}
func (s *Server) Start() error {
if s.externalController { if s.externalController {
s.checkAndDownloadExternalUI() s.checkAndDownloadExternalUI()
var ( var (
@@ -178,8 +173,6 @@ func (s *Server) Start(stage adapter.StartStage) error {
} }
}() }()
} }
}
return nil return nil
} }
@@ -241,12 +234,14 @@ func (s *Server) TrafficManager() *trafficontrol.Manager {
return s.trafficManager return s.trafficManager
} }
func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn { func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule) (net.Conn, adapter.Tracker) {
return trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.outboundManager, matchedRule, matchOutbound) tracker := trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.router, matchedRule)
return tracker, tracker
} }
func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) N.PacketConn { func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule) (N.PacketConn, adapter.Tracker) {
return trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.outboundManager, matchedRule, matchOutbound) tracker := trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.router, matchedRule)
return tracker, tracker
} }
func authentication(serverSecret string) func(next http.Handler) http.Handler { func authentication(serverSecret string) func(next http.Handler) http.Handler {

View File

@@ -15,6 +15,7 @@ import (
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service/filemanager" "github.com/sagernet/sing/service/filemanager"
) )
@@ -44,13 +45,16 @@ func (s *Server) downloadExternalUI() error {
s.logger.Info("downloading external ui") s.logger.Info("downloading external ui")
var detour adapter.Outbound var detour adapter.Outbound
if s.externalUIDownloadDetour != "" { if s.externalUIDownloadDetour != "" {
outbound, loaded := s.outboundManager.Outbound(s.externalUIDownloadDetour) outbound, loaded := s.router.Outbound(s.externalUIDownloadDetour)
if !loaded { if !loaded {
return E.New("detour outbound not found: ", s.externalUIDownloadDetour) return E.New("detour outbound not found: ", s.externalUIDownloadDetour)
} }
detour = outbound detour = outbound
} else { } else {
outbound := s.outboundManager.Default() outbound, err := s.router.DefaultOutbound(N.NetworkTCP)
if err != nil {
return err
}
detour = outbound detour = outbound
} }
httpClient := &http.Client{ httpClient := &http.Client{

View File

@@ -5,6 +5,7 @@ import (
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
R "github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic" "github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
@@ -87,6 +88,7 @@ func (t TrackerMetadata) MarshalJSON() ([]byte, error) {
} }
type Tracker interface { type Tracker interface {
adapter.Tracker
Metadata() TrackerMetadata Metadata() TrackerMetadata
Close() error Close() error
} }
@@ -106,6 +108,10 @@ func (tt *TCPConn) Close() error {
return tt.ExtendedConn.Close() return tt.ExtendedConn.Close()
} }
func (tt *TCPConn) Leave() {
tt.manager.Leave(tt)
}
func (tt *TCPConn) Upstream() any { func (tt *TCPConn) Upstream() any {
return tt.ExtendedConn return tt.ExtendedConn
} }
@@ -118,7 +124,7 @@ func (tt *TCPConn) WriterReplaceable() bool {
return true return true
} }
func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundContext, outboundManager adapter.OutboundManager, matchRule adapter.Rule, matchOutbound adapter.Outbound) *TCPConn { func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundContext, router adapter.Router, rule adapter.Rule) *TCPConn {
id, _ := uuid.NewV4() id, _ := uuid.NewV4()
var ( var (
chain []string chain []string
@@ -126,13 +132,17 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundCont
outbound string outbound string
outboundType string outboundType string
) )
if matchOutbound != nil { var action adapter.RuleAction
next = matchOutbound.Tag() if rule != nil {
} else { action = rule.Action()
next = outboundManager.Default().Tag() }
if routeAction, isRouteAction := action.(*R.RuleActionRoute); isRouteAction {
next = routeAction.Outbound
} else if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil {
next = defaultOutbound.Tag()
} }
for { for {
detour, loaded := outboundManager.Outbound(next) detour, loaded := router.Outbound(next)
if !loaded { if !loaded {
break break
} }
@@ -162,7 +172,7 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundCont
Upload: upload, Upload: upload,
Download: download, Download: download,
Chain: common.Reverse(chain), Chain: common.Reverse(chain),
Rule: matchRule, Rule: rule,
Outbound: outbound, Outbound: outbound,
OutboundType: outboundType, OutboundType: outboundType,
}, },
@@ -187,6 +197,10 @@ func (ut *UDPConn) Close() error {
return ut.PacketConn.Close() return ut.PacketConn.Close()
} }
func (ut *UDPConn) Leave() {
ut.manager.Leave(ut)
}
func (ut *UDPConn) Upstream() any { func (ut *UDPConn) Upstream() any {
return ut.PacketConn return ut.PacketConn
} }
@@ -199,7 +213,7 @@ func (ut *UDPConn) WriterReplaceable() bool {
return true return true
} }
func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.InboundContext, outboundManager adapter.OutboundManager, matchRule adapter.Rule, matchOutbound adapter.Outbound) *UDPConn { func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.InboundContext, router adapter.Router, rule adapter.Rule) *UDPConn {
id, _ := uuid.NewV4() id, _ := uuid.NewV4()
var ( var (
chain []string chain []string
@@ -207,13 +221,17 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.Inbound
outbound string outbound string
outboundType string outboundType string
) )
if matchOutbound != nil { var action adapter.RuleAction
next = matchOutbound.Tag() if rule != nil {
} else { action = rule.Action()
next = outboundManager.Default().Tag() }
if routeAction, isRouteAction := action.(*R.RuleActionRoute); isRouteAction {
next = routeAction.Outbound
} else if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil {
next = defaultOutbound.Tag()
} }
for { for {
detour, loaded := outboundManager.Outbound(next) detour, loaded := router.Outbound(next)
if !loaded { if !loaded {
break break
} }
@@ -243,7 +261,7 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.Inbound
Upload: upload, Upload: upload,
Download: download, Download: download,
Chain: common.Reverse(chain), Chain: common.Reverse(chain),
Rule: matchRule, Rule: rule,
Outbound: outbound, Outbound: outbound,
OutboundType: outboundType, OutboundType: outboundType,
}, },

View File

@@ -1,7 +1,6 @@
package deprecated package deprecated
import ( import (
"github.com/sagernet/sing-box/common/badversion"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
@@ -24,9 +23,8 @@ func (n Note) Impending() bool {
if !semver.IsValid("v" + C.Version) { if !semver.IsValid("v" + C.Version) {
return false return false
} }
versionCurrent := badversion.Parse(C.Version) versionMinor := semver.Compare(semver.MajorMinor("v"+C.Version), "v"+n.ScheduledVersion)
versionMinor := badversion.Parse(n.ScheduledVersion).Minor - versionCurrent.Minor if versionMinor < 0 {
if versionCurrent.PreReleaseIdentifier == "" && versionMinor < 0 {
panic("invalid deprecated note: " + n.Name) panic("invalid deprecated note: " + n.Name)
} }
return versionMinor <= 1 return versionMinor <= 1
@@ -51,7 +49,6 @@ var OptionBadMatchSource = Note{
Description: "legacy match source rule item", Description: "legacy match source rule item",
DeprecatedVersion: "1.10.0", DeprecatedVersion: "1.10.0",
ScheduledVersion: "1.11.0", ScheduledVersion: "1.11.0",
EnvName: "BAD_MATCH_SOURCE",
MigrationLink: "https://sing-box.sagernet.org/deprecated/#match-source-rule-items-are-renamed", MigrationLink: "https://sing-box.sagernet.org/deprecated/#match-source-rule-items-are-renamed",
} }
@@ -78,33 +75,12 @@ var OptionTUNAddressX = Note{
Description: "legacy tun address fields", Description: "legacy tun address fields",
DeprecatedVersion: "1.10.0", DeprecatedVersion: "1.10.0",
ScheduledVersion: "1.12.0", ScheduledVersion: "1.12.0",
EnvName: "TUN_ADDRESS_X",
MigrationLink: "https://sing-box.sagernet.org/migration/#tun-address-fields-are-merged", MigrationLink: "https://sing-box.sagernet.org/migration/#tun-address-fields-are-merged",
} }
var OptionSpecialOutbounds = Note{
Name: "special-outbounds",
Description: "legacy special outbounds",
DeprecatedVersion: "1.11.0",
ScheduledVersion: "1.13.0",
EnvName: "SPECIAL_OUTBOUNDS",
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions",
}
var OptionInboundOptions = Note{
Name: "inbound-options",
Description: "legacy inbound fields",
DeprecatedVersion: "1.11.0",
ScheduledVersion: "1.13.0",
EnvName: "INBOUND_OPTIONS",
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions",
}
var Options = []Note{ var Options = []Note{
OptionBadMatchSource, OptionBadMatchSource,
OptionGEOIP, OptionGEOIP,
OptionGEOSITE, OptionGEOSITE,
OptionTUNAddressX, OptionTUNAddressX,
OptionSpecialOutbounds,
OptionInboundOptions,
} }

View File

@@ -7,23 +7,15 @@ import (
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
) )
type stderrManager struct { type envManager struct {
logger logger.Logger logger logger.Logger
reported map[string]bool
} }
func NewStderrManager(logger logger.Logger) Manager { func NewEnvManager(logger logger.Logger) Manager {
return &stderrManager{ return &envManager{logger: logger}
logger: logger,
reported: make(map[string]bool),
}
} }
func (f *stderrManager) ReportDeprecated(feature Note) { func (f *envManager) ReportDeprecated(feature Note) {
if f.reported[feature.Name] {
return
}
f.reported[feature.Name] = true
if !feature.Impending() { if !feature.Impending() {
f.logger.Warn(feature.MessageWithLink()) f.logger.Warn(feature.MessageWithLink())
return return

View File

@@ -38,7 +38,11 @@ func (s *CommandServer) handleSetClashMode(conn net.Conn) error {
if service == nil { if service == nil {
return writeError(conn, E.New("service not ready")) return writeError(conn, E.New("service not ready"))
} }
service.clashServer.(*clashapi.Server).SetMode(newMode) clashServer := service.instance.Router().ClashServer()
if clashServer == nil {
return writeError(conn, E.New("Clash API disabled"))
}
clashServer.(*clashapi.Server).SetMode(newMode)
return writeError(conn, nil) return writeError(conn, nil)
} }
@@ -65,14 +69,18 @@ func (s *CommandServer) handleModeConn(conn net.Conn) error {
return ctx.Err() return ctx.Err()
} }
} }
err := writeClashModeList(conn, s.service.clashServer) clashServer := s.service.instance.Router().ClashServer()
if clashServer == nil {
return binary.Write(conn, binary.BigEndian, uint16(0))
}
err := writeClashModeList(conn, clashServer)
if err != nil { if err != nil {
return err return err
} }
for { for {
select { select {
case <-s.modeUpdate: case <-s.modeUpdate:
err = varbin.Write(conn, binary.BigEndian, s.service.clashServer.Mode()) err = varbin.Write(conn, binary.BigEndian, clashServer.Mode())
if err != nil { if err != nil {
return err return err
} }

View File

@@ -20,6 +20,7 @@ type CommandClient struct {
type CommandClientOptions struct { type CommandClientOptions struct {
Command int32 Command int32
StatusInterval int64 StatusInterval int64
IsMainClient bool
} }
type CommandClientHandler interface { type CommandClientHandler interface {
@@ -28,6 +29,7 @@ type CommandClientHandler interface {
ClearLogs() ClearLogs()
WriteLogs(messageList StringIterator) WriteLogs(messageList StringIterator)
WriteStatus(message *StatusMessage) WriteStatus(message *StatusMessage)
OpenURL(url string)
WriteGroups(message OutboundGroupIterator) WriteGroups(message OutboundGroupIterator)
InitializeClashMode(modeList StringIterator, currentMode string) InitializeClashMode(modeList StringIterator, currentMode string)
UpdateClashMode(newMode string) UpdateClashMode(newMode string)
@@ -91,9 +93,13 @@ func (c *CommandClient) Connect() error {
c.handler.Connected() c.handler.Connected()
go c.handleLogConn(conn) go c.handleLogConn(conn)
case CommandStatus: case CommandStatus:
err = binary.Write(conn, binary.BigEndian, c.options.IsMainClient)
if err != nil {
return E.Cause(err, "write is main client")
}
err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval) err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval)
if err != nil { if err != nil {
return E.Cause(err, "write interval") return E.Cause(err, "write header")
} }
c.handler.Connected() c.handler.Connected()
go c.handleStatusConn(conn) go c.handleStatusConn(conn)

View File

@@ -45,7 +45,11 @@ func (s *CommandServer) handleCloseConnection(conn net.Conn) error {
if service == nil { if service == nil {
return writeError(conn, E.New("service not ready")) return writeError(conn, E.New("service not ready"))
} }
targetConn := service.clashServer.(*clashapi.Server).TrafficManager().Connection(uuid.FromStringOrNil(connId)) clashServer := service.instance.Router().ClashServer()
if clashServer == nil {
return writeError(conn, E.New("Clash API disabled"))
}
targetConn := clashServer.(*clashapi.Server).TrafficManager().Connection(uuid.FromStringOrNil(connId))
if targetConn == nil { if targetConn == nil {
return writeError(conn, E.New("connection already closed")) return writeError(conn, E.New("connection already closed"))
} }

View File

@@ -49,7 +49,11 @@ func (s *CommandServer) handleConnectionsConn(conn net.Conn) error {
for { for {
service := s.service service := s.service
if service != nil { if service != nil {
trafficManager = service.clashServer.(*clashapi.Server).TrafficManager() clashServer := service.instance.Router().ClashServer()
if clashServer == nil {
return E.New("Clash API disabled")
}
trafficManager = clashServer.(*clashapi.Server).TrafficManager()
break break
} }
select { select {

View File

@@ -0,0 +1,40 @@
package libbox
import (
"encoding/binary"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin"
)
type myEvent interface {
writeTo(writer varbin.Writer)
}
func readEvent(reader varbin.Reader) (myEvent, error) {
eventType, err := reader.ReadByte()
if err != nil {
return nil, err
}
switch eventType {
case eventTypeEmpty:
return nil, nil
case eventTypeOpenURL:
url, err := varbin.ReadValue[string](reader, binary.BigEndian)
if err != nil {
return nil, err
}
return &eventOpenURL{URL: url}, nil
default:
return nil, E.New("unknown event type: ", eventType)
}
}
type eventOpenURL struct {
URL string
}
func (e *eventOpenURL) writeTo(writer varbin.Writer) {
writer.WriteByte(eventTypeOpenURL)
varbin.Write(writer, binary.BigEndian, e.URL)
}

View File

@@ -109,7 +109,7 @@ func readGroups(reader io.Reader) (OutboundGroupIterator, error) {
func writeGroups(writer io.Writer, boxService *BoxService) error { func writeGroups(writer io.Writer, boxService *BoxService) error {
historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx) historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx)
cacheFile := service.FromContext[adapter.CacheFile](boxService.ctx) cacheFile := service.FromContext[adapter.CacheFile](boxService.ctx)
outbounds := boxService.instance.Outbound().Outbounds() outbounds := boxService.instance.Router().Outbounds()
var iGroups []adapter.OutboundGroup var iGroups []adapter.OutboundGroup
for _, it := range outbounds { for _, it := range outbounds {
if group, isGroup := it.(adapter.OutboundGroup); isGroup { if group, isGroup := it.(adapter.OutboundGroup); isGroup {
@@ -130,7 +130,7 @@ func writeGroups(writer io.Writer, boxService *BoxService) error {
} }
for _, itemTag := range iGroup.All() { for _, itemTag := range iGroup.All() {
itemOutbound, isLoaded := boxService.instance.Outbound().Outbound(itemTag) itemOutbound, isLoaded := boxService.instance.Router().Outbound(itemTag)
if !isLoaded { if !isLoaded {
continue continue
} }

View File

@@ -43,7 +43,7 @@ func (s *CommandServer) handleSelectOutbound(conn net.Conn) error {
if service == nil { if service == nil {
return writeError(conn, E.New("service not ready")) return writeError(conn, E.New("service not ready"))
} }
outboundGroup, isLoaded := service.instance.Outbound().Outbound(groupTag) outboundGroup, isLoaded := service.instance.Router().Outbound(groupTag)
if !isLoaded { if !isLoaded {
return writeError(conn, E.New("selector not found: ", groupTag)) return writeError(conn, E.New("selector not found: ", groupTag))
} }

View File

@@ -33,6 +33,7 @@ type CommandServer struct {
urlTestUpdate chan struct{} urlTestUpdate chan struct{}
modeUpdate chan struct{} modeUpdate chan struct{}
logReset chan struct{} logReset chan struct{}
events chan myEvent
closedConnections []Connection closedConnections []Connection
} }
@@ -52,6 +53,7 @@ func NewCommandServer(handler CommandServerHandler, maxLines int32) *CommandServ
urlTestUpdate: make(chan struct{}, 1), urlTestUpdate: make(chan struct{}, 1),
modeUpdate: make(chan struct{}, 1), modeUpdate: make(chan struct{}, 1),
logReset: make(chan struct{}, 1), logReset: make(chan struct{}, 1),
events: make(chan myEvent, 8),
} }
server.observer = observable.NewObserver[string](server.subscriber, 64) server.observer = observable.NewObserver[string](server.subscriber, 64)
return server return server
@@ -60,7 +62,13 @@ func NewCommandServer(handler CommandServerHandler, maxLines int32) *CommandServ
func (s *CommandServer) SetService(newService *BoxService) { func (s *CommandServer) SetService(newService *BoxService) {
if newService != nil { if newService != nil {
service.PtrFromContext[urltest.HistoryStorage](newService.ctx).SetHook(s.urlTestUpdate) service.PtrFromContext[urltest.HistoryStorage](newService.ctx).SetHook(s.urlTestUpdate)
newService.clashServer.(*clashapi.Server).SetModeUpdateHook(s.modeUpdate) newService.instance.Router().ClashServer().(*clashapi.Server).SetModeUpdateHook(s.modeUpdate)
newService.platformInterface.openURLFunc = func(url string) {
select {
case s.events <- &eventOpenURL{URL: url}:
default:
}
}
} }
s.service = newService s.service = newService
s.notifyURLTestUpdate() s.notifyURLTestUpdate()

View File

@@ -1,6 +1,7 @@
package libbox package libbox
import ( import (
std_bufio "bufio"
"encoding/binary" "encoding/binary"
"net" "net"
"runtime" "runtime"
@@ -9,9 +10,15 @@ import (
"github.com/sagernet/sing-box/common/conntrack" "github.com/sagernet/sing-box/common/conntrack"
"github.com/sagernet/sing-box/experimental/clashapi" "github.com/sagernet/sing-box/experimental/clashapi"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/memory" "github.com/sagernet/sing/common/memory"
) )
const (
eventTypeEmpty byte = iota
eventTypeOpenURL
)
type StatusMessage struct { type StatusMessage struct {
Memory int64 Memory int64
Goroutines int32 Goroutines int32
@@ -31,42 +38,86 @@ func (s *CommandServer) readStatus() StatusMessage {
message.ConnectionsOut = int32(conntrack.Count()) message.ConnectionsOut = int32(conntrack.Count())
if s.service != nil { if s.service != nil {
if clashServer := s.service.instance.Router().ClashServer(); clashServer != nil {
message.TrafficAvailable = true message.TrafficAvailable = true
trafficManager := s.service.clashServer.(*clashapi.Server).TrafficManager() trafficManager := clashServer.(*clashapi.Server).TrafficManager()
message.Uplink, message.Downlink = trafficManager.Now() message.Uplink, message.Downlink = trafficManager.Now()
message.UplinkTotal, message.DownlinkTotal = trafficManager.Total() message.UplinkTotal, message.DownlinkTotal = trafficManager.Total()
message.ConnectionsIn = int32(trafficManager.ConnectionsLen()) message.ConnectionsIn = int32(trafficManager.ConnectionsLen())
} }
}
return message return message
} }
func (s *CommandServer) handleStatusConn(conn net.Conn) error { func (s *CommandServer) handleStatusConn(conn net.Conn) error {
var isMainClient bool
err := binary.Read(conn, binary.BigEndian, &isMainClient)
if err != nil {
return E.Cause(err, "read is main client")
}
var interval int64 var interval int64
err := binary.Read(conn, binary.BigEndian, &interval) err = binary.Read(conn, binary.BigEndian, &interval)
if err != nil { if err != nil {
return E.Cause(err, "read interval") return E.Cause(err, "read interval")
} }
ticker := time.NewTicker(time.Duration(interval)) ticker := time.NewTicker(time.Duration(interval))
defer ticker.Stop() defer ticker.Stop()
ctx := connKeepAlive(conn) ctx := connKeepAlive(conn)
writer := std_bufio.NewWriter(conn)
if isMainClient {
for {
writer.WriteByte(eventTypeEmpty)
err = binary.Write(conn, binary.BigEndian, s.readStatus())
if err != nil {
return err
}
writer.Flush()
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
case event := <-s.events:
event.writeTo(writer)
writer.Flush()
}
}
} else {
for { for {
err = binary.Write(conn, binary.BigEndian, s.readStatus()) err = binary.Write(conn, binary.BigEndian, s.readStatus())
if err != nil { if err != nil {
return err return err
} }
writer.Flush()
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
case <-ticker.C: case <-ticker.C:
} }
} }
}
} }
func (c *CommandClient) handleStatusConn(conn net.Conn) { func (c *CommandClient) handleStatusConn(conn net.Conn) {
reader := std_bufio.NewReader(conn)
for { for {
if c.options.IsMainClient {
rawEvent, err := readEvent(reader)
if err != nil {
c.handler.Disconnected(err.Error())
return
}
switch event := rawEvent.(type) {
case *eventOpenURL:
c.handler.OpenURL(event.URL)
continue
case nil:
default:
panic(F.ToString("unexpected event type: ", event))
}
}
var message StatusMessage var message StatusMessage
err := binary.Read(conn, binary.BigEndian, &message) err := binary.Read(reader, binary.BigEndian, &message)
if err != nil { if err != nil {
c.handler.Disconnected(err.Error()) c.handler.Disconnected(err.Error())
return return

View File

@@ -41,7 +41,7 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error {
if serviceNow == nil { if serviceNow == nil {
return nil return nil
} }
abstractOutboundGroup, isLoaded := serviceNow.instance.Outbound().Outbound(groupTag) abstractOutboundGroup, isLoaded := serviceNow.instance.Router().Outbound(groupTag)
if !isLoaded { if !isLoaded {
return writeError(conn, E.New("outbound group not found: ", groupTag)) return writeError(conn, E.New("outbound group not found: ", groupTag))
} }
@@ -55,7 +55,7 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error {
} else { } else {
historyStorage := service.PtrFromContext[urltest.HistoryStorage](serviceNow.ctx) historyStorage := service.PtrFromContext[urltest.HistoryStorage](serviceNow.ctx)
outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound {
itOutbound, _ := serviceNow.instance.Outbound().Outbound(it) itOutbound, _ := serviceNow.instance.Router().Outbound(it)
return itOutbound return itOutbound
}), func(it adapter.Outbound) bool { }), func(it adapter.Outbound) bool {
if it == nil { if it == nil {

View File

@@ -30,12 +30,11 @@ func parseConfig(ctx context.Context, configContent string) (option.Options, err
} }
func CheckConfig(configContent string) error { func CheckConfig(configContent string) error {
ctx := box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry()) options, err := parseConfig(box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry()), configContent)
options, err := parseConfig(ctx, configContent)
if err != nil { if err != nil {
return err return err
} }
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
ctx = service.ContextWith[platform.Interface](ctx, (*platformInterfaceStub)(nil)) ctx = service.ContextWith[platform.Interface](ctx, (*platformInterfaceStub)(nil))
instance, err := box.New(box.Options{ instance, err := box.New(box.Options{
@@ -50,7 +49,7 @@ func CheckConfig(configContent string) error {
type platformInterfaceStub struct{} type platformInterfaceStub struct{}
func (s *platformInterfaceStub) Initialize(networkManager adapter.NetworkManager) error { func (s *platformInterfaceStub) Initialize(ctx context.Context, router adapter.Router) error {
return nil return nil
} }
@@ -58,7 +57,7 @@ func (s *platformInterfaceStub) UsePlatformAutoDetectInterfaceControl() bool {
return true return true
} }
func (s *platformInterfaceStub) AutoDetectInterfaceControl(fd int) error { func (s *platformInterfaceStub) AutoDetectInterfaceControl() control.Func {
return nil return nil
} }
@@ -74,7 +73,11 @@ func (s *platformInterfaceStub) CreateDefaultInterfaceMonitor(logger logger.Logg
return (*interfaceMonitorStub)(nil) return (*interfaceMonitorStub)(nil)
} }
func (s *platformInterfaceStub) Interfaces() ([]adapter.NetworkInterface, error) { func (s *platformInterfaceStub) UsePlatformInterfaceGetter() bool {
return true
}
func (s *platformInterfaceStub) Interfaces() ([]control.Interface, error) {
return nil, os.ErrInvalid return nil, os.ErrInvalid
} }
@@ -107,8 +110,16 @@ func (s *interfaceMonitorStub) Close() error {
return os.ErrInvalid return os.ErrInvalid
} }
func (s *interfaceMonitorStub) DefaultInterface() *control.Interface { func (s *interfaceMonitorStub) DefaultInterfaceName(destination netip.Addr) string {
return nil return ""
}
func (s *interfaceMonitorStub) DefaultInterfaceIndex(destination netip.Addr) int {
return -1
}
func (s *interfaceMonitorStub) DefaultInterface(destination netip.Addr) (string, int) {
return "", -1
} }
func (s *interfaceMonitorStub) OverrideAndroidVPN() bool { func (s *interfaceMonitorStub) OverrideAndroidVPN() bool {
@@ -126,8 +137,7 @@ func (s *interfaceMonitorStub) RegisterCallback(callback tun.DefaultInterfaceUpd
func (s *interfaceMonitorStub) UnregisterCallback(element *list.Element[tun.DefaultInterfaceUpdateCallback]) { func (s *interfaceMonitorStub) UnregisterCallback(element *list.Element[tun.DefaultInterfaceUpdateCallback]) {
} }
func (s *platformInterfaceStub) SendNotification(notification *platform.Notification) error { func (s *platformInterfaceStub) OpenURL(url string) {
return nil
} }
func FormatConfig(configContent string) (string, error) { func FormatConfig(configContent string) (string, error) {

View File

@@ -4,28 +4,27 @@ import (
"sync" "sync"
"github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing/common"
) )
var _ deprecated.Manager = (*deprecatedManager)(nil) var _ deprecated.Manager = (*deprecatedManager)(nil)
type deprecatedManager struct { type deprecatedManager struct {
access sync.Mutex access sync.Mutex
notes []deprecated.Note features []deprecated.Note
} }
func (m *deprecatedManager) ReportDeprecated(feature deprecated.Note) { func (m *deprecatedManager) ReportDeprecated(feature deprecated.Note) {
m.access.Lock() m.access.Lock()
defer m.access.Unlock() defer m.access.Unlock()
m.notes = common.Uniq(append(m.notes, feature)) m.features = append(m.features, feature)
} }
func (m *deprecatedManager) Get() []deprecated.Note { func (m *deprecatedManager) Get() []deprecated.Note {
m.access.Lock() m.access.Lock()
defer m.access.Unlock() defer m.access.Unlock()
notes := m.notes features := m.features
m.notes = nil m.features = nil
return notes return features
} }
var _ = deprecated.Note(DeprecatedNote{}) var _ = deprecated.Note(DeprecatedNote{})

View File

@@ -1,11 +0,0 @@
//go:build !unix
package libbox
import (
"net"
)
func linkFlags(rawFlags uint32) net.Flags {
panic("stub!")
}

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