V2Ray API
The V2Ray API provides a gRPC-based statistics and system monitoring interface, compatible with the V2Ray stats service protocol. It enables per-inbound, per-outbound, and per-user traffic tracking.
Source: experimental/v2rayapi/
Registration
Like the Clash API, the V2Ray API registers via init() with a build tag guard:
// v2rayapi.go (with_v2ray_api build tag)
func init() {
experimental.RegisterV2RayServerConstructor(NewServer)
}
// v2rayapi_stub.go (!with_v2ray_api)
func init() {
experimental.RegisterV2RayServerConstructor(func(...) (adapter.V2RayServer, error) {
return nil, E.New(`v2ray api is not included in this build, rebuild with -tags with_v2ray_api`)
})
}Server Architecture
type Server struct {
logger log.Logger
listen string // e.g., "127.0.0.1:10085"
tcpListener net.Listener
grpcServer *grpc.Server
statsService *StatsService
}The server creates a gRPC server with insecure credentials (no TLS) and registers the StatsService:
func NewServer(logger, options) (adapter.V2RayServer, error) {
grpcServer := grpc.NewServer(grpc.Creds(insecure.NewCredentials()))
statsService := NewStatsService(options.Stats)
if statsService != nil {
RegisterStatsServiceServer(grpcServer, statsService)
}
return &Server{grpcServer: grpcServer, statsService: statsService}, nil
}Service Name Override
The gRPC service descriptor name is overridden to match V2Ray's naming convention:
func init() {
StatsService_ServiceDesc.ServiceName = "v2ray.core.app.stats.command.StatsService"
}This ensures compatibility with V2Ray client tools that expect this specific service name.
Stats Service
Configuration
type StatsService struct {
createdAt time.Time
inbounds map[string]bool // tracked inbound tags
outbounds map[string]bool // tracked outbound tags
users map[string]bool // tracked user names
access sync.Mutex
counters map[string]*atomic.Int64
}Only inbounds, outbounds, and users explicitly listed in the configuration are tracked:
{
"experimental": {
"v2ray_api": {
"listen": "127.0.0.1:10085",
"stats": {
"enabled": true,
"inbounds": ["vmess-in"],
"outbounds": ["proxy", "direct"],
"users": ["user1", "user2"]
}
}
}
}Counter Naming Convention
Counters follow V2Ray's >>> delimited naming scheme:
inbound>>>vmess-in>>>traffic>>>uplink
inbound>>>vmess-in>>>traffic>>>downlink
outbound>>>proxy>>>traffic>>>uplink
outbound>>>proxy>>>traffic>>>downlink
user>>>user1>>>traffic>>>uplink
user>>>user1>>>traffic>>>downlinkConnection Wrapping
The stats service implements adapter.ConnectionTracker, wrapping routed connections with byte counters:
func (s *StatsService) RoutedConnection(ctx, conn, metadata, matchedRule, matchOutbound) net.Conn {
inbound := metadata.Inbound
user := metadata.User
outbound := matchOutbound.Tag()
// Build counter lists for matching tracked entities
var readCounter, writeCounter []*atomic.Int64
if inbound != "" && s.inbounds[inbound] {
readCounter = append(readCounter, s.loadOrCreateCounter("inbound>>>"+inbound+">>>traffic>>>uplink"))
writeCounter = append(writeCounter, s.loadOrCreateCounter("inbound>>>"+inbound+">>>traffic>>>downlink"))
}
if outbound != "" && s.outbounds[outbound] {
readCounter = append(readCounter, s.loadOrCreateCounter("outbound>>>"+outbound+">>>traffic>>>uplink"))
writeCounter = append(writeCounter, s.loadOrCreateCounter("outbound>>>"+outbound+">>>traffic>>>downlink"))
}
if user != "" && s.users[user] {
readCounter = append(readCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>uplink"))
writeCounter = append(writeCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>downlink"))
}
if !countInbound && !countOutbound && !countUser {
return conn // no tracking needed, return unwrapped
}
return bufio.NewInt64CounterConn(conn, readCounter, writeCounter)
}The same logic applies to RoutedPacketConnection for UDP traffic.
gRPC Protocol
Proto Definition
syntax = "proto3";
package experimental.v2rayapi;
// Registered as "v2ray.core.app.stats.command.StatsService"
service StatsService {
rpc GetStats(GetStatsRequest) returns (GetStatsResponse) {}
rpc QueryStats(QueryStatsRequest) returns (QueryStatsResponse) {}
rpc GetSysStats(SysStatsRequest) returns (SysStatsResponse) {}
}
message GetStatsRequest {
string name = 1; // Counter name (e.g., "inbound>>>vmess-in>>>traffic>>>uplink")
bool reset = 2; // Reset counter after reading
}
message Stat {
string name = 1;
int64 value = 2;
}
message QueryStatsRequest {
string pattern = 1; // Deprecated single pattern
bool reset = 2;
repeated string patterns = 3; // Multiple patterns
bool regexp = 4; // Use regex matching
}
message SysStatsResponse {
uint32 NumGoroutine = 1;
uint32 NumGC = 2;
uint64 Alloc = 3;
uint64 TotalAlloc = 4;
uint64 Sys = 5;
uint64 Mallocs = 6;
uint64 Frees = 7;
uint64 LiveObjects = 8;
uint64 PauseTotalNs = 9;
uint32 Uptime = 10;
}GetStats
Retrieves a single counter by exact name:
func (s *StatsService) GetStats(ctx, request) (*GetStatsResponse, error) {
counter, loaded := s.counters[request.Name]
if !loaded {
return nil, E.New(request.Name, " not found.")
}
var value int64
if request.Reset_ {
value = counter.Swap(0) // atomic read-and-reset
} else {
value = counter.Load()
}
return &GetStatsResponse{Stat: &Stat{Name: request.Name, Value: value}}, nil
}QueryStats
Queries multiple counters by pattern matching:
func (s *StatsService) QueryStats(ctx, request) (*QueryStatsResponse, error) {
// Three modes:
// 1. No patterns: return all counters
// 2. Regexp=true: compile patterns as regex, match counter names
// 3. Regexp=false: use strings.Contains for substring matching
// If reset=true, atomically swap each matched counter to 0
}GetSysStats
Returns Go runtime statistics:
func (s *StatsService) GetSysStats(ctx, request) (*SysStatsResponse, error) {
var rtm runtime.MemStats
runtime.ReadMemStats(&rtm)
return &SysStatsResponse{
Uptime: uint32(time.Since(s.createdAt).Seconds()),
NumGoroutine: uint32(runtime.NumGoroutine()),
Alloc: rtm.Alloc,
TotalAlloc: rtm.TotalAlloc,
Sys: rtm.Sys,
Mallocs: rtm.Mallocs,
Frees: rtm.Frees,
LiveObjects: rtm.Mallocs - rtm.Frees,
NumGC: rtm.NumGC,
PauseTotalNs: rtm.PauseTotalNs,
}, nil
}Start Lifecycle
The gRPC server starts in the PostStart stage:
func (s *Server) Start(stage adapter.StartStage) error {
if stage != adapter.StartStatePostStart {
return nil
}
listener, _ := net.Listen("tcp", s.listen)
go s.grpcServer.Serve(listener)
return nil
}Reimplementation Notes
- The gRPC service must use the service name
v2ray.core.app.stats.command.StatsServicefor compatibility with V2Ray client tools - Counter naming follows the
entity>>>tag>>>traffic>>>directionconvention where direction isuplink(client reads / data sent to upstream) ordownlink(client writes / data received from upstream) - Counters are lazily created on first connection -- they do not pre-exist at startup
- The
resetflag on bothGetStatsandQueryStatsatomically swaps the counter to 0 and returns the old value QueryStatswith no patterns returns all counters, which can be used for monitoring dashboards- The stats service only wraps connections whose inbound/outbound/user tags appear in the configured tracking lists -- connections not matching any tracked entity pass through without overhead
- Both TCP (
net.Conn) and UDP (N.PacketConn) connections are tracked with separate counter wrapper types