This commit is contained in:
admin8800
2026-05-10 06:41:44 +00:00
parent d104b6e40d
commit 3eb70ee9ed
252 changed files with 42215 additions and 2 deletions
+592
View File
@@ -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
}
+147
View File
@@ -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
View File
@@ -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)
}
+95
View File
@@ -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
}
+40
View File
@@ -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
}
+139
View File
@@ -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
}
+12
View File
@@ -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)
}
+13
View File
@@ -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")
}
+23
View File
@@ -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)
}
+34
View File
@@ -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`)
})
}
+219
View File
@@ -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
}
+153
View File
@@ -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
}