add
This commit is contained in:
+592
@@ -0,0 +1,592 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/alireza0/s-ui/util/common"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/certificate"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport/local"
|
||||
"github.com/sagernet/sing-box/experimental"
|
||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/protocol/direct"
|
||||
"github.com/sagernet/sing-box/route"
|
||||
sbCommon "github.com/sagernet/sing/common"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/pause"
|
||||
)
|
||||
|
||||
var _ adapter.SimpleLifecycle = (*Box)(nil)
|
||||
|
||||
type Box struct {
|
||||
createdAt time.Time
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
network *route.NetworkManager
|
||||
endpoint *endpoint.Manager
|
||||
inbound *inbound.Manager
|
||||
outbound *outbound.Manager
|
||||
service *boxService.Manager
|
||||
dnsTransport *dns.TransportManager
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
internalService []adapter.LifecycleService
|
||||
statsTracker *StatsTracker
|
||||
connTracker *ConnTracker
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
option.Options
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
func Context(
|
||||
ctx context.Context,
|
||||
inboundRegistry adapter.InboundRegistry,
|
||||
outboundRegistry adapter.OutboundRegistry,
|
||||
endpointRegistry adapter.EndpointRegistry,
|
||||
dnsTransportRegistry adapter.DNSTransportRegistry,
|
||||
serviceRegistry adapter.ServiceRegistry,
|
||||
) context.Context {
|
||||
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
|
||||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.InboundOptionsRegistry](ctx, inboundRegistry)
|
||||
ctx = service.ContextWith[adapter.InboundRegistry](ctx, inboundRegistry)
|
||||
}
|
||||
if service.FromContext[option.OutboundOptionsRegistry](ctx) == nil ||
|
||||
service.FromContext[adapter.OutboundRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.OutboundOptionsRegistry](ctx, outboundRegistry)
|
||||
ctx = service.ContextWith[adapter.OutboundRegistry](ctx, outboundRegistry)
|
||||
}
|
||||
if service.FromContext[option.EndpointOptionsRegistry](ctx) == nil ||
|
||||
service.FromContext[adapter.EndpointRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry)
|
||||
ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry)
|
||||
}
|
||||
if service.FromContext[adapter.DNSTransportRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
|
||||
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
|
||||
}
|
||||
if service.FromContext[adapter.ServiceRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
|
||||
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func NewBox(options Options) (*Box, error) {
|
||||
var err error
|
||||
createdAt := time.Now()
|
||||
ctx := options.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
ctx = service.ContextWithDefaultRegistry(ctx)
|
||||
|
||||
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
|
||||
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
|
||||
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
||||
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
|
||||
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
|
||||
|
||||
if endpointRegistry == nil {
|
||||
return nil, common.NewError("missing endpoint registry in context")
|
||||
}
|
||||
if inboundRegistry == nil {
|
||||
return nil, common.NewError("missing inbound registry in context")
|
||||
}
|
||||
if outboundRegistry == nil {
|
||||
return nil, common.NewError("missing outbound registry in context")
|
||||
}
|
||||
if dnsTransportRegistry == nil {
|
||||
return nil, common.NewError("missing DNS transport registry in context")
|
||||
}
|
||||
if serviceRegistry == nil {
|
||||
return nil, common.NewError("missing service registry in context")
|
||||
}
|
||||
|
||||
ctx = pause.WithDefaultManager(ctx)
|
||||
experimentalOptions := sbCommon.PtrValueOrDefault(options.Experimental)
|
||||
var needCacheFile bool
|
||||
var needClashAPI bool
|
||||
var needV2RayAPI bool
|
||||
if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled {
|
||||
needCacheFile = true
|
||||
}
|
||||
if experimentalOptions.ClashAPI != nil {
|
||||
needClashAPI = true
|
||||
}
|
||||
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
||||
needV2RayAPI = true
|
||||
}
|
||||
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
|
||||
var defaultLogWriter io.Writer
|
||||
if platformInterface != nil {
|
||||
defaultLogWriter = io.Discard
|
||||
}
|
||||
var logFactory log.Factory
|
||||
logFactory, err = NewFactory(log.Options{
|
||||
Context: ctx,
|
||||
Options: sbCommon.PtrValueOrDefault(options.Log),
|
||||
DefaultWriter: defaultLogWriter,
|
||||
BaseTime: createdAt,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, common.NewError("create log factory", err)
|
||||
}
|
||||
factory = logFactory
|
||||
|
||||
var internalServices []adapter.LifecycleService
|
||||
certificateOptions := sbCommon.PtrValueOrDefault(options.Certificate)
|
||||
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||
len(certificateOptions.Certificate) > 0 ||
|
||||
len(certificateOptions.CertificatePath) > 0 ||
|
||||
len(certificateOptions.CertificateDirectoryPath) > 0 {
|
||||
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||
internalServices = append(internalServices, certificateStore)
|
||||
}
|
||||
|
||||
routeOptions := sbCommon.PtrValueOrDefault(options.Route)
|
||||
dnsOptions := sbCommon.PtrValueOrDefault(options.DNS)
|
||||
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
|
||||
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
||||
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
|
||||
|
||||
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
||||
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
||||
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
||||
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
||||
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
||||
|
||||
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||
|
||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
||||
if err != nil {
|
||||
return nil, common.NewError("initialize network manager", err)
|
||||
}
|
||||
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
||||
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
||||
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
||||
service.MustRegister[adapter.Router](ctx, router)
|
||||
err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet)
|
||||
if err != nil {
|
||||
return nil, common.NewError("initialize router", err)
|
||||
}
|
||||
for i, transportOptions := range dnsOptions.Servers {
|
||||
var tag string
|
||||
if transportOptions.Tag != "" {
|
||||
tag = transportOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = dnsTransportManager.Create(
|
||||
ctx,
|
||||
logFactory.NewLogger(F.ToString("dns/", transportOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
transportOptions.Type,
|
||||
transportOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, common.NewError("initialize DNS server[", i, "]", err)
|
||||
}
|
||||
}
|
||||
err = dnsRouter.Initialize(dnsOptions.Rules)
|
||||
if err != nil {
|
||||
return nil, common.NewError("initialize dns router", err)
|
||||
}
|
||||
for i, endpointOptions := range options.Endpoints {
|
||||
var tag string
|
||||
if endpointOptions.Tag != "" {
|
||||
tag = endpointOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = endpointManager.Create(
|
||||
ctx,
|
||||
router,
|
||||
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
endpointOptions.Type,
|
||||
endpointOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, common.NewError("initialize endpoint["+F.ToString(i)+"] "+tag, err)
|
||||
}
|
||||
}
|
||||
for i, inboundOptions := range options.Inbounds {
|
||||
var tag string
|
||||
if inboundOptions.Tag != "" {
|
||||
tag = inboundOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = inboundManager.Create(
|
||||
ctx,
|
||||
router,
|
||||
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
inboundOptions.Type,
|
||||
inboundOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, common.NewError("initialize inbound[", i, "] ", tag, err)
|
||||
}
|
||||
}
|
||||
for i, outboundOptions := range options.Outbounds {
|
||||
var tag string
|
||||
if outboundOptions.Tag != "" {
|
||||
tag = outboundOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
outboundCtx := ctx
|
||||
if tag != "" {
|
||||
// TODO: remove this
|
||||
outboundCtx = adapter.WithContext(outboundCtx, &adapter.InboundContext{
|
||||
Outbound: tag,
|
||||
})
|
||||
}
|
||||
err = outboundManager.Create(
|
||||
outboundCtx,
|
||||
router,
|
||||
logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
outboundOptions.Type,
|
||||
outboundOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, common.NewError("initialize outbound["+F.ToString(i)+"] "+tag, err)
|
||||
}
|
||||
}
|
||||
for i, serviceOptions := range options.Services {
|
||||
var tag string
|
||||
if serviceOptions.Tag != "" {
|
||||
tag = serviceOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = serviceManager.Create(
|
||||
ctx,
|
||||
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
serviceOptions.Type,
|
||||
serviceOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, common.NewError("initialize service["+F.ToString(i)+"]"+tag, err)
|
||||
}
|
||||
}
|
||||
outboundManager.Initialize(func() (adapter.Outbound, error) {
|
||||
return direct.NewOutbound(
|
||||
ctx,
|
||||
router,
|
||||
logFactory.NewLogger("outbound/direct"),
|
||||
"direct",
|
||||
option.DirectOutboundOptions{},
|
||||
)
|
||||
})
|
||||
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
|
||||
return local.NewTransport(
|
||||
ctx,
|
||||
logFactory.NewLogger("dns/local"),
|
||||
"local",
|
||||
option.LocalDNSServerOptions{},
|
||||
)
|
||||
})
|
||||
if platformInterface != nil {
|
||||
err = platformInterface.Initialize(networkManager)
|
||||
if err != nil {
|
||||
return nil, common.NewError("initialize platform interface", err)
|
||||
}
|
||||
}
|
||||
statsTracker := NewStatsTracker()
|
||||
connTracker := NewConnTracker()
|
||||
router.AppendTracker(statsTracker)
|
||||
router.AppendTracker(connTracker)
|
||||
|
||||
if needCacheFile {
|
||||
cacheFile := cachefile.New(ctx, sbCommon.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||
internalServices = append(internalServices, cacheFile)
|
||||
}
|
||||
if needClashAPI {
|
||||
clashAPIOptions := sbCommon.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
||||
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
|
||||
clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), clashAPIOptions)
|
||||
if err != nil {
|
||||
return nil, common.NewError(err, "create clash-server")
|
||||
}
|
||||
router.AppendTracker(clashServer)
|
||||
service.MustRegister[adapter.ClashServer](ctx, clashServer)
|
||||
internalServices = append(internalServices, clashServer)
|
||||
}
|
||||
if needV2RayAPI {
|
||||
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), sbCommon.PtrValueOrDefault(experimentalOptions.V2RayAPI))
|
||||
if err != nil {
|
||||
return nil, common.NewError(err, "create v2ray-server")
|
||||
}
|
||||
if v2rayServer.StatsService() != nil {
|
||||
router.AppendTracker(v2rayServer.StatsService())
|
||||
internalServices = append(internalServices, v2rayServer)
|
||||
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
||||
}
|
||||
}
|
||||
ntpOptions := sbCommon.PtrValueOrDefault(options.NTP)
|
||||
if ntpOptions.Enabled {
|
||||
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions, ntpOptions.ServerIsDomain())
|
||||
if err != nil {
|
||||
return nil, common.NewError(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)
|
||||
internalServices = append(internalServices, adapter.NewLifecycleService(timeService, "ntp service"))
|
||||
}
|
||||
return &Box{
|
||||
network: networkManager,
|
||||
endpoint: endpointManager,
|
||||
inbound: inboundManager,
|
||||
outbound: outboundManager,
|
||||
dnsTransport: dnsTransportManager,
|
||||
service: serviceManager,
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
internalService: internalServices,
|
||||
statsTracker: statsTracker,
|
||||
connTracker: connTracker,
|
||||
done: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Box) PreStart() error {
|
||||
err := s.preStart()
|
||||
if err != nil {
|
||||
// TODO: remove catch error
|
||||
defer func() {
|
||||
v := recover()
|
||||
if v != nil {
|
||||
s.logger.Error(err.Error())
|
||||
s.logger.Error("panic on early close: " + fmt.Sprint(v))
|
||||
}
|
||||
}()
|
||||
s.Close()
|
||||
return err
|
||||
}
|
||||
s.logger.Info("sing-box pre-started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Box) Start() error {
|
||||
err := s.start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.logger.Info("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Box) preStart() error {
|
||||
monitor := taskmonitor.New(s.logger, C.StartTimeout)
|
||||
monitor.Start("start logger")
|
||||
err := s.logFactory.Start()
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return common.NewError(err, "start logger")
|
||||
}
|
||||
err = adapter.StartNamed(s.logger, adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Box) start() error {
|
||||
err := s.preStart()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.logger, adapter.StartStateStart, s.internalService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.endpoint, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.logger, adapter.StartStatePostStart, s.internalService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.logger, adapter.StartStateStarted, s.internalService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Box) Close() error {
|
||||
select {
|
||||
case <-s.done:
|
||||
return nil
|
||||
default:
|
||||
close(s.done)
|
||||
}
|
||||
var err error
|
||||
s.logger.Info("closing sing-box")
|
||||
for _, closeItem := range []struct {
|
||||
name string
|
||||
service adapter.Lifecycle
|
||||
}{
|
||||
{"service", s.service},
|
||||
{"endpoint", s.endpoint},
|
||||
{"inbound", s.inbound},
|
||||
{"outbound", s.outbound},
|
||||
{"router", s.router},
|
||||
{"connection", s.connection},
|
||||
{"dns-router", s.dnsRouter},
|
||||
{"dns-transport", s.dnsTransport},
|
||||
{"network", s.network},
|
||||
} {
|
||||
if closeItem.service == nil {
|
||||
continue
|
||||
}
|
||||
func() {
|
||||
defer func() {
|
||||
if v := recover(); v != nil {
|
||||
err = errors.Join(err, common.NewError(fmt.Errorf("panic: %v", v), "close "+closeItem.name))
|
||||
s.logger.Error("panic closing ", closeItem.name, ": ", v)
|
||||
}
|
||||
}()
|
||||
s.logger.Trace("close ", closeItem.name)
|
||||
startTime := time.Now()
|
||||
closeErr := closeItem.service.Close()
|
||||
if closeErr != nil {
|
||||
closeErr = common.NewError(closeErr, "close "+closeItem.name)
|
||||
}
|
||||
err = errors.Join(err, closeErr)
|
||||
s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}()
|
||||
}
|
||||
for _, lifecycleService := range s.internalService {
|
||||
if lifecycleService == nil {
|
||||
continue
|
||||
}
|
||||
func() {
|
||||
defer func() {
|
||||
if v := recover(); v != nil {
|
||||
err = errors.Join(err, common.NewError(fmt.Errorf("panic: %v", v), "close "+lifecycleService.Name()))
|
||||
s.logger.Error("panic closing ", lifecycleService.Name(), ": ", v)
|
||||
}
|
||||
}()
|
||||
s.logger.Trace("close ", lifecycleService.Name())
|
||||
startTime := time.Now()
|
||||
closeErr := lifecycleService.Close()
|
||||
if closeErr != nil {
|
||||
closeErr = common.NewError(closeErr, "close "+lifecycleService.Name())
|
||||
}
|
||||
err = errors.Join(err, closeErr)
|
||||
s.logger.Trace("close ", lifecycleService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}()
|
||||
}
|
||||
s.logger.Trace("close logger")
|
||||
startTime := time.Now()
|
||||
closeErr := s.logFactory.Close()
|
||||
if closeErr != nil {
|
||||
closeErr = common.NewError(closeErr, "close logger")
|
||||
}
|
||||
err = errors.Join(err, closeErr)
|
||||
s.logger.Trace("close logger completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
s.logger.Info("sing-box closed (live time: ", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
if s.statsTracker != nil {
|
||||
s.statsTracker.Reset()
|
||||
}
|
||||
if s.connTracker != nil {
|
||||
s.connTracker.Reset()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Box) Uptime() uint32 {
|
||||
return uint32(time.Since(s.createdAt).Seconds())
|
||||
}
|
||||
|
||||
func (s *Box) Network() adapter.NetworkManager {
|
||||
return s.network
|
||||
}
|
||||
|
||||
func (s *Box) Router() adapter.Router {
|
||||
return s.router
|
||||
}
|
||||
|
||||
func (s *Box) Inbound() adapter.InboundManager {
|
||||
return s.inbound
|
||||
}
|
||||
|
||||
func (s *Box) Outbound() adapter.OutboundManager {
|
||||
return s.outbound
|
||||
}
|
||||
|
||||
func (s *Box) Endpoint() adapter.EndpointManager {
|
||||
return s.endpoint
|
||||
}
|
||||
|
||||
func (s *Box) StatsTracker() *StatsTracker {
|
||||
return s.statsTracker
|
||||
}
|
||||
|
||||
func (s *Box) ConnTracker() *ConnTracker {
|
||||
return s.connTracker
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"github.com/alireza0/s-ui/logger"
|
||||
"github.com/alireza0/s-ui/util/common"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
func (c *Core) AddInbound(config []byte) error {
|
||||
if !c.isRunning {
|
||||
return common.NewError("sing-box is not running")
|
||||
}
|
||||
var err error
|
||||
var inbound_config option.Inbound
|
||||
err = inbound_config.UnmarshalJSONContext(c.GetCtx(), config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = inbound_manager.Create(
|
||||
c.GetCtx(),
|
||||
router,
|
||||
factory.NewLogger("inbound/"+inbound_config.Type+"["+inbound_config.Tag+"]"),
|
||||
inbound_config.Tag,
|
||||
inbound_config.Type,
|
||||
inbound_config.Options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Core) RemoveInbound(tag string) error {
|
||||
if !c.isRunning {
|
||||
return common.NewError("sing-box is not running")
|
||||
}
|
||||
logger.Info("remove inbound: ", tag)
|
||||
return inbound_manager.Remove(tag)
|
||||
}
|
||||
|
||||
func (c *Core) AddOutbound(config []byte) error {
|
||||
if !c.isRunning {
|
||||
return common.NewError("sing-box is not running")
|
||||
}
|
||||
var err error
|
||||
var outbound_config option.Outbound
|
||||
|
||||
err = outbound_config.UnmarshalJSONContext(c.GetCtx(), config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outboundCtx := adapter.WithContext(c.GetCtx(), &adapter.InboundContext{
|
||||
Outbound: outbound_config.Tag,
|
||||
})
|
||||
|
||||
err = outbound_manager.Create(
|
||||
outboundCtx,
|
||||
router,
|
||||
factory.NewLogger("outbound/"+outbound_config.Type+"["+outbound_config.Tag+"]"),
|
||||
outbound_config.Tag,
|
||||
outbound_config.Type,
|
||||
outbound_config.Options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Core) RemoveOutbound(tag string) error {
|
||||
if !c.isRunning {
|
||||
return common.NewError("sing-box is not running")
|
||||
}
|
||||
logger.Info("remove outbound: ", tag)
|
||||
return outbound_manager.Remove(tag)
|
||||
}
|
||||
|
||||
func (c *Core) AddEndpoint(config []byte) error {
|
||||
if !c.isRunning {
|
||||
return common.NewError("sing-box is not running")
|
||||
}
|
||||
var err error
|
||||
var endpoint_config option.Endpoint
|
||||
|
||||
err = endpoint_config.UnmarshalJSONContext(c.GetCtx(), config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = endpoint_manager.Create(
|
||||
c.GetCtx(),
|
||||
router,
|
||||
factory.NewLogger("endpoint/"+endpoint_config.Type+"["+endpoint_config.Tag+"]"),
|
||||
endpoint_config.Tag,
|
||||
endpoint_config.Type,
|
||||
endpoint_config.Options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Core) RemoveEndpoint(tag string) error {
|
||||
if !c.isRunning {
|
||||
return common.NewError("sing-box is not running")
|
||||
}
|
||||
logger.Info("remove endpoint: ", tag)
|
||||
return endpoint_manager.Remove(tag)
|
||||
}
|
||||
|
||||
func (c *Core) AddService(config []byte) error {
|
||||
if !c.isRunning {
|
||||
return common.NewError("sing-box is not running")
|
||||
}
|
||||
var err error
|
||||
var srv_config option.Service
|
||||
|
||||
err = srv_config.UnmarshalJSONContext(c.GetCtx(), config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = service_manager.Create(
|
||||
c.GetCtx(),
|
||||
factory.NewLogger("service/"+srv_config.Type+"["+srv_config.Tag+"]"),
|
||||
srv_config.Tag,
|
||||
srv_config.Type,
|
||||
srv_config.Options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Core) RemoveService(tag string) error {
|
||||
if !c.isRunning {
|
||||
return common.NewError("sing-box is not running")
|
||||
}
|
||||
logger.Info("remove service: ", tag)
|
||||
return service_manager.Remove(tag)
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
suiLog "github.com/alireza0/s-ui/logger"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/observable"
|
||||
"github.com/sagernet/sing/service/filemanager"
|
||||
)
|
||||
|
||||
type PlatformWriter struct{}
|
||||
|
||||
func (p PlatformWriter) DisableColors() bool {
|
||||
return true
|
||||
}
|
||||
func (p PlatformWriter) WriteMessage(level log.Level, message string) {
|
||||
switch level {
|
||||
case log.LevelInfo:
|
||||
suiLog.Info(message)
|
||||
case log.LevelWarn:
|
||||
suiLog.Warning(message)
|
||||
case log.LevelPanic:
|
||||
case log.LevelFatal:
|
||||
case log.LevelError:
|
||||
suiLog.Error(message)
|
||||
default:
|
||||
suiLog.Debug(message)
|
||||
}
|
||||
}
|
||||
|
||||
func NewFactory(options log.Options) (log.Factory, error) {
|
||||
logOptions := options.Options
|
||||
|
||||
if logOptions.Disabled {
|
||||
return log.NewNOPFactory(), nil
|
||||
}
|
||||
|
||||
var logWriter io.Writer
|
||||
var logFilePath string
|
||||
|
||||
switch logOptions.Output {
|
||||
case "":
|
||||
logWriter = options.DefaultWriter
|
||||
if logWriter == nil {
|
||||
logWriter = os.Stderr
|
||||
}
|
||||
case "stderr":
|
||||
logWriter = os.Stderr
|
||||
case "stdout":
|
||||
logWriter = os.Stdout
|
||||
default:
|
||||
logFilePath = logOptions.Output
|
||||
}
|
||||
logFormatter := log.Formatter{
|
||||
BaseTime: options.BaseTime,
|
||||
DisableColors: logOptions.DisableColor || logFilePath != "",
|
||||
DisableTimestamp: !logOptions.Timestamp && logFilePath != "",
|
||||
FullTimestamp: logOptions.Timestamp,
|
||||
TimestampFormat: "-0700 2006-01-02 15:04:05",
|
||||
}
|
||||
factory := NewDefaultFactory(
|
||||
options.Context,
|
||||
logFormatter,
|
||||
logWriter,
|
||||
logFilePath,
|
||||
)
|
||||
if logOptions.Level != "" {
|
||||
logLevel, err := log.ParseLevel(logOptions.Level)
|
||||
if err != nil {
|
||||
return nil, common.Error("parse log level", err)
|
||||
}
|
||||
factory.SetLevel(logLevel)
|
||||
} else {
|
||||
factory.SetLevel(log.LevelTrace)
|
||||
}
|
||||
return factory, nil
|
||||
}
|
||||
|
||||
var _ log.Factory = (*defaultFactory)(nil)
|
||||
|
||||
type defaultFactory struct {
|
||||
ctx context.Context
|
||||
formatter log.Formatter
|
||||
writer io.Writer
|
||||
file *os.File
|
||||
filePath string
|
||||
level log.Level
|
||||
subscriber *observable.Subscriber[log.Entry]
|
||||
observer *observable.Observer[log.Entry]
|
||||
}
|
||||
|
||||
func NewDefaultFactory(
|
||||
ctx context.Context,
|
||||
formatter log.Formatter,
|
||||
writer io.Writer,
|
||||
filePath string,
|
||||
) log.ObservableFactory {
|
||||
factory := &defaultFactory{
|
||||
ctx: ctx,
|
||||
formatter: formatter,
|
||||
writer: writer,
|
||||
filePath: filePath,
|
||||
level: log.LevelTrace,
|
||||
subscriber: observable.NewSubscriber[log.Entry](128),
|
||||
}
|
||||
return factory
|
||||
}
|
||||
|
||||
func (f *defaultFactory) Start() error {
|
||||
if f.filePath != "" {
|
||||
logFile, err := filemanager.OpenFile(f.ctx, f.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.writer = logFile
|
||||
f.file = logFile
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *defaultFactory) Close() error {
|
||||
return common.Close(
|
||||
common.PtrOrNil(f.file),
|
||||
f.subscriber,
|
||||
)
|
||||
}
|
||||
|
||||
func (f *defaultFactory) Level() log.Level {
|
||||
return f.level
|
||||
}
|
||||
|
||||
func (f *defaultFactory) SetLevel(level log.Level) {
|
||||
f.level = level
|
||||
}
|
||||
|
||||
func (f *defaultFactory) Logger() log.ContextLogger {
|
||||
return f.NewLogger("")
|
||||
}
|
||||
|
||||
func (f *defaultFactory) NewLogger(tag string) log.ContextLogger {
|
||||
return &observableLogger{f, tag}
|
||||
}
|
||||
|
||||
func (f *defaultFactory) Subscribe() (subscription observable.Subscription[log.Entry], done <-chan struct{}, err error) {
|
||||
return f.observer.Subscribe()
|
||||
}
|
||||
|
||||
func (f *defaultFactory) UnSubscribe(sub observable.Subscription[log.Entry]) {
|
||||
f.observer.UnSubscribe(sub)
|
||||
}
|
||||
|
||||
type observableLogger struct {
|
||||
*defaultFactory
|
||||
tag string
|
||||
}
|
||||
|
||||
func (l *observableLogger) Log(ctx context.Context, level log.Level, args []any) {
|
||||
level = log.OverrideLevelFromContext(level, ctx)
|
||||
if level > l.level {
|
||||
return
|
||||
}
|
||||
msg := F.ToString(args...)
|
||||
switch level {
|
||||
case log.LevelInfo:
|
||||
suiLog.Info(l.tag, msg)
|
||||
case log.LevelWarn:
|
||||
suiLog.Warning(l.tag, msg)
|
||||
case log.LevelPanic:
|
||||
case log.LevelFatal:
|
||||
case log.LevelError:
|
||||
suiLog.Error(l.tag, msg)
|
||||
default:
|
||||
suiLog.Debug(l.tag, msg)
|
||||
}
|
||||
if (l.filePath != "" || l.writer != os.Stderr) && l.writer != nil {
|
||||
message := l.formatter.Format(ctx, level, l.tag, msg, time.Now())
|
||||
l.writer.Write([]byte(message))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *observableLogger) Trace(args ...any) {
|
||||
l.TraceContext(context.Background(), args...)
|
||||
}
|
||||
|
||||
func (l *observableLogger) Debug(args ...any) {
|
||||
l.DebugContext(context.Background(), args...)
|
||||
}
|
||||
|
||||
func (l *observableLogger) Info(args ...any) {
|
||||
l.InfoContext(context.Background(), args...)
|
||||
}
|
||||
|
||||
func (l *observableLogger) Warn(args ...any) {
|
||||
l.WarnContext(context.Background(), args...)
|
||||
}
|
||||
|
||||
func (l *observableLogger) Error(args ...any) {
|
||||
l.ErrorContext(context.Background(), args...)
|
||||
}
|
||||
|
||||
func (l *observableLogger) Fatal(args ...any) {
|
||||
l.FatalContext(context.Background(), args...)
|
||||
}
|
||||
|
||||
func (l *observableLogger) Panic(args ...any) {
|
||||
l.PanicContext(context.Background(), args...)
|
||||
}
|
||||
|
||||
func (l *observableLogger) TraceContext(ctx context.Context, args ...any) {
|
||||
l.Log(ctx, log.LevelTrace, args)
|
||||
}
|
||||
|
||||
func (l *observableLogger) DebugContext(ctx context.Context, args ...any) {
|
||||
l.Log(ctx, log.LevelDebug, args)
|
||||
}
|
||||
|
||||
func (l *observableLogger) InfoContext(ctx context.Context, args ...any) {
|
||||
l.Log(ctx, log.LevelInfo, args)
|
||||
}
|
||||
|
||||
func (l *observableLogger) WarnContext(ctx context.Context, args ...any) {
|
||||
l.Log(ctx, log.LevelWarn, args)
|
||||
}
|
||||
|
||||
func (l *observableLogger) ErrorContext(ctx context.Context, args ...any) {
|
||||
l.Log(ctx, log.LevelError, args)
|
||||
}
|
||||
|
||||
func (l *observableLogger) FatalContext(ctx context.Context, args ...any) {
|
||||
l.Log(ctx, log.LevelFatal, args)
|
||||
}
|
||||
|
||||
func (l *observableLogger) PanicContext(ctx context.Context, args ...any) {
|
||||
l.Log(ctx, log.LevelPanic, args)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/alireza0/s-ui/logger"
|
||||
|
||||
sb "github.com/sagernet/sing-box"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
_ "github.com/sagernet/sing-box/experimental/clashapi"
|
||||
_ "github.com/sagernet/sing-box/experimental/v2rayapi"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
_ "github.com/sagernet/sing-box/transport/v2rayquic"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
var (
|
||||
globalCtx context.Context
|
||||
inbound_manager adapter.InboundManager
|
||||
outbound_manager adapter.OutboundManager
|
||||
service_manager adapter.ServiceManager
|
||||
endpoint_manager adapter.EndpointManager
|
||||
router adapter.Router
|
||||
factory log.Factory
|
||||
)
|
||||
|
||||
type Core struct {
|
||||
isRunning bool
|
||||
instance *Box
|
||||
}
|
||||
|
||||
func NewCore() *Core {
|
||||
globalCtx = context.Background()
|
||||
globalCtx = sb.Context(globalCtx, InboundRegistry(), OutboundRegistry(), EndpointRegistry(), DNSTransportRegistry(), ServiceRegistry())
|
||||
return &Core{
|
||||
isRunning: false,
|
||||
instance: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Core) GetCtx() context.Context {
|
||||
return globalCtx
|
||||
}
|
||||
|
||||
func (c *Core) GetInstance() *Box {
|
||||
return c.instance
|
||||
}
|
||||
|
||||
func (c *Core) Start(sbConfig []byte) error {
|
||||
var opt option.Options
|
||||
err := opt.UnmarshalJSONContext(globalCtx, sbConfig)
|
||||
if err != nil {
|
||||
logger.Error("Unmarshal config err:", err.Error())
|
||||
}
|
||||
|
||||
c.instance, err = NewBox(Options{
|
||||
Context: globalCtx,
|
||||
Options: opt,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.instance.Start()
|
||||
if err != nil {
|
||||
_ = c.instance.Close()
|
||||
c.instance = nil
|
||||
return err
|
||||
}
|
||||
|
||||
globalCtx = service.ContextWith(globalCtx, c)
|
||||
inbound_manager = service.FromContext[adapter.InboundManager](globalCtx)
|
||||
outbound_manager = service.FromContext[adapter.OutboundManager](globalCtx)
|
||||
service_manager = service.FromContext[adapter.ServiceManager](globalCtx)
|
||||
endpoint_manager = service.FromContext[adapter.EndpointManager](globalCtx)
|
||||
router = service.FromContext[adapter.Router](globalCtx)
|
||||
|
||||
c.isRunning = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Core) Stop() error {
|
||||
c.isRunning = false
|
||||
if c.instance == nil {
|
||||
return nil
|
||||
}
|
||||
err := c.instance.Close()
|
||||
c.instance = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Core) IsRunning() bool {
|
||||
return c.isRunning
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
urltest "github.com/sagernet/sing-box/common/urltest"
|
||||
)
|
||||
|
||||
const checkTimeout = 15 * time.Second
|
||||
|
||||
type CheckOutboundResult struct {
|
||||
OK bool
|
||||
Delay uint16
|
||||
Error string
|
||||
}
|
||||
|
||||
func CheckOutbound(ctx context.Context, tag string, link string) (result CheckOutboundResult) {
|
||||
if outbound_manager == nil {
|
||||
result.Error = "core not running"
|
||||
return result
|
||||
}
|
||||
ob, ok := outbound_manager.Outbound(tag)
|
||||
if !ok {
|
||||
result.Error = "outbound not found"
|
||||
return result
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, checkTimeout)
|
||||
defer cancel()
|
||||
|
||||
delay, err := urltest.URLTest(ctx, link, ob)
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
return result
|
||||
}
|
||||
result.OK = true
|
||||
result.Delay = delay
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport"
|
||||
"github.com/sagernet/sing-box/dns/transport/dhcp"
|
||||
"github.com/sagernet/sing-box/dns/transport/fakeip"
|
||||
"github.com/sagernet/sing-box/dns/transport/hosts"
|
||||
"github.com/sagernet/sing-box/dns/transport/local"
|
||||
"github.com/sagernet/sing-box/dns/transport/quic"
|
||||
"github.com/sagernet/sing-box/protocol/anytls"
|
||||
"github.com/sagernet/sing-box/protocol/block"
|
||||
"github.com/sagernet/sing-box/protocol/direct"
|
||||
"github.com/sagernet/sing-box/protocol/group"
|
||||
"github.com/sagernet/sing-box/protocol/http"
|
||||
"github.com/sagernet/sing-box/protocol/hysteria"
|
||||
"github.com/sagernet/sing-box/protocol/hysteria2"
|
||||
"github.com/sagernet/sing-box/protocol/mixed"
|
||||
"github.com/sagernet/sing-box/protocol/naive"
|
||||
_ "github.com/sagernet/sing-box/protocol/naive/quic"
|
||||
"github.com/sagernet/sing-box/protocol/redirect"
|
||||
"github.com/sagernet/sing-box/protocol/shadowsocks"
|
||||
"github.com/sagernet/sing-box/protocol/shadowtls"
|
||||
"github.com/sagernet/sing-box/protocol/socks"
|
||||
"github.com/sagernet/sing-box/protocol/ssh"
|
||||
"github.com/sagernet/sing-box/protocol/tor"
|
||||
"github.com/sagernet/sing-box/protocol/trojan"
|
||||
"github.com/sagernet/sing-box/protocol/tuic"
|
||||
"github.com/sagernet/sing-box/protocol/tun"
|
||||
"github.com/sagernet/sing-box/protocol/vless"
|
||||
"github.com/sagernet/sing-box/protocol/vmess"
|
||||
"github.com/sagernet/sing-box/protocol/wireguard"
|
||||
"github.com/sagernet/sing-box/service/ccm"
|
||||
"github.com/sagernet/sing-box/service/ocm"
|
||||
"github.com/sagernet/sing-box/service/resolved"
|
||||
"github.com/sagernet/sing-box/service/ssmapi"
|
||||
_ "github.com/sagernet/sing-box/transport/v2rayquic"
|
||||
)
|
||||
|
||||
func InboundRegistry() *inbound.Registry {
|
||||
registry := inbound.NewRegistry()
|
||||
|
||||
tun.RegisterInbound(registry)
|
||||
redirect.RegisterRedirect(registry)
|
||||
redirect.RegisterTProxy(registry)
|
||||
direct.RegisterInbound(registry)
|
||||
|
||||
socks.RegisterInbound(registry)
|
||||
http.RegisterInbound(registry)
|
||||
mixed.RegisterInbound(registry)
|
||||
|
||||
shadowsocks.RegisterInbound(registry)
|
||||
vmess.RegisterInbound(registry)
|
||||
trojan.RegisterInbound(registry)
|
||||
naive.RegisterInbound(registry)
|
||||
shadowtls.RegisterInbound(registry)
|
||||
vless.RegisterInbound(registry)
|
||||
anytls.RegisterInbound(registry)
|
||||
|
||||
hysteria.RegisterInbound(registry)
|
||||
tuic.RegisterInbound(registry)
|
||||
hysteria2.RegisterInbound(registry)
|
||||
|
||||
return registry
|
||||
}
|
||||
|
||||
func OutboundRegistry() *outbound.Registry {
|
||||
registry := outbound.NewRegistry()
|
||||
|
||||
direct.RegisterOutbound(registry)
|
||||
|
||||
block.RegisterOutbound(registry)
|
||||
|
||||
group.RegisterSelector(registry)
|
||||
group.RegisterURLTest(registry)
|
||||
|
||||
socks.RegisterOutbound(registry)
|
||||
http.RegisterOutbound(registry)
|
||||
shadowsocks.RegisterOutbound(registry)
|
||||
vmess.RegisterOutbound(registry)
|
||||
trojan.RegisterOutbound(registry)
|
||||
registerNaiveOutbound(registry)
|
||||
tor.RegisterOutbound(registry)
|
||||
ssh.RegisterOutbound(registry)
|
||||
shadowtls.RegisterOutbound(registry)
|
||||
vless.RegisterOutbound(registry)
|
||||
anytls.RegisterOutbound(registry)
|
||||
|
||||
hysteria.RegisterOutbound(registry)
|
||||
tuic.RegisterOutbound(registry)
|
||||
hysteria2.RegisterOutbound(registry)
|
||||
|
||||
return registry
|
||||
}
|
||||
|
||||
func EndpointRegistry() *endpoint.Registry {
|
||||
registry := endpoint.NewRegistry()
|
||||
|
||||
wireguard.RegisterEndpoint(registry)
|
||||
registerTailscaleEndpoint(registry)
|
||||
|
||||
return registry
|
||||
}
|
||||
|
||||
func DNSTransportRegistry() *dns.TransportRegistry {
|
||||
registry := dns.NewTransportRegistry()
|
||||
|
||||
transport.RegisterTCP(registry)
|
||||
transport.RegisterUDP(registry)
|
||||
transport.RegisterTLS(registry)
|
||||
transport.RegisterHTTPS(registry)
|
||||
hosts.RegisterTransport(registry)
|
||||
local.RegisterTransport(registry)
|
||||
fakeip.RegisterTransport(registry)
|
||||
|
||||
quic.RegisterTransport(registry)
|
||||
quic.RegisterHTTP3Transport(registry)
|
||||
dhcp.RegisterTransport(registry)
|
||||
registerTailscaleTransport(registry)
|
||||
|
||||
return registry
|
||||
}
|
||||
|
||||
func ServiceRegistry() *service.Registry {
|
||||
registry := service.NewRegistry()
|
||||
|
||||
resolved.RegisterService(registry)
|
||||
ssmapi.RegisterService(registry)
|
||||
|
||||
registerDERPService(registry)
|
||||
ccm.RegisterService(registry)
|
||||
ocm.RegisterService(registry)
|
||||
|
||||
return registry
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//go:build with_naive_outbound
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/protocol/naive"
|
||||
)
|
||||
|
||||
func registerNaiveOutbound(registry *outbound.Registry) {
|
||||
naive.RegisterOutbound(registry)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//go:build !with_naive_outbound
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"github.com/alireza0/s-ui/logger"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
)
|
||||
|
||||
func registerNaiveOutbound(registry *outbound.Registry) {
|
||||
// naive outbound is disabled when built without with_naive_outbound tag
|
||||
logger.Error("naive outbound is disabled when built without with_naive_outbound tag")
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//go:build with_tailscale
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/protocol/tailscale"
|
||||
"github.com/sagernet/sing-box/service/derp"
|
||||
)
|
||||
|
||||
func registerTailscaleEndpoint(registry *endpoint.Registry) {
|
||||
tailscale.RegisterEndpoint(registry)
|
||||
}
|
||||
|
||||
func registerTailscaleTransport(registry *dns.TransportRegistry) {
|
||||
tailscale.RegistryTransport(registry)
|
||||
}
|
||||
|
||||
func registerDERPService(registry *service.Registry) {
|
||||
derp.Register(registry)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//go:build !with_tailscale
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/service"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func registerTailscaleEndpoint(registry *endpoint.Registry) {
|
||||
endpoint.Register[option.TailscaleEndpointOptions](registry, C.TypeTailscale, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TailscaleEndpointOptions) (adapter.Endpoint, error) {
|
||||
return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`)
|
||||
})
|
||||
}
|
||||
|
||||
func registerTailscaleTransport(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.TailscaleDNSServerOptions](registry, C.DNSTypeTailscale, func(ctx context.Context, logger log.ContextLogger, tag string, options option.TailscaleDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`)
|
||||
})
|
||||
}
|
||||
|
||||
func registerDERPService(registry *service.Registry) {
|
||||
service.Register[option.DERPServiceOptions](registry, C.TypeDERP, func(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPServiceOptions) (adapter.Service, error) {
|
||||
return nil, E.New(`DERP is not included in this build, rebuild with -tags with_tailscale`)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type ConnectionInfo struct {
|
||||
ID string
|
||||
Conn net.Conn
|
||||
PacketConn network.PacketConn
|
||||
Inbound string
|
||||
Type string // "tcp" or "udp"
|
||||
}
|
||||
|
||||
type ConnTracker struct {
|
||||
access sync.Mutex
|
||||
connections map[string]*ConnectionInfo
|
||||
}
|
||||
|
||||
func NewConnTracker() *ConnTracker {
|
||||
return &ConnTracker{
|
||||
connections: make(map[string]*ConnectionInfo),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ConnTracker) Reset() {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
for _, connInfo := range c.connections {
|
||||
if connInfo.Conn != nil {
|
||||
_ = connInfo.Conn.Close()
|
||||
}
|
||||
if connInfo.PacketConn != nil {
|
||||
_ = connInfo.PacketConn.Close()
|
||||
}
|
||||
}
|
||||
c.connections = make(map[string]*ConnectionInfo)
|
||||
}
|
||||
|
||||
func (c *ConnTracker) generateConnectionID() string {
|
||||
return uuid.Must(uuid.NewV4()).String()
|
||||
}
|
||||
|
||||
func (c *ConnTracker) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn {
|
||||
connID := c.generateConnectionID()
|
||||
connInfo := &ConnectionInfo{
|
||||
ID: connID,
|
||||
Conn: conn,
|
||||
Inbound: metadata.Inbound,
|
||||
Type: "tcp",
|
||||
}
|
||||
|
||||
c.trackConnection(connID, connInfo)
|
||||
|
||||
return c.createWrappedConn(conn, connID)
|
||||
}
|
||||
|
||||
func (c *ConnTracker) RoutedPacketConnection(ctx context.Context, conn network.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) network.PacketConn {
|
||||
connID := c.generateConnectionID()
|
||||
connInfo := &ConnectionInfo{
|
||||
ID: connID,
|
||||
PacketConn: conn,
|
||||
Inbound: metadata.Inbound,
|
||||
Type: "udp",
|
||||
}
|
||||
|
||||
c.trackConnection(connID, connInfo)
|
||||
|
||||
return c.createWrappedPacketConn(conn, connID)
|
||||
}
|
||||
|
||||
func (c *ConnTracker) CloseConnByInbound(inbound string) int {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
|
||||
closedCount := 0
|
||||
for connID, connInfo := range c.connections {
|
||||
if connInfo.Inbound == inbound {
|
||||
if connInfo.Conn != nil {
|
||||
connInfo.Conn.Close()
|
||||
}
|
||||
if connInfo.PacketConn != nil {
|
||||
connInfo.PacketConn.Close()
|
||||
}
|
||||
delete(c.connections, connID)
|
||||
closedCount++
|
||||
}
|
||||
}
|
||||
return closedCount
|
||||
}
|
||||
|
||||
func (c *ConnTracker) trackConnection(connID string, connInfo *ConnectionInfo) {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
c.connections[connID] = connInfo
|
||||
}
|
||||
|
||||
func (c *ConnTracker) untrackConnection(connID string) {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
delete(c.connections, connID)
|
||||
}
|
||||
|
||||
// shouldUntrackIOErr reports whether err indicates the connection is done (peer closed, reset, etc.).
|
||||
func shouldUntrackIOErr(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, io.EOF) {
|
||||
return true
|
||||
}
|
||||
var ne net.Error
|
||||
if errors.As(err, &ne) {
|
||||
return !ne.Temporary()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *ConnTracker) createWrappedConn(conn net.Conn, connID string) *wrappedConn {
|
||||
return &wrappedConn{
|
||||
Conn: conn,
|
||||
tracker: c,
|
||||
connID: connID,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ConnTracker) createWrappedPacketConn(conn network.PacketConn, connID string) *wrappedPacketConn {
|
||||
return &wrappedPacketConn{
|
||||
PacketConn: conn,
|
||||
tracker: c,
|
||||
connID: connID,
|
||||
}
|
||||
}
|
||||
|
||||
type wrappedConn struct {
|
||||
net.Conn
|
||||
tracker *ConnTracker
|
||||
connID string
|
||||
untrackOnce sync.Once
|
||||
}
|
||||
|
||||
func (w *wrappedConn) doUntrack() {
|
||||
w.untrackOnce.Do(func() {
|
||||
w.tracker.untrackConnection(w.connID)
|
||||
})
|
||||
}
|
||||
|
||||
func (w *wrappedConn) Read(b []byte) (int, error) {
|
||||
n, err := w.Conn.Read(b)
|
||||
if shouldUntrackIOErr(err) {
|
||||
w.doUntrack()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (w *wrappedConn) Write(b []byte) (int, error) {
|
||||
n, err := w.Conn.Write(b)
|
||||
if err != nil && shouldUntrackIOErr(err) {
|
||||
w.doUntrack()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (w *wrappedConn) Close() error {
|
||||
w.doUntrack()
|
||||
return w.Conn.Close()
|
||||
}
|
||||
|
||||
func (w *wrappedConn) Upstream() any {
|
||||
return w.Conn
|
||||
}
|
||||
|
||||
type wrappedPacketConn struct {
|
||||
network.PacketConn
|
||||
tracker *ConnTracker
|
||||
connID string
|
||||
untrackOnce sync.Once
|
||||
}
|
||||
|
||||
func (w *wrappedPacketConn) doUntrack() {
|
||||
w.untrackOnce.Do(func() {
|
||||
w.tracker.untrackConnection(w.connID)
|
||||
})
|
||||
}
|
||||
|
||||
func (w *wrappedPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||
dest, err := w.PacketConn.ReadPacket(buffer)
|
||||
if shouldUntrackIOErr(err) {
|
||||
w.doUntrack()
|
||||
}
|
||||
return dest, err
|
||||
}
|
||||
|
||||
func (w *wrappedPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||
err := w.PacketConn.WritePacket(buffer, destination)
|
||||
if err != nil && shouldUntrackIOErr(err) {
|
||||
w.doUntrack()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *wrappedPacketConn) Close() error {
|
||||
w.doUntrack()
|
||||
return w.PacketConn.Close()
|
||||
}
|
||||
|
||||
func (w *wrappedPacketConn) Upstream() any {
|
||||
return w.PacketConn
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/alireza0/s-ui/database/model"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing/common/atomic"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
"github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type Counter struct {
|
||||
read *atomic.Int64
|
||||
write *atomic.Int64
|
||||
}
|
||||
|
||||
type StatsTracker struct {
|
||||
access sync.Mutex
|
||||
inbounds map[string]Counter
|
||||
outbounds map[string]Counter
|
||||
users map[string]Counter
|
||||
}
|
||||
|
||||
func NewStatsTracker() *StatsTracker {
|
||||
return &StatsTracker{
|
||||
inbounds: make(map[string]Counter),
|
||||
outbounds: make(map[string]Counter),
|
||||
users: make(map[string]Counter),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *StatsTracker) Reset() {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
c.inbounds = make(map[string]Counter)
|
||||
c.outbounds = make(map[string]Counter)
|
||||
c.users = make(map[string]Counter)
|
||||
}
|
||||
|
||||
func (c *StatsTracker) getReadCounters(inbound string, outbound string, user string) ([]*atomic.Int64, []*atomic.Int64) {
|
||||
var readCounter []*atomic.Int64
|
||||
var writeCounter []*atomic.Int64
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
|
||||
if inbound != "" {
|
||||
readCounter = append(readCounter, c.loadOrCreateCounter(&c.inbounds, inbound).read)
|
||||
writeCounter = append(writeCounter, c.inbounds[inbound].write)
|
||||
}
|
||||
if outbound != "" {
|
||||
readCounter = append(readCounter, c.loadOrCreateCounter(&c.outbounds, outbound).read)
|
||||
writeCounter = append(writeCounter, c.outbounds[outbound].write)
|
||||
}
|
||||
if user != "" {
|
||||
readCounter = append(readCounter, c.loadOrCreateCounter(&c.users, user).read)
|
||||
writeCounter = append(writeCounter, c.users[user].write)
|
||||
}
|
||||
return readCounter, writeCounter
|
||||
}
|
||||
|
||||
func (c *StatsTracker) loadOrCreateCounter(obj *map[string]Counter, name string) Counter {
|
||||
counter, loaded := (*obj)[name]
|
||||
if loaded {
|
||||
return counter
|
||||
}
|
||||
counter = Counter{read: &atomic.Int64{}, write: &atomic.Int64{}}
|
||||
(*obj)[name] = counter
|
||||
return counter
|
||||
}
|
||||
|
||||
func (c *StatsTracker) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn {
|
||||
readCounter, writeCounter := c.getReadCounters(metadata.Inbound, matchOutbound.Tag(), metadata.User)
|
||||
return bufio.NewInt64CounterConn(conn, readCounter, writeCounter)
|
||||
}
|
||||
|
||||
func (c *StatsTracker) RoutedPacketConnection(ctx context.Context, conn network.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) network.PacketConn {
|
||||
readCounter, writeCounter := c.getReadCounters(metadata.Inbound, matchOutbound.Tag(), metadata.User)
|
||||
return bufio.NewInt64CounterPacketConn(conn, readCounter, nil, writeCounter, nil)
|
||||
}
|
||||
|
||||
func (c *StatsTracker) GetStats() *[]model.Stats {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
|
||||
dt := time.Now().Unix()
|
||||
|
||||
s := []model.Stats{}
|
||||
for inbound, counter := range c.inbounds {
|
||||
down := counter.write.Swap(0)
|
||||
up := counter.read.Swap(0)
|
||||
if down > 0 || up > 0 {
|
||||
s = append(s, model.Stats{
|
||||
DateTime: dt,
|
||||
Resource: "inbound",
|
||||
Tag: inbound,
|
||||
Direction: false,
|
||||
Traffic: down,
|
||||
}, model.Stats{
|
||||
DateTime: dt,
|
||||
Resource: "inbound",
|
||||
Tag: inbound,
|
||||
Direction: true,
|
||||
Traffic: up,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for outbound, counter := range c.outbounds {
|
||||
down := counter.write.Swap(0)
|
||||
up := counter.read.Swap(0)
|
||||
if down > 0 || up > 0 {
|
||||
s = append(s, model.Stats{
|
||||
DateTime: dt,
|
||||
Resource: "outbound",
|
||||
Tag: outbound,
|
||||
Direction: false,
|
||||
Traffic: down,
|
||||
}, model.Stats{
|
||||
DateTime: dt,
|
||||
Resource: "outbound",
|
||||
Tag: outbound,
|
||||
Direction: true,
|
||||
Traffic: up,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for user, counter := range c.users {
|
||||
down := counter.write.Swap(0)
|
||||
up := counter.read.Swap(0)
|
||||
if down > 0 || up > 0 {
|
||||
s = append(s, model.Stats{
|
||||
DateTime: dt,
|
||||
Resource: "user",
|
||||
Tag: user,
|
||||
Direction: false,
|
||||
Traffic: down,
|
||||
}, model.Stats{
|
||||
DateTime: dt,
|
||||
Resource: "user",
|
||||
Tag: user,
|
||||
Direction: true,
|
||||
Traffic: up,
|
||||
})
|
||||
}
|
||||
}
|
||||
return &s
|
||||
}
|
||||
Reference in New Issue
Block a user