The daemon's own log (engine + control-plane) went only to os.Stderr → procd → the logread RAM ring: no file, no wall-clock timestamps, no size cap, and "LogLevel=none" silenced ONLY the engine while the control-plane kept writing at trace. So "download last day/3d/all", "limit the size" and "fully turn it off" were all unmet. New shater/logsink: one long-lived, atomically-reconfigurable Sink that receives BOTH halves' byte streams, stamps every complete line with a UTC RFC3339 wall clock (what makes date ranges real), and fans each line to a size-capped 2-segment rotated file (ToFile) and/or the real os.Stderr (ToSyslog). Both off = the line is dropped — the only true full silence. Persistent path sits behind a stats-style disk-free guard (suspend+warn once, auto-resume); tmpfs path is bounded by the cap itself. ANSI stripped from the file copy only. Wiring: control-plane via log.SetStdLogger over the sink; engine via a new box.Options.DefaultLogWriter threaded into all three box.New sites (apply/close-then-start/restore) by engine.SetDefaultLogWriter; live reconfigure on every apply.Reconcile (SIGHUP / control socket / panel apply) so panel changes take effect without a daemon restart. controlLogLevel now makes the control-plane respect Globals.LogLevel (silent vocab → panic-only; unknown → warn, mirroring generate). Globals: LogToSyslog/LogToFile (default true), LogPersist (default false = /var/log tmpfs; true = /etc/shater flash), LogMaxKB (default 2048, clamped [128,8192]; 0 = default, not off — LogToFile is the off switch). UCI parse/render/aliases + ValidateGlobals clamp-warn. Endpoint GET /api/log?range=1d|3d|all (session-gated): streams the log line by line, oldest segment first, filtered by the timestamp prefix; UTC attachment filename. Honest fallbacks — file off + syslog on → a "# note: … syslog ring only, ranges approximate" comment then a `logread -e shater` scrape; both off → "# logging disabled". Unknown range → 400. init.d: shater/shater-cron gate their `logger -t` status lines on log_syslog so "logread off" is honest at the shell layer too. Tests: logsink rotation-cap/timestamp/toggle-gating/engine→sink, model round-trip + validate, endpoint session-gate/range/fallbacks. VM-verified on QEMU (x86_64, OpenWrt 24.10): download+ranges, size-cap rotation, file-off/full-off, persistent path, live reconfigure — all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
712 lines
26 KiB
Go
712 lines
26 KiB
Go
package box
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"runtime/debug"
|
|
"time"
|
|
|
|
"github.com/sagernet/sing-box/adapter"
|
|
boxCertificate "github.com/sagernet/sing-box/adapter/certificate"
|
|
"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/dnstrack"
|
|
"github.com/sagernet/sing-box/common/httpclient"
|
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
|
"github.com/sagernet/sing-box/common/tls"
|
|
"github.com/sagernet/sing-box/common/trafficcontrol"
|
|
"github.com/sagernet/sing-box/common/urltest"
|
|
C "github.com/sagernet/sing-box/constant"
|
|
"github.com/sagernet/sing-box/dns"
|
|
"github.com/sagernet/sing-box/experimental"
|
|
"github.com/sagernet/sing-box/experimental/cachefile"
|
|
"github.com/sagernet/sing-box/experimental/deprecated"
|
|
"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"
|
|
"github.com/sagernet/sing/common"
|
|
E "github.com/sagernet/sing/common/exceptions"
|
|
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
|
|
certificateProvider *boxCertificate.Manager
|
|
dnsTransport *dns.TransportManager
|
|
dnsRouter *dns.Router
|
|
connection *route.ConnectionManager
|
|
router *route.Router
|
|
httpClientService adapter.LifecycleService
|
|
internalService []adapter.LifecycleService
|
|
done chan struct{}
|
|
}
|
|
|
|
type Options struct {
|
|
option.Options
|
|
Context context.Context
|
|
PlatformLogWriter log.PlatformWriter
|
|
// lx:begin shaterd_logsink
|
|
// DefaultLogWriter, when non-nil, is where the box log goes while
|
|
// options.Log.Output is unset — replacing log.New's nil->os.Stderr default.
|
|
// The shater daemon passes its long-lived logsink here (via
|
|
// engine.SetDefaultLogWriter) so every Apply-swapped box writes into the
|
|
// same reconfigurable sink instead of raw stderr. It takes precedence over
|
|
// the platform-interface io.Discard fallback below: an embedder that
|
|
// supplies a writer wants the log, explicitly.
|
|
DefaultLogWriter io.Writer
|
|
// lx:end
|
|
}
|
|
|
|
func Context(
|
|
ctx context.Context,
|
|
inboundRegistry adapter.InboundRegistry,
|
|
outboundRegistry adapter.OutboundRegistry,
|
|
endpointRegistry adapter.EndpointRegistry,
|
|
dnsTransportRegistry adapter.DNSTransportRegistry,
|
|
serviceRegistry adapter.ServiceRegistry,
|
|
certificateProviderRegistry adapter.CertificateProviderRegistry,
|
|
) 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)
|
|
}
|
|
if service.FromContext[adapter.CertificateProviderRegistry](ctx) == nil {
|
|
ctx = service.ContextWith[option.CertificateProviderOptionsRegistry](ctx, certificateProviderRegistry)
|
|
ctx = service.ContextWith[adapter.CertificateProviderRegistry](ctx, certificateProviderRegistry)
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
func New(options Options) (*Box, 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)
|
|
certificateProviderRegistry := service.FromContext[adapter.CertificateProviderRegistry](ctx)
|
|
|
|
if endpointRegistry == nil {
|
|
return nil, E.New("missing endpoint registry in context")
|
|
}
|
|
if inboundRegistry == nil {
|
|
return nil, E.New("missing inbound registry in context")
|
|
}
|
|
if outboundRegistry == nil {
|
|
return nil, E.New("missing outbound registry in context")
|
|
}
|
|
if dnsTransportRegistry == nil {
|
|
return nil, E.New("missing DNS transport registry in context")
|
|
}
|
|
if serviceRegistry == nil {
|
|
return nil, E.New("missing service registry in context")
|
|
}
|
|
if certificateProviderRegistry == nil {
|
|
return nil, E.New("missing certificate provider registry in context")
|
|
}
|
|
|
|
ctx = pause.WithDefaultManager(ctx)
|
|
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
|
err := applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var needCacheFile bool
|
|
var needClashAPI bool
|
|
var needV2RayAPI bool
|
|
if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil {
|
|
needCacheFile = true
|
|
}
|
|
// lx:begin lx_command
|
|
// A platform log writer (always set on Android/libbox) historically forced
|
|
// needClashAPI, because the Clash server was the only log/traffic observer.
|
|
// With with_clash_api dropped (lx), that turned every Android start into a
|
|
// fatal "clash api is not included in this build". Split the concern: the
|
|
// platform writer needs *observability* (Observable log factory + traffic
|
|
// manager), NOT the Clash server specifically. Only an explicit clash_api
|
|
// config block now creates the Clash server; the native CommandClient
|
|
// (SubscribeLog / SubscribeConnections) serves the platform client instead.
|
|
if experimentalOptions.ClashAPI != nil {
|
|
needClashAPI = true
|
|
}
|
|
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
|
needV2RayAPI = true
|
|
}
|
|
needAPIService := common.Any(options.Services, func(it option.Service) bool {
|
|
return it.Type == C.TypeAPI
|
|
})
|
|
// Observability (log stream + connection/traffic tracking) is required by the
|
|
// platform client and by either API surface, independent of the Clash server.
|
|
needObservable := needClashAPI || needAPIService || options.PlatformLogWriter != nil
|
|
// lx:end lx_command
|
|
if service.PtrFromContext[urltest.HistoryStorage](ctx) == nil {
|
|
ctx = service.ContextWithPtr(ctx, urltest.NewHistoryStorage())
|
|
}
|
|
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
|
|
var defaultLogWriter io.Writer
|
|
if platformInterface != nil {
|
|
defaultLogWriter = io.Discard
|
|
}
|
|
// lx:begin shaterd_logsink — an explicitly-supplied default writer wins.
|
|
if options.DefaultLogWriter != nil {
|
|
defaultLogWriter = options.DefaultLogWriter
|
|
}
|
|
// lx:end
|
|
logFactory, err := log.New(log.Options{
|
|
Context: ctx,
|
|
Options: common.PtrValueOrDefault(options.Log),
|
|
Observable: needObservable, // lx: was needClashAPI || needAPIService
|
|
DefaultWriter: defaultLogWriter,
|
|
BaseTime: createdAt,
|
|
PlatformWriter: options.PlatformLogWriter,
|
|
})
|
|
if err != nil {
|
|
return nil, E.Cause(err, "create log factory")
|
|
}
|
|
service.MustRegister[log.Factory](ctx, logFactory)
|
|
|
|
var internalServices []adapter.LifecycleService
|
|
routeOptions := common.PtrValueOrDefault(options.Route)
|
|
certificateOptions := common.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(logFactory.NewLogger("certificate"), certificateOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
|
internalServices = append(internalServices, certificateStore)
|
|
}
|
|
dnsOptions := common.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)
|
|
certificateProviderManager := boxCertificate.NewManager(logFactory.NewLogger("certificate-provider"), certificateProviderRegistry)
|
|
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)
|
|
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager)
|
|
dnsRouter, err := dns.NewRouter(ctx, logFactory, dnsOptions)
|
|
if err != nil {
|
|
return nil, E.Cause(err, "initialize DNS router")
|
|
}
|
|
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
|
service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter)
|
|
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
|
if err != nil {
|
|
return nil, E.Cause(err, "initialize network manager")
|
|
}
|
|
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
|
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
|
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
|
// Must register after ConnectionManager: the Apple HTTP engine's proxy bridge reads it from the context when Manager.Start resolves the default client.
|
|
httpClientManager := httpclient.NewManager(ctx, logFactory.NewLogger("httpclient"), options.HTTPClients, routeOptions.DefaultHTTPClient)
|
|
service.MustRegister[adapter.HTTPClientManager](ctx, httpClientManager)
|
|
httpClientService := adapter.LifecycleService(httpClientManager)
|
|
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
|
service.MustRegister[adapter.Router](ctx, router)
|
|
service.MustRegister[adapter.ReachabilityInvalidator](ctx, router) // lx: SPEC 020 — event points invalidate the reachable cache
|
|
err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet)
|
|
if err != nil {
|
|
return nil, E.Cause(err, "initialize router")
|
|
}
|
|
// lx: shater feedback #2 — build the connection/traffic tracker UNCONDITIONALLY
|
|
// (it was gated on needObservable). The shater control-plane subscribes to this
|
|
// Manager's in-process connection-event stream (SubscribeEvents) to attribute
|
|
// per-device DESTINATION DOMAINS ("which client hit which host" — DPI-style).
|
|
// box.New historically created the tracker only under needObservable (clash-api /
|
|
// api service / platform log writer), and the shater generator emits NONE of those
|
|
// on the router, so the tracker — and therefore every connection event — would
|
|
// never exist (the same dead-stream situation the DNS query manager was in before
|
|
// it was pre-registered in the engine ctx). Unlike the DNS manager this one CANNOT
|
|
// be pre-built in the engine ctx: it needs the outboundManager that only exists
|
|
// here inside box.New, and box.New reuses the engine's shared registry, so the
|
|
// aggregator reads it back via service.PtrFromContext (engine.ConnManager). The
|
|
// pointer changes on every Apply swap; the aggregator resubscribes on pointer
|
|
// identity (mirroring the DNS loop's box-owned fallback).
|
|
//
|
|
// Why this is safe / opens NO port: the Clash server is still gated on
|
|
// needClashAPI below — this branch creates only the pure in-process observable.
|
|
// The observable's Emit is non-blocking (drops when nobody is draining, see
|
|
// sing/common/observable), so an UNSUBSCRIBED tracker costs only the per-connection
|
|
// byte counters + bounded connection map that clash already imposes — no hot-path
|
|
// stall. This is the identical construct upstream builds whenever clash API is on.
|
|
trafficManager := trafficcontrol.NewManager(outboundManager)
|
|
service.MustRegisterPtr(ctx, trafficManager)
|
|
router.AppendTracker(trafficManager)
|
|
internalServices = append(internalServices, trafficManager)
|
|
if needObservable { // lx: was needClashAPI || needAPIService — platform writer needs the DNS stream too
|
|
// lx: SPEC 018 — DNS query stream. Registered as *dnstrack.Manager so the dns
|
|
// client (service.PtrFromContext in dns/client_log.go — pairs with MustRegisterPtr;
|
|
// FromContext[*T] keys on **T and silently returns nil, the §180 dead-stream bug)
|
|
// emits structured, process-attributed query events the command server exposes as
|
|
// SubscribeDNSQueries. Only the DNS manager stays gated on needObservable: the
|
|
// engine pre-registers a STABLE *dnstrack.Manager in its own ctx, so on the router
|
|
// (needObservable=false) this branch is skipped and that stable manager is used;
|
|
// the traffic manager above was moved out of this block so it is always present.
|
|
dnsQueryManager := dnstrack.NewManager()
|
|
service.MustRegisterPtr(ctx, dnsQueryManager)
|
|
internalServices = append(internalServices, dnsQueryManager)
|
|
}
|
|
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
|
var timeService *tls.TimeServiceWrapper
|
|
if ntpOptions.Enabled {
|
|
timeService = new(tls.TimeServiceWrapper)
|
|
service.MustRegister[ntp.TimeService](ctx, timeService)
|
|
}
|
|
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, E.Cause(err, "initialize DNS server[", i, "]")
|
|
}
|
|
}
|
|
err = dnsRouter.Initialize(dnsOptions.Rules)
|
|
if err != nil {
|
|
return nil, E.Cause(err, "initialize dns router")
|
|
}
|
|
for i, endpointOptions := range options.Endpoints {
|
|
var tag string
|
|
if endpointOptions.Tag != "" {
|
|
tag = endpointOptions.Tag
|
|
} else {
|
|
tag = F.ToString(i)
|
|
}
|
|
endpointCtx := ctx
|
|
if tag != "" {
|
|
// TODO: remove this
|
|
endpointCtx = adapter.WithContext(endpointCtx, &adapter.InboundContext{
|
|
Outbound: tag,
|
|
})
|
|
}
|
|
err = endpointManager.Create(
|
|
endpointCtx,
|
|
router,
|
|
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
|
|
tag,
|
|
endpointOptions.Type,
|
|
endpointOptions.Options,
|
|
)
|
|
if err != nil {
|
|
return nil, E.Cause(err, "initialize endpoint[", i, "]")
|
|
}
|
|
}
|
|
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, E.Cause(err, "initialize inbound[", i, "]")
|
|
}
|
|
}
|
|
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, E.Cause(err, "initialize service[", i, "]")
|
|
}
|
|
}
|
|
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, E.Cause(err, "initialize outbound[", i, "]")
|
|
}
|
|
}
|
|
for i, certificateProviderOptions := range options.CertificateProviders {
|
|
var tag string
|
|
if certificateProviderOptions.Tag != "" {
|
|
tag = certificateProviderOptions.Tag
|
|
} else {
|
|
tag = F.ToString(i)
|
|
}
|
|
err = certificateProviderManager.Create(
|
|
ctx,
|
|
logFactory.NewLogger(F.ToString("certificate-provider/", certificateProviderOptions.Type, "[", tag, "]")),
|
|
tag,
|
|
certificateProviderOptions.Type,
|
|
certificateProviderOptions.Options,
|
|
)
|
|
if err != nil {
|
|
return nil, E.Cause(err, "initialize certificate provider[", i, "]")
|
|
}
|
|
}
|
|
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 dnsTransportRegistry.CreateDNSTransport(
|
|
ctx,
|
|
logFactory.NewLogger("dns/local"),
|
|
"local",
|
|
C.DNSTypeLocal,
|
|
&option.LocalDNSServerOptions{},
|
|
)
|
|
})
|
|
httpClientManager.Initialize(func() (*httpclient.ManagedTransport, error) {
|
|
deprecated.Report(ctx, deprecated.OptionImplicitDefaultHTTPClient)
|
|
var httpClientOptions option.HTTPClientOptions
|
|
httpClientOptions.DefaultOutbound = true
|
|
return httpclient.NewTransport(ctx, logFactory.NewLogger("httpclient"), "", httpClientOptions)
|
|
})
|
|
if platformInterface != nil {
|
|
err = platformInterface.Initialize(networkManager)
|
|
if err != nil {
|
|
return nil, E.Cause(err, "initialize platform interface")
|
|
}
|
|
}
|
|
if needCacheFile {
|
|
cacheFile := cachefile.New(ctx, logFactory.NewLogger("cache-file"), common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
|
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
|
internalServices = append(internalServices, cacheFile)
|
|
}
|
|
if needClashAPI {
|
|
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
|
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
|
|
clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), clashAPIOptions)
|
|
if err != nil {
|
|
return nil, E.Cause(err, "create clash-server")
|
|
}
|
|
service.MustRegister[adapter.ClashServer](ctx, clashServer)
|
|
internalServices = append(internalServices, clashServer)
|
|
}
|
|
if needV2RayAPI {
|
|
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
|
|
if err != nil {
|
|
return nil, E.Cause(err, "create v2ray-server")
|
|
}
|
|
if v2rayServer.StatsService() != nil {
|
|
router.AppendTracker(v2rayServer.StatsService())
|
|
internalServices = append(internalServices, v2rayServer)
|
|
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
|
}
|
|
}
|
|
if ntpOptions.Enabled {
|
|
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions, ntpOptions.ServerIsDomain())
|
|
if err != nil {
|
|
return nil, E.Cause(err, "create NTP service")
|
|
}
|
|
ntpService := ntp.NewService(ntp.Options{
|
|
Context: ctx,
|
|
Dialer: ntpDialer,
|
|
Logger: logFactory.NewLogger("ntp"),
|
|
Server: ntpOptions.ServerOptions.Build(),
|
|
Interval: time.Duration(ntpOptions.Interval),
|
|
WriteToSystem: ntpOptions.WriteToSystem,
|
|
})
|
|
timeService.TimeService = ntpService
|
|
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
|
|
}
|
|
return &Box{
|
|
network: networkManager,
|
|
endpoint: endpointManager,
|
|
inbound: inboundManager,
|
|
outbound: outboundManager,
|
|
dnsTransport: dnsTransportManager,
|
|
service: serviceManager,
|
|
certificateProvider: certificateProviderManager,
|
|
dnsRouter: dnsRouter,
|
|
connection: connectionManager,
|
|
router: router,
|
|
httpClientService: httpClientService,
|
|
createdAt: createdAt,
|
|
logFactory: logFactory,
|
|
logger: logFactory.Logger(),
|
|
internalService: internalServices,
|
|
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 {
|
|
println(err.Error())
|
|
debug.PrintStack()
|
|
panic("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 {
|
|
// TODO: remove catch error
|
|
defer func() {
|
|
v := recover()
|
|
if v != nil {
|
|
println(err.Error())
|
|
debug.PrintStack()
|
|
println("panic on early start: " + fmt.Sprint(v))
|
|
}
|
|
}()
|
|
s.Close()
|
|
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 E.Cause(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, s.certificateProvider)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = adapter.StartNamed(s.logger, adapter.StartStateStart, []adapter.LifecycleService{s.httpClientService})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = adapter.Start(s.logger, adapter.StartStateStart, s.router, s.dnsRouter)
|
|
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.endpoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = adapter.Start(s.logger, adapter.StartStateStart, s.certificateProvider)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, 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.endpoint, s.certificateProvider, s.inbound, 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.endpoint, s.certificateProvider, s.inbound, 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 os.ErrClosed
|
|
default:
|
|
close(s.done)
|
|
}
|
|
var err error
|
|
for _, closeItem := range []struct {
|
|
name string
|
|
service adapter.Lifecycle
|
|
}{
|
|
{"service", s.service},
|
|
{"inbound", s.inbound},
|
|
{"certificate-provider", s.certificateProvider},
|
|
{"endpoint", s.endpoint},
|
|
{"outbound", s.outbound},
|
|
{"router", s.router},
|
|
{"connection", s.connection},
|
|
{"dns-router", s.dnsRouter},
|
|
{"dns-transport", s.dnsTransport},
|
|
{"network", s.network},
|
|
} {
|
|
done := adapter.LogElapsed(s.logger, "close ", closeItem.name)
|
|
err = E.Append(err, closeItem.service.Close(), func(err error) error {
|
|
return E.Cause(err, "close ", closeItem.name)
|
|
})
|
|
done()
|
|
}
|
|
if s.httpClientService != nil {
|
|
s.logger.Trace("close ", s.httpClientService.Name())
|
|
startTime := time.Now()
|
|
err = E.Append(err, s.httpClientService.Close(), func(err error) error {
|
|
return E.Cause(err, "close ", s.httpClientService.Name())
|
|
})
|
|
s.logger.Trace("close ", s.httpClientService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
|
}
|
|
for _, lifecycleService := range s.internalService {
|
|
done := adapter.LogElapsed(s.logger, "close ", lifecycleService.Name())
|
|
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
|
return E.Cause(err, "close ", lifecycleService.Name())
|
|
})
|
|
done()
|
|
}
|
|
done := adapter.LogElapsed(s.logger, "close logger")
|
|
err = E.Append(err, s.logFactory.Close(), func(err error) error {
|
|
return E.Cause(err, "close logger")
|
|
})
|
|
done()
|
|
return err
|
|
}
|
|
|
|
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) LogFactory() log.Factory {
|
|
return s.logFactory
|
|
}
|