Skip to content

V2Ray API

يوفر V2Ray API واجهة إحصائيات ومراقبة نظام قائمة على gRPC، متوافقة مع بروتوكول خدمة إحصائيات V2Ray. يمكّن من تتبع حركة المرور لكل منفذ وارد، ومنفذ صادر، ولكل مستخدم.

المصدر: experimental/v2rayapi/

التسجيل

مثل Clash API، يسجل V2Ray API عبر init() مع حماية بعلامة بناء:

go
// 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`)
    })
}

هندسة الخادم

go
type Server struct {
    logger       log.Logger
    listen       string           // مثل "127.0.0.1:10085"
    tcpListener  net.Listener
    grpcServer   *grpc.Server
    statsService *StatsService
}

ينشئ الخادم خادم gRPC ببيانات اعتماد غير آمنة (بدون TLS) ويسجل StatsService:

go
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:

go
func init() {
    StatsService_ServiceDesc.ServiceName = "v2ray.core.app.stats.command.StatsService"
}

هذا يضمن التوافق مع أدوات عميل V2Ray التي تتوقع اسم الخدمة المحدد هذا.

خدمة الإحصائيات

التهيئة

go
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
}

يتم تتبع فقط المنافذ الواردة والصادرة والمستخدمين المدرجين صراحة في التهيئة:

json
{
  "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، وتغلف الاتصالات الموجهة بعدادات بايت:

go
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

protobuf
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

يسترجع عداداً واحداً بالاسم الدقيق:

go
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

يستعلم عن عدة عدادات بمطابقة الأنماط:

go
func (s *StatsService) QueryStats(ctx, request) (*QueryStatsResponse, error) {
    // ثلاثة أوضاع:
    // 1. بدون أنماط: إرجاع جميع العدادات
    // 2. Regexp=true: تجميع الأنماط كتعبيرات نمطية، مطابقة أسماء العدادات
    // 3. Regexp=false: استخدام strings.Contains للمطابقة الجزئية

    // إذا reset=true، مبادلة كل عداد مطابق إلى 0 ذرياً
}

GetSysStats

يُرجع إحصائيات وقت تشغيل Go:

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:

go
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
}

ملاحظات إعادة التنفيذ

  1. يجب أن تستخدم خدمة gRPC اسم الخدمة v2ray.core.app.stats.command.StatsService للتوافق مع أدوات عميل V2Ray
  2. تسمية العدادات تتبع اصطلاح entity>>>tag>>>traffic>>>direction حيث الاتجاه هو uplink (قراءات العميل / البيانات المرسلة للمنبع) أو downlink (كتابات العميل / البيانات المستلمة من المنبع)
  3. العدادات تُنشأ بشكل كسول عند أول اتصال -- لا تكون موجودة مسبقاً عند بدء التشغيل
  4. علم reset في كل من GetStats و QueryStats يبادل العداد ذرياً إلى 0 ويُرجع القيمة القديمة
  5. QueryStats بدون أنماط يُرجع جميع العدادات، وهو ما يمكن استخدامه للوحات المراقبة
  6. خدمة الإحصائيات تغلف فقط الاتصالات التي تظهر وسوم منافذها الواردة/الصادرة/مستخدميها في قوائم التتبع المهيأة -- الاتصالات التي لا تطابق أي كيان متتبع تمر دون حمل إضافي
  7. كل من اتصالات TCP (net.Conn) و UDP (N.PacketConn) تُتتبع بأنواع أغلفة عدادات منفصلة