V2Ray API
يوفر V2Ray API واجهة إحصائيات ومراقبة نظام قائمة على gRPC، متوافقة مع بروتوكول خدمة إحصائيات V2Ray. يمكّن من تتبع حركة المرور لكل منفذ وارد، ومنفذ صادر، ولكل مستخدم.
المصدر: experimental/v2rayapi/
التسجيل
مثل Clash API، يسجل V2Ray API عبر init() مع حماية بعلامة بناء:
// 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`)
})
}هندسة الخادم
type Server struct {
logger log.Logger
listen string // مثل "127.0.0.1:10085"
tcpListener net.Listener
grpcServer *grpc.Server
statsService *StatsService
}ينشئ الخادم خادم gRPC ببيانات اعتماد غير آمنة (بدون TLS) ويسجل 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
}تجاوز اسم الخدمة
يتم تجاوز اسم واصف خدمة gRPC ليتطابق مع اصطلاح تسمية V2Ray:
func init() {
StatsService_ServiceDesc.ServiceName = "v2ray.core.app.stats.command.StatsService"
}هذا يضمن التوافق مع أدوات عميل V2Ray التي تتوقع اسم الخدمة المحدد هذا.
خدمة الإحصائيات
التهيئة
type StatsService struct {
createdAt time.Time
inbounds map[string]bool // وسوم المنافذ الواردة المتتبعة
outbounds map[string]bool // وسوم المنافذ الصادرة المتتبعة
users map[string]bool // أسماء المستخدمين المتتبعة
access sync.Mutex
counters map[string]*atomic.Int64
}يتم تتبع فقط المنافذ الواردة والصادرة والمستخدمين المدرجين صراحة في التهيئة:
{
"experimental": {
"v2ray_api": {
"listen": "127.0.0.1:10085",
"stats": {
"enabled": true,
"inbounds": ["vmess-in"],
"outbounds": ["proxy", "direct"],
"users": ["user1", "user2"]
}
}
}
}اصطلاح تسمية العدادات
تتبع العدادات نمط تسمية V2Ray المفصول بـ >>>:
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>>>downlinkتغليف الاتصال
تنفذ خدمة الإحصائيات adapter.ConnectionTracker، وتغلف الاتصالات الموجهة بعدادات بايت:
func (s *StatsService) RoutedConnection(ctx, conn, metadata, matchedRule, matchOutbound) net.Conn {
inbound := metadata.Inbound
user := metadata.User
outbound := matchOutbound.Tag()
// بناء قوائم العدادات للكيانات المتتبعة المطابقة
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 // لا حاجة للتتبع، إرجاع بدون تغليف
}
return bufio.NewInt64CounterConn(conn, readCounter, writeCounter)
}نفس المنطق ينطبق على RoutedPacketConnection لحركة مرور UDP.
بروتوكول gRPC
تعريف Proto
syntax = "proto3";
package experimental.v2rayapi;
// مسجل كـ "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; // اسم العداد (مثل "inbound>>>vmess-in>>>traffic>>>uplink")
bool reset = 2; // إعادة تعيين العداد بعد القراءة
}
message Stat {
string name = 1;
int64 value = 2;
}
message QueryStatsRequest {
string pattern = 1; // نمط واحد مهمل
bool reset = 2;
repeated string patterns = 3; // أنماط متعددة
bool regexp = 4; // استخدام مطابقة التعبير النمطي
}
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
يسترجع عداداً واحداً بالاسم الدقيق:
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) // قراءة وإعادة تعيين ذرية
} else {
value = counter.Load()
}
return &GetStatsResponse{Stat: &Stat{Name: request.Name, Value: value}}, nil
}QueryStats
يستعلم عن عدة عدادات بمطابقة الأنماط:
func (s *StatsService) QueryStats(ctx, request) (*QueryStatsResponse, error) {
// ثلاثة أوضاع:
// 1. بدون أنماط: إرجاع جميع العدادات
// 2. Regexp=true: تجميع الأنماط كتعبيرات نمطية، مطابقة أسماء العدادات
// 3. Regexp=false: استخدام strings.Contains للمطابقة الجزئية
// إذا reset=true، مبادلة كل عداد مطابق إلى 0 ذرياً
}GetSysStats
يُرجع إحصائيات وقت تشغيل Go:
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
}دورة حياة البدء
يبدأ خادم gRPC في مرحلة PostStart:
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
}ملاحظات إعادة التنفيذ
- يجب أن تستخدم خدمة gRPC اسم الخدمة
v2ray.core.app.stats.command.StatsServiceللتوافق مع أدوات عميل V2Ray - تسمية العدادات تتبع اصطلاح
entity>>>tag>>>traffic>>>directionحيث الاتجاه هوuplink(قراءات العميل / البيانات المرسلة للمنبع) أوdownlink(كتابات العميل / البيانات المستلمة من المنبع) - العدادات تُنشأ بشكل كسول عند أول اتصال -- لا تكون موجودة مسبقاً عند بدء التشغيل
- علم
resetفي كل منGetStatsوQueryStatsيبادل العداد ذرياً إلى 0 ويُرجع القيمة القديمة QueryStatsبدون أنماط يُرجع جميع العدادات، وهو ما يمكن استخدامه للوحات المراقبة- خدمة الإحصائيات تغلف فقط الاتصالات التي تظهر وسوم منافذها الواردة/الصادرة/مستخدميها في قوائم التتبع المهيأة -- الاتصالات التي لا تطابق أي كيان متتبع تمر دون حمل إضافي
- كل من اتصالات TCP (
net.Conn) و UDP (N.PacketConn) تُتتبع بأنواع أغلفة عدادات منفصلة