diff --git a/Dockerfile b/Dockerfile index 0b1ac735..15bee88b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN set -ex \ && export COMMIT=$(git rev-parse --short HEAD) \ && export VERSION=$(go run ./cmd/internal/read_tag) \ && go build -v -trimpath -tags \ - "with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \ + "with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api,with_metric_api" \ -o /go/bin/sing-box \ -ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid= -checklinkname=0" \ ./cmd/sing-box diff --git a/Makefile b/Makefile index 78721f1d..73a65baa 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ NAME = sing-box COMMIT = $(shell git rev-parse --short HEAD) -TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls +TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_metric_api,with_quic,with_utls TAGS_GO121 = with_ech TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121) TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server @@ -208,4 +208,4 @@ clean: update: git fetch git reset FETCH_HEAD --hard - git clean -fdx \ No newline at end of file + git clean -fdx diff --git a/adapter/experimental.go b/adapter/experimental.go index 0cab5ed5..863fa8b4 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -118,10 +118,20 @@ func OutboundTag(detour Outbound) string { type V2RayServer interface { Service - StatsService() V2RayStatsService + StatsService() PacketTracking } -type V2RayStatsService interface { - RoutedConnection(inbound string, outbound string, user string, conn net.Conn) net.Conn - RoutedPacketConnection(inbound string, outbound string, user string, conn N.PacketConn) N.PacketConn +type MetricService interface { + Service + PacketTracking } + +type PacketTracking interface { + WithConnCounters(inbound, outbound, user string) ConnAdapter[net.Conn] + WithPacketConnCounters(inbound, outbound, user string) ConnAdapter[N.PacketConn] +} + +// ConnAdapter +// Transform a connection to another connection. The T should be either of +// net.Conn or N.PacketConn. +type ConnAdapter[T any] func(T) T diff --git a/box.go b/box.go index 716b1b09..4a7295f5 100644 --- a/box.go +++ b/box.go @@ -61,6 +61,7 @@ func New(options Options) (*Box, error) { var needCacheFile bool var needClashAPI bool var needV2RayAPI bool + var needMetricAPI bool if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil { needCacheFile = true } @@ -70,6 +71,9 @@ func New(options Options) (*Box, error) { if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" { needV2RayAPI = true } + if experimentalOptions.MetricAPI.Enabled() { + needMetricAPI = true + } var defaultLogWriter io.Writer if options.PlatformInterface != nil { defaultLogWriter = io.Discard @@ -183,6 +187,14 @@ func New(options Options) (*Box, error) { router.SetV2RayServer(v2rayServer) preServices2["v2ray api"] = v2rayServer } + if needMetricAPI { + metricServer, err := experimental.NewMetricServer(logFactory.NewLogger("metric-api"), common.PtrValueOrDefault(experimentalOptions.MetricAPI)) + if err != nil { + return nil, E.Cause(err, "create metric api server") + } + router.SetMetricServer(metricServer) + preServices2["metric api"] = metricServer + } return &Box{ router: router, inbounds: inbounds, diff --git a/cmd/internal/build_libbox/main.go b/cmd/internal/build_libbox/main.go index fc9308ff..ab269904 100644 --- a/cmd/internal/build_libbox/main.go +++ b/cmd/internal/build_libbox/main.go @@ -54,7 +54,7 @@ func init() { sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=") debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag) - sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api") + sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api", "with_metric_api") iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack") debugTags = append(debugTags, "debug") } diff --git a/docs/configuration/experimental/index.md b/docs/configuration/experimental/index.md index a1a515cf..e4ce8a47 100644 --- a/docs/configuration/experimental/index.md +++ b/docs/configuration/experimental/index.md @@ -2,7 +2,7 @@ !!! quote "Changes in sing-box 1.8.0" - :material-plus: [cache_file](#cache_file) + :material-plus: [cache_file](#cache_file) :material-alert-decagram: [clash_api](#clash_api) ### Structure @@ -23,4 +23,5 @@ |--------------|----------------------------| | `cache_file` | [Cache File](./cache-file/) | | `clash_api` | [Clash API](./clash-api/) | -| `v2ray_api` | [V2Ray API](./v2ray-api/) | \ No newline at end of file +| `metric_api` | [Metric API](./metric-api/) | +| `v2ray_api` | [V2Ray API](./v2ray-api/) | diff --git a/docs/configuration/experimental/index.zh.md b/docs/configuration/experimental/index.zh.md index 01246c44..908a9541 100644 --- a/docs/configuration/experimental/index.zh.md +++ b/docs/configuration/experimental/index.zh.md @@ -2,7 +2,7 @@ !!! quote "sing-box 1.8.0 中的更改" - :material-plus: [cache_file](#cache_file) + :material-plus: [cache_file](#cache_file) :material-alert-decagram: [clash_api](#clash_api) ### 结构 @@ -12,6 +12,7 @@ "experimental": { "cache_file": {}, "clash_api": {}, + "metrics": {}, "v2ray_api": {} } } @@ -19,8 +20,9 @@ ### 字段 -| 键 | 格式 | -|--------------|--------------------------| +| 键 | 格式 | +|--------------|------------------------------| | `cache_file` | [缓存文件](./cache-file/) | -| `clash_api` | [Clash API](./clash-api/) | -| `v2ray_api` | [V2Ray API](./v2ray-api/) | \ No newline at end of file +| `clash_api` | [Clash API](./clash-api/) | +| `metric_api` | [Metric API](./metric-api/) | +| `v2ray_api` | [V2Ray API](./v2ray-api/) | diff --git a/docs/configuration/experimental/metric-api.md b/docs/configuration/experimental/metric-api.md new file mode 100644 index 00000000..f8d9c131 --- /dev/null +++ b/docs/configuration/experimental/metric-api.md @@ -0,0 +1,21 @@ + +### Structure + +```json +{ + "metrics": { + "listen": ":8080", + "path": "/metrics" + } +} +``` + +### Fields + +#### listen + +Prometheus metrics API listening address, disabled if empty. + +#### path + +Prometheus scrape path, `/metrics` will be used if empty. diff --git a/docs/configuration/experimental/metric-api.zh.md b/docs/configuration/experimental/metric-api.zh.md new file mode 100644 index 00000000..6ff2cf61 --- /dev/null +++ b/docs/configuration/experimental/metric-api.zh.md @@ -0,0 +1,21 @@ + +### Structure + +```json +{ + "metrics": { + "listen": ":8080", + "path": "/metrics" + } +} +``` + +### Fields + +#### listen + +Prometheus 指标监听地址,如果为空则禁用。 + +#### path + +HTTP 路径,如果为空则使用 `/metrics`。本路径可用于 Prometheus exporter 进行抓取。 diff --git a/docs/installation/build-from-source.md b/docs/installation/build-from-source.md index 3ee5b6c5..5281ee42 100644 --- a/docs/installation/build-from-source.md +++ b/docs/installation/build-from-source.md @@ -65,6 +65,7 @@ go build -tags "tag_a tag_b" ./cmd/sing-box | `with_reality_server` | :material-check: | Build with reality TLS server support, see [TLS](/configuration/shared/tls/). | | `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls/). | | `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). | +| `with_metric_api` | :material-check: | Build with Prometheus metric API support, see [Experimental](/configuration/experimental#metric-api-fields). | | `with_v2ray_api` | :material-close:️ | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). | | `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). | | `with_embedded_tor` (CGO required) | :material-close:️ | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). | diff --git a/docs/installation/build-from-source.zh.md b/docs/installation/build-from-source.zh.md index e5da3e33..308b8cad 100644 --- a/docs/installation/build-from-source.zh.md +++ b/docs/installation/build-from-source.zh.md @@ -65,6 +65,7 @@ go build -tags "tag_a tag_b" ./cmd/sing-box | `with_reality_server` | :material-check: | Build with reality TLS server support, see [TLS](/configuration/shared/tls/). | | `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls/). | | `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). | +| `with_metric_api` | :material-check: | Build with Prometheus metric API support, see [Experimental](/configuration/experimental#metric-api-fields). | | `with_v2ray_api` | :material-close:️ | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). | | `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). | | `with_embedded_tor` (CGO required) | :material-close:️ | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). | diff --git a/experimental/metricapi.go b/experimental/metricapi.go new file mode 100644 index 00000000..ceeeaff9 --- /dev/null +++ b/experimental/metricapi.go @@ -0,0 +1,24 @@ +package experimental + +import ( + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" +) + +type MetricServerConstructor = func(logger log.Logger, options option.MetricOptions) (adapter.MetricService, error) + +var metricServerConstructor MetricServerConstructor + +func RegisterMetricServerConstructor(constructor MetricServerConstructor) { + metricServerConstructor = constructor +} + +func NewMetricServer(logger log.Logger, options option.MetricOptions) (adapter.MetricService, error) { + if metricServerConstructor == nil { + return nil, os.ErrInvalid + } + return metricServerConstructor(logger, options) +} diff --git a/experimental/metrics/server.go b/experimental/metrics/server.go new file mode 100644 index 00000000..41215672 --- /dev/null +++ b/experimental/metrics/server.go @@ -0,0 +1,77 @@ +package metrics + +import ( + "errors" + "net" + "net/http" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/experimental" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + + "github.com/go-chi/chi/v5" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +var _ adapter.MetricService = (*metricServer)(nil) + +func init() { + experimental.RegisterMetricServerConstructor(NewServer) +} + +type metricServer struct { + http *http.Server + + logger log.Logger + opts option.MetricOptions + + packetCountersInbound *prometheus.CounterVec + packetCountersOutbound *prometheus.CounterVec +} + +func NewServer(logger log.Logger, opts option.MetricOptions) (adapter.MetricService, error) { + r := chi.NewRouter() + _server := &http.Server{ + Addr: opts.Listen, + Handler: r, + } + if opts.Path == "" { + opts.Path = "/metrics" + } + r.Get(opts.Path, promhttp.Handler().ServeHTTP) + server := &metricServer{ + http: _server, + logger: logger, + opts: opts, + } + err := server.registerMetrics() + return server, err +} + +func (s *metricServer) Start() error { + if !s.opts.Enabled() { + return nil + } + listener, err := net.Listen("tcp", s.opts.Listen) + if err != nil { + return err + } + s.logger.Info("metrics api listening at ", s.http.Addr, s.opts.Path) + go func() { + err := s.http.Serve(listener) + if err != nil && !errors.Is(err, http.ErrServerClosed) { + s.logger.Error("metrics api serve error: ", err) + } + }() + return nil +} + +func (s *metricServer) Close() error { + if !s.opts.Enabled() { + return nil + } + return common.Close(common.PtrOrNil(s.http)) +} diff --git a/experimental/metrics/tracker.go b/experimental/metrics/tracker.go new file mode 100644 index 00000000..a5614dcf --- /dev/null +++ b/experimental/metrics/tracker.go @@ -0,0 +1,52 @@ +package metrics + +import ( + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/bufio" + N "github.com/sagernet/sing/common/network" + + "github.com/prometheus/client_golang/prometheus" +) + +func (s *metricServer) registerMetrics() error { + s.packetCountersInbound = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "inbound_packet_bytes", + Help: "Total bytes of inbound packets", + }, []string{"inbound", "user"}) + + s.packetCountersOutbound = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "outbound_packet_bytes", + Help: "Total bytes of outbound packets", + }, []string{"outbound", "user"}) + var err error + err = prometheus.Register(s.packetCountersInbound) + err = prometheus.Register(s.packetCountersOutbound) + return err +} + +func (s *metricServer) WithConnCounters(inbound, outbound, user string) adapter.ConnAdapter[net.Conn] { + incRead, incWrite := s.getPacketCounters(inbound, outbound, user) + return func(conn net.Conn) net.Conn { + return bufio.NewCounterConn(conn, []N.CountFunc{incRead}, []N.CountFunc{incWrite}) + } +} + +func (s *metricServer) WithPacketConnCounters(inbound, outbound, user string) adapter.ConnAdapter[N.PacketConn] { + incRead, incWrite := s.getPacketCounters(inbound, outbound, user) + return func(conn N.PacketConn) N.PacketConn { + return bufio.NewCounterPacketConn(conn, []N.CountFunc{incRead}, []N.CountFunc{incWrite}) + } +} + +func (s *metricServer) getPacketCounters(inbound, outbound, user string) ( + readCounters N.CountFunc, + writeCounters N.CountFunc, +) { + return func(n int64) { + s.packetCountersInbound.WithLabelValues(inbound, user).Add(float64(n)) + }, func(n int64) { + s.packetCountersOutbound.WithLabelValues(outbound, user).Add(float64(n)) + } +} diff --git a/experimental/v2rayapi/server.go b/experimental/v2rayapi/server.go index 8b4b4385..2c9dc161 100644 --- a/experimental/v2rayapi/server.go +++ b/experimental/v2rayapi/server.go @@ -70,6 +70,6 @@ func (s *Server) Close() error { ) } -func (s *Server) StatsService() adapter.V2RayStatsService { +func (s *Server) StatsService() adapter.PacketTracking { return s.statsService } diff --git a/experimental/v2rayapi/stats.go b/experimental/v2rayapi/stats.go index 38b9a301..3031b130 100644 --- a/experimental/v2rayapi/stats.go +++ b/experimental/v2rayapi/stats.go @@ -22,8 +22,8 @@ func init() { } var ( - _ adapter.V2RayStatsService = (*StatsService)(nil) - _ StatsServiceServer = (*StatsService)(nil) + _ adapter.PacketTracking = (*StatsService)(nil) + _ StatsServiceServer = (*StatsService)(nil) ) type StatsService struct { @@ -60,14 +60,14 @@ func NewStatsService(options option.V2RayStatsServiceOptions) *StatsService { } } -func (s *StatsService) RoutedConnection(inbound string, outbound string, user string, conn net.Conn) net.Conn { +func (s *StatsService) getPacketCounters(inbound, outbound, user string) (incRead N.CountFunc, inWrite N.CountFunc) { var readCounter []*atomic.Int64 var writeCounter []*atomic.Int64 countInbound := inbound != "" && s.inbounds[inbound] countOutbound := outbound != "" && s.outbounds[outbound] countUser := user != "" && s.users[user] if !countInbound && !countOutbound && !countUser { - return conn + return func(n int64) {}, func(n int64) {} } s.access.Lock() if countInbound { @@ -83,33 +83,29 @@ func (s *StatsService) RoutedConnection(inbound string, outbound string, user st writeCounter = append(writeCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>downlink")) } s.access.Unlock() - return bufio.NewInt64CounterConn(conn, readCounter, writeCounter) + return func(n int64) { + for _, c := range readCounter { + c.Add(n) + } + }, func(n int64) { + for _, c := range writeCounter { + c.Add(n) + } + } } -func (s *StatsService) RoutedPacketConnection(inbound string, outbound string, user string, conn N.PacketConn) N.PacketConn { - var readCounter []*atomic.Int64 - var writeCounter []*atomic.Int64 - countInbound := inbound != "" && s.inbounds[inbound] - countOutbound := outbound != "" && s.outbounds[outbound] - countUser := user != "" && s.users[user] - if !countInbound && !countOutbound && !countUser { - return conn +func (s *StatsService) WithConnCounters(inbound, outbound, user string) adapter.ConnAdapter[net.Conn] { + rd, wr := s.getPacketCounters(inbound, outbound, user) + return func(conn net.Conn) net.Conn { + return bufio.NewCounterConn(conn, []N.CountFunc{rd}, []N.CountFunc{wr}) } - s.access.Lock() - if countInbound { - readCounter = append(readCounter, s.loadOrCreateCounter("inbound>>>"+inbound+">>>traffic>>>uplink")) - writeCounter = append(writeCounter, s.loadOrCreateCounter("inbound>>>"+inbound+">>>traffic>>>downlink")) +} + +func (s *StatsService) WithPacketConnCounters(inbound, outbound, user string) adapter.ConnAdapter[N.PacketConn] { + rd, wr := s.getPacketCounters(inbound, outbound, user) + return func(conn N.PacketConn) N.PacketConn { + return bufio.NewCounterPacketConn(conn, []N.CountFunc{rd}, []N.CountFunc{wr}) } - if countOutbound { - readCounter = append(readCounter, s.loadOrCreateCounter("outbound>>>"+outbound+">>>traffic>>>uplink")) - writeCounter = append(writeCounter, s.loadOrCreateCounter("outbound>>>"+outbound+">>>traffic>>>downlink")) - } - if countUser { - readCounter = append(readCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>uplink")) - writeCounter = append(writeCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>downlink")) - } - s.access.Unlock() - return bufio.NewInt64CounterPacketConn(conn, readCounter, writeCounter) } func (s *StatsService) GetStats(ctx context.Context, request *GetStatsRequest) (*GetStatsResponse, error) { diff --git a/go.mod b/go.mod index c259463c..47fd8897 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,7 @@ require ( golang.org/x/sys v0.22.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 google.golang.org/grpc v1.63.2 - google.golang.org/protobuf v1.33.0 + google.golang.org/protobuf v1.34.2 howett.net/plist v1.0.1 ) @@ -59,6 +59,8 @@ require ( require ( github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/brotli v1.0.6 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gaukas/godicttls v0.0.4 // indirect @@ -72,15 +74,20 @@ require ( github.com/hashicorp/yamux v0.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect - github.com/klauspost/compress v1.17.4 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/libdns/libdns v0.2.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.4.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/onsi/ginkgo/v2 v2.9.7 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.20.2 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect @@ -96,7 +103,7 @@ require ( golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.23.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect - gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index ee79e875..8f0bfc8f 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,12 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc= github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -53,9 +57,12 @@ github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtL github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -78,6 +85,8 @@ github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30= github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE= github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss= @@ -91,6 +100,14 @@ github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= +github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= @@ -213,9 +230,12 @@ google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/include/metricapi.go b/include/metricapi.go new file mode 100644 index 00000000..577e5670 --- /dev/null +++ b/include/metricapi.go @@ -0,0 +1,5 @@ +//go:build with_metric_api + +package include + +import _ "github.com/sagernet/sing-box/experimental/metrics" diff --git a/include/metricapi_stub.go b/include/metricapi_stub.go new file mode 100644 index 00000000..a70ca61f --- /dev/null +++ b/include/metricapi_stub.go @@ -0,0 +1,17 @@ +//go:build !with_metric_api + +package include + +import ( + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/experimental" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func init() { + experimental.RegisterMetricServerConstructor(func(logger log.Logger, options option.MetricOptions) (adapter.MetricService, error) { + return nil, E.New(`metric api is not included in this build, rebuild with -tags with_metric_api`) + }) +} diff --git a/option/experimental.go b/option/experimental.go index 6ab66385..fda17c5b 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -5,6 +5,7 @@ type ExperimentalOptions struct { ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` Debug *DebugOptions `json:"debug,omitempty"` + MetricAPI *MetricOptions `json:"metrics,omitempty"` } type CacheFileOptions struct { @@ -50,3 +51,12 @@ type V2RayStatsServiceOptions struct { Outbounds []string `json:"outbounds,omitempty"` Users []string `json:"users,omitempty"` } + +type MetricOptions struct { + Listen string `json:"listen,omitempty"` + Path string `json:"path,omitempty"` +} + +func (m *MetricOptions) Enabled() bool { + return m != nil && m.Listen != "" +} diff --git a/route/router.go b/route/router.go index 742f5bd2..71177255 100644 --- a/route/router.go +++ b/route/router.go @@ -93,6 +93,7 @@ type Router struct { pauseManager pause.Manager clashServer adapter.ClashServer v2rayServer adapter.V2RayServer + metricServer adapter.MetricService platformInterface platform.Interface needWIFIState bool needPackageManager bool @@ -921,9 +922,12 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad } if r.v2rayServer != nil { if statsService := r.v2rayServer.StatsService(); statsService != nil { - conn = statsService.RoutedConnection(metadata.Inbound, detour.Tag(), metadata.User, conn) + conn = statsService.WithConnCounters(metadata.Inbound, detour.Tag(), metadata.User)(conn) } } + if r.metricServer != nil { + conn = r.metricServer.WithConnCounters(metadata.Inbound, detour.Tag(), metadata.User)(conn) + } return detour.NewConnection(ctx, conn, metadata) } @@ -1097,9 +1101,12 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m } if r.v2rayServer != nil { if statsService := r.v2rayServer.StatsService(); statsService != nil { - conn = statsService.RoutedPacketConnection(metadata.Inbound, detour.Tag(), metadata.User, conn) + conn = statsService.WithPacketConnCounters(metadata.Inbound, detour.Tag(), metadata.User)(conn) } } + if r.metricServer != nil { + conn = r.metricServer.WithPacketConnCounters(metadata.Inbound, detour.Tag(), metadata.User)(conn) + } if metadata.FakeIP { conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination) } @@ -1265,6 +1272,10 @@ func (r *Router) SetV2RayServer(server adapter.V2RayServer) { r.v2rayServer = server } +func (r *Router) SetMetricServer(server adapter.MetricService) { + r.metricServer = server +} + func (r *Router) OnPackagesUpdated(packages int, sharedUsers int) { r.logger.Info("updated packages list: ", packages, " packages, ", sharedUsers, " shared users") }