diff --git a/PasarGuardNodeBridge/abstract_node.py b/PasarGuardNodeBridge/abstract_node.py index df8e2ac..91a6559 100644 --- a/PasarGuardNodeBridge/abstract_node.py +++ b/PasarGuardNodeBridge/abstract_node.py @@ -41,6 +41,16 @@ async def get_stats( ) -> service.StatResponse | None: raise NotImplementedError + async def collect_usage(self, stat_type: service.StatType, timeout: int | None = None) -> service.UsageReceipt: + """Read a durable receipt without resetting counters; safe to repeat.""" + raise NotImplementedError + + async def acknowledge_usage( + self, stat_type: service.StatType, receipt_id: str, timeout: int | None = None + ) -> service.Empty: + """Release a receipt only after the caller durably stores it; safe to repeat.""" + raise NotImplementedError + @abstractmethod async def get_outbounds_latency(self, name: str = "", timeout: int | None = None) -> service.LatencyResponse | None: raise NotImplementedError diff --git a/PasarGuardNodeBridge/common/service.proto b/PasarGuardNodeBridge/common/service.proto index 738e7b9..4ee4fcc 100644 --- a/PasarGuardNodeBridge/common/service.proto +++ b/PasarGuardNodeBridge/common/service.proto @@ -58,6 +58,23 @@ message StatRequest { StatType type = 3; } +// Accounting reads never reset core counters. Receipts survive until the panel +// acknowledges them after committing its own durable transaction. +message UsageRequest { + StatType type = 1; +} + +message UsageReceipt { + string receipt_id = 1; + int64 collected_at = 2; // Unix milliseconds + repeated Stat stats = 3; +} + +message UsageAck { + StatType type = 1; + string receipt_id = 2; +} + message OnlineStatResponse { string name = 1; int64 value = 2; @@ -220,6 +237,8 @@ service NodeService { rpc GetBackendStats(Empty) returns (BackendStatsResponse) {} rpc GetStats(StatRequest) returns (StatResponse) {} + rpc CollectUsage(UsageRequest) returns (UsageReceipt) {} + rpc AcknowledgeUsage(UsageAck) returns (Empty) {} rpc GetOutboundsLatency(LatencyRequest) returns (LatencyResponse) {} rpc GetUserOnlineStats(StatRequest) returns (OnlineStatResponse) {} diff --git a/PasarGuardNodeBridge/common/service_grpc.py b/PasarGuardNodeBridge/common/service_grpc.py index 7bcba74..ceddf1b 100644 --- a/PasarGuardNodeBridge/common/service_grpc.py +++ b/PasarGuardNodeBridge/common/service_grpc.py @@ -42,6 +42,14 @@ async def GetBackendStats(self, stream: 'grpclib.server.Stream[PasarGuardNodeBri async def GetStats(self, stream: 'grpclib.server.Stream[PasarGuardNodeBridge.common.service_pb2.StatRequest, PasarGuardNodeBridge.common.service_pb2.StatResponse]') -> None: pass + @abc.abstractmethod + async def CollectUsage(self, stream: 'grpclib.server.Stream[PasarGuardNodeBridge.common.service_pb2.UsageRequest, PasarGuardNodeBridge.common.service_pb2.UsageReceipt]') -> None: + pass + + @abc.abstractmethod + async def AcknowledgeUsage(self, stream: 'grpclib.server.Stream[PasarGuardNodeBridge.common.service_pb2.UsageAck, PasarGuardNodeBridge.common.service_pb2.Empty]') -> None: + pass + @abc.abstractmethod async def GetOutboundsLatency(self, stream: 'grpclib.server.Stream[PasarGuardNodeBridge.common.service_pb2.LatencyRequest, PasarGuardNodeBridge.common.service_pb2.LatencyResponse]') -> None: pass @@ -134,6 +142,18 @@ def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]: PasarGuardNodeBridge.common.service_pb2.StatRequest, PasarGuardNodeBridge.common.service_pb2.StatResponse, ), + '/service.NodeService/CollectUsage': grpclib.const.Handler( + self.CollectUsage, + grpclib.const.Cardinality.UNARY_UNARY, + PasarGuardNodeBridge.common.service_pb2.UsageRequest, + PasarGuardNodeBridge.common.service_pb2.UsageReceipt, + ), + '/service.NodeService/AcknowledgeUsage': grpclib.const.Handler( + self.AcknowledgeUsage, + grpclib.const.Cardinality.UNARY_UNARY, + PasarGuardNodeBridge.common.service_pb2.UsageAck, + PasarGuardNodeBridge.common.service_pb2.Empty, + ), '/service.NodeService/GetOutboundsLatency': grpclib.const.Handler( self.GetOutboundsLatency, grpclib.const.Cardinality.UNARY_UNARY, @@ -254,6 +274,18 @@ def __init__(self, channel: grpclib.client.Channel) -> None: PasarGuardNodeBridge.common.service_pb2.StatRequest, PasarGuardNodeBridge.common.service_pb2.StatResponse, ) + self.CollectUsage = grpclib.client.UnaryUnaryMethod( + channel, + '/service.NodeService/CollectUsage', + PasarGuardNodeBridge.common.service_pb2.UsageRequest, + PasarGuardNodeBridge.common.service_pb2.UsageReceipt, + ) + self.AcknowledgeUsage = grpclib.client.UnaryUnaryMethod( + channel, + '/service.NodeService/AcknowledgeUsage', + PasarGuardNodeBridge.common.service_pb2.UsageAck, + PasarGuardNodeBridge.common.service_pb2.Empty, + ) self.GetOutboundsLatency = grpclib.client.UnaryUnaryMethod( channel, '/service.NodeService/GetOutboundsLatency', diff --git a/PasarGuardNodeBridge/common/service_pb2.py b/PasarGuardNodeBridge/common/service_pb2.py index f35327c..06ec94e 100644 --- a/PasarGuardNodeBridge/common/service_pb2.py +++ b/PasarGuardNodeBridge/common/service_pb2.py @@ -24,7 +24,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n)PasarGuardNodeBridge/common/service.proto\x12\x07service\"\x07\n\x05\x45mpty\"O\n\x10\x42\x61seInfoResponse\x12\x0f\n\x07started\x18\x01 \x01(\x08\x12\x14\n\x0c\x63ore_version\x18\x02 \x01(\t\x12\x14\n\x0cnode_version\x18\x03 \x01(\t\"\x89\x01\n\x07\x42\x61\x63kend\x12\"\n\x04type\x18\x01 \x01(\x0e\x32\x14.service.BackendType\x12\x0e\n\x06\x63onfig\x18\x02 \x01(\t\x12\x1c\n\x05users\x18\x03 \x03(\x0b\x32\r.service.User\x12\x12\n\nkeep_alive\x18\x04 \x01(\x04\x12\x18\n\x10\x65xclude_inbounds\x18\x05 \x03(\t\"\x15\n\x03Log\x12\x0e\n\x06\x64\x65tail\x18\x01 \x01(\t\"?\n\x04Stat\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0c\n\x04link\x18\x03 \x01(\t\x12\r\n\x05value\x18\x04 \x01(\x03\",\n\x0cStatResponse\x12\x1c\n\x05stats\x18\x01 \x03(\x0b\x32\r.service.Stat\"K\n\x0bStatRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05reset\x18\x02 \x01(\x08\x12\x1f\n\x04type\x18\x03 \x01(\x0e\x32\x11.service.StatType\"1\n\x12OnlineStatResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x03\"\x8f\x01\n\x19StatsOnlineIpListResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x38\n\x03ips\x18\x02 \x03(\x0b\x32+.service.StatsOnlineIpListResponse.IpsEntry\x1a*\n\x08IpsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x03:\x02\x38\x01\"\x82\x01\n\x07Latency\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05\x61live\x18\x02 \x01(\x08\x12\r\n\x05\x64\x65lay\x18\x03 \x01(\x03\x12\x0c\n\x04link\x18\x04 \x01(\t\x12\x16\n\x0elast_seen_time\x18\x05 \x01(\x03\x12\x15\n\rlast_try_time\x18\x06 \x01(\x03\x12\x0e\n\x06source\x18\x07 \x01(\t\"\x1e\n\x0eLatencyRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"6\n\x0fLatencyResponse\x12#\n\tlatencies\x18\x01 \x03(\x0b\x32\x10.service.Latency\"\xcc\x01\n\x14\x42\x61\x63kendStatsResponse\x12\x15\n\rnum_goroutine\x18\x01 \x01(\r\x12\x0e\n\x06num_gc\x18\x02 \x01(\r\x12\r\n\x05\x61lloc\x18\x03 \x01(\x04\x12\x13\n\x0btotal_alloc\x18\x04 \x01(\x04\x12\x0b\n\x03sys\x18\x05 \x01(\x04\x12\x0f\n\x07mallocs\x18\x06 \x01(\x04\x12\r\n\x05\x66rees\x18\x07 \x01(\x04\x12\x14\n\x0clive_objects\x18\x08 \x01(\x04\x12\x16\n\x0epause_total_ns\x18\t \x01(\x04\x12\x0e\n\x06uptime\x18\n \x01(\r\"\xb4\x01\n\x13SystemStatsResponse\x12\x11\n\tmem_total\x18\x01 \x01(\x04\x12\x10\n\x08mem_used\x18\x02 \x01(\x04\x12\x11\n\tcpu_cores\x18\x03 \x01(\x04\x12\x11\n\tcpu_usage\x18\x04 \x01(\x01\x12 \n\x18incoming_bandwidth_speed\x18\x05 \x01(\x04\x12 \n\x18outgoing_bandwidth_speed\x18\x06 \x01(\x04\x12\x0e\n\x06uptime\x18\x07 \x01(\x04\"\x13\n\x05Vmess\x12\n\n\x02id\x18\x01 \x01(\t\"!\n\x05Vless\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04\x66low\x18\x02 \x01(\t\"\x1a\n\x06Trojan\x12\x10\n\x08password\x18\x01 \x01(\t\"/\n\x0bShadowsocks\x12\x10\n\x08password\x18\x01 \x01(\t\x12\x0e\n\x06method\x18\x02 \x01(\t\"1\n\tWireguard\x12\x12\n\npublic_key\x18\x01 \x01(\t\x12\x10\n\x08peer_ips\x18\x02 \x03(\t\"\x18\n\x08Hysteria\x12\x0c\n\x04\x61uth\x18\x01 \x01(\t\"\xdd\x01\n\x05Proxy\x12\x1d\n\x05vmess\x18\x01 \x01(\x0b\x32\x0e.service.Vmess\x12\x1d\n\x05vless\x18\x02 \x01(\x0b\x32\x0e.service.Vless\x12\x1f\n\x06trojan\x18\x03 \x01(\x0b\x32\x0f.service.Trojan\x12)\n\x0bshadowsocks\x18\x04 \x01(\x0b\x32\x14.service.Shadowsocks\x12%\n\twireguard\x18\x05 \x01(\x0b\x32\x12.service.Wireguard\x12#\n\x08hysteria\x18\x06 \x01(\x0b\x32\x11.service.Hysteria\"H\n\x04User\x12\r\n\x05\x65mail\x18\x01 \x01(\t\x12\x1f\n\x07proxies\x18\x02 \x01(\x0b\x32\x0e.service.Proxy\x12\x10\n\x08inbounds\x18\x03 \x03(\t\"%\n\x05Users\x12\x1c\n\x05users\x18\x01 \x03(\x0b\x32\r.service.User\"G\n\nUsersChunk\x12\x1c\n\x05users\x18\x01 \x03(\x0b\x32\r.service.User\x12\r\n\x05index\x18\x02 \x01(\x04\x12\x0c\n\x04last\x18\x03 \x01(\x08\"5\n\x0bRoutingRule\x12\x14\n\x0coutbound_tag\x18\x01 \x01(\t\x12\x10\n\x08rule_tag\x18\x02 \x01(\t\";\n\x14RoutingRulesResponse\x12#\n\x05rules\x18\x01 \x03(\x0b\x32\x14.service.RoutingRule\"\"\n\x13\x42\x61lancerInfoRequest\x12\x0b\n\x03tag\x18\x01 \x01(\t\"I\n\x14\x42\x61lancerInfoResponse\x12\x17\n\x0foverride_target\x18\x01 \x01(\t\x12\x18\n\x10principle_target\x18\x02 \x03(\t\"\xba\x02\n\x10TestRouteRequest\x12\x13\n\x0binbound_tag\x18\x01 \x01(\t\x12\x0f\n\x07network\x18\x02 \x01(\t\x12\x11\n\ttarget_ip\x18\x03 \x01(\t\x12\x15\n\rtarget_domain\x18\x04 \x01(\t\x12\x13\n\x0btarget_port\x18\x05 \x01(\r\x12\x10\n\x08protocol\x18\x06 \x01(\t\x12\x0c\n\x04user\x18\x07 \x01(\t\x12=\n\nattributes\x18\x08 \x03(\x0b\x32).service.TestRouteRequest.AttributesEntry\x12\x17\n\x0f\x66ield_selectors\x18\t \x03(\t\x12\x16\n\x0epublish_result\x18\n \x01(\x08\x1a\x31\n\x0f\x41ttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"}\n\x0bRouteResult\x12\x14\n\x0coutbound_tag\x18\x01 \x01(\t\x12\x1b\n\x13outbound_group_tags\x18\x02 \x03(\t\x12\x13\n\x0binbound_tag\x18\x03 \x01(\t\x12\x0f\n\x07network\x18\x04 \x01(\t\x12\x15\n\rtarget_domain\x18\x05 \x01(\t\";\n\x15\x41\x64\x64RoutingRuleRequest\x12\x0c\n\x04rule\x18\x01 \x01(\t\x12\x14\n\x0cshould_reset\x18\x02 \x01(\x08\",\n\x18RemoveRoutingRuleRequest\x12\x10\n\x08rule_tag\x18\x01 \x01(\t\"E\n\x1dOverrideBalancerTargetRequest\x12\x14\n\x0c\x62\x61lancer_tag\x18\x01 \x01(\t\x12\x0e\n\x06target\x18\x02 \x01(\t*&\n\x0b\x42\x61\x63kendType\x12\x08\n\x04XRAY\x10\x00\x12\r\n\tWIREGUARD\x10\x01*_\n\x08StatType\x12\r\n\tOutbounds\x10\x00\x12\x0c\n\x08Outbound\x10\x01\x12\x0c\n\x08Inbounds\x10\x02\x12\x0b\n\x07Inbound\x10\x03\x12\r\n\tUsersStat\x10\x04\x12\x0c\n\x08UserStat\x10\x05\x32\xdc\t\n\x0bNodeService\x12\x36\n\x05Start\x12\x10.service.Backend\x1a\x19.service.BaseInfoResponse\"\x00\x12(\n\x04Stop\x12\x0e.service.Empty\x1a\x0e.service.Empty\"\x00\x12:\n\x0bGetBaseInfo\x12\x0e.service.Empty\x1a\x19.service.BaseInfoResponse\"\x00\x12+\n\x07GetLogs\x12\x0e.service.Empty\x1a\x0c.service.Log\"\x00\x30\x01\x12@\n\x0eGetSystemStats\x12\x0e.service.Empty\x1a\x1c.service.SystemStatsResponse\"\x00\x12\x42\n\x0fGetBackendStats\x12\x0e.service.Empty\x1a\x1d.service.BackendStatsResponse\"\x00\x12\x39\n\x08GetStats\x12\x14.service.StatRequest\x1a\x15.service.StatResponse\"\x00\x12J\n\x13GetOutboundsLatency\x12\x17.service.LatencyRequest\x1a\x18.service.LatencyResponse\"\x00\x12I\n\x12GetUserOnlineStats\x12\x14.service.StatRequest\x1a\x1b.service.OnlineStatResponse\"\x00\x12V\n\x18GetUserOnlineIpListStats\x12\x14.service.StatRequest\x1a\".service.StatsOnlineIpListResponse\"\x00\x12-\n\x08SyncUser\x12\r.service.User\x1a\x0e.service.Empty\"\x00(\x01\x12-\n\tSyncUsers\x12\x0e.service.Users\x1a\x0e.service.Empty\"\x00\x12;\n\x10SyncUsersChunked\x12\x13.service.UsersChunk\x1a\x0e.service.Empty\"\x00(\x01\x12\x43\n\x10ListRoutingRules\x12\x0e.service.Empty\x1a\x1d.service.RoutingRulesResponse\"\x00\x12P\n\x0fGetBalancerInfo\x12\x1c.service.BalancerInfoRequest\x1a\x1d.service.BalancerInfoResponse\"\x00\x12>\n\tTestRoute\x12\x19.service.TestRouteRequest\x1a\x14.service.RouteResult\"\x00\x12\x42\n\x0e\x41\x64\x64RoutingRule\x12\x1e.service.AddRoutingRuleRequest\x1a\x0e.service.Empty\"\x00\x12H\n\x11RemoveRoutingRule\x12!.service.RemoveRoutingRuleRequest\x1a\x0e.service.Empty\"\x00\x12R\n\x16OverrideBalancerTarget\x12&.service.OverrideBalancerTargetRequest\x1a\x0e.service.Empty\"\x00\x42#Z!github.com/pasarguard/node/commonb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n)PasarGuardNodeBridge/common/service.proto\x12\x07service\"\x07\n\x05\x45mpty\"O\n\x10\x42\x61seInfoResponse\x12\x0f\n\x07started\x18\x01 \x01(\x08\x12\x14\n\x0c\x63ore_version\x18\x02 \x01(\t\x12\x14\n\x0cnode_version\x18\x03 \x01(\t\"\x89\x01\n\x07\x42\x61\x63kend\x12\"\n\x04type\x18\x01 \x01(\x0e\x32\x14.service.BackendType\x12\x0e\n\x06\x63onfig\x18\x02 \x01(\t\x12\x1c\n\x05users\x18\x03 \x03(\x0b\x32\r.service.User\x12\x12\n\nkeep_alive\x18\x04 \x01(\x04\x12\x18\n\x10\x65xclude_inbounds\x18\x05 \x03(\t\"\x15\n\x03Log\x12\x0e\n\x06\x64\x65tail\x18\x01 \x01(\t\"?\n\x04Stat\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0c\n\x04link\x18\x03 \x01(\t\x12\r\n\x05value\x18\x04 \x01(\x03\",\n\x0cStatResponse\x12\x1c\n\x05stats\x18\x01 \x03(\x0b\x32\r.service.Stat\"K\n\x0bStatRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05reset\x18\x02 \x01(\x08\x12\x1f\n\x04type\x18\x03 \x01(\x0e\x32\x11.service.StatType\"/\n\x0cUsageRequest\x12\x1f\n\x04type\x18\x01 \x01(\x0e\x32\x11.service.StatType\"V\n\x0cUsageReceipt\x12\x12\n\nreceipt_id\x18\x01 \x01(\t\x12\x14\n\x0c\x63ollected_at\x18\x02 \x01(\x03\x12\x1c\n\x05stats\x18\x03 \x03(\x0b\x32\r.service.Stat\"?\n\x08UsageAck\x12\x1f\n\x04type\x18\x01 \x01(\x0e\x32\x11.service.StatType\x12\x12\n\nreceipt_id\x18\x02 \x01(\t\"1\n\x12OnlineStatResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x03\"\x8f\x01\n\x19StatsOnlineIpListResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x38\n\x03ips\x18\x02 \x03(\x0b\x32+.service.StatsOnlineIpListResponse.IpsEntry\x1a*\n\x08IpsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x03:\x02\x38\x01\"\x82\x01\n\x07Latency\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05\x61live\x18\x02 \x01(\x08\x12\r\n\x05\x64\x65lay\x18\x03 \x01(\x03\x12\x0c\n\x04link\x18\x04 \x01(\t\x12\x16\n\x0elast_seen_time\x18\x05 \x01(\x03\x12\x15\n\rlast_try_time\x18\x06 \x01(\x03\x12\x0e\n\x06source\x18\x07 \x01(\t\"\x1e\n\x0eLatencyRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"6\n\x0fLatencyResponse\x12#\n\tlatencies\x18\x01 \x03(\x0b\x32\x10.service.Latency\"\xcc\x01\n\x14\x42\x61\x63kendStatsResponse\x12\x15\n\rnum_goroutine\x18\x01 \x01(\r\x12\x0e\n\x06num_gc\x18\x02 \x01(\r\x12\r\n\x05\x61lloc\x18\x03 \x01(\x04\x12\x13\n\x0btotal_alloc\x18\x04 \x01(\x04\x12\x0b\n\x03sys\x18\x05 \x01(\x04\x12\x0f\n\x07mallocs\x18\x06 \x01(\x04\x12\r\n\x05\x66rees\x18\x07 \x01(\x04\x12\x14\n\x0clive_objects\x18\x08 \x01(\x04\x12\x16\n\x0epause_total_ns\x18\t \x01(\x04\x12\x0e\n\x06uptime\x18\n \x01(\r\"\xb4\x01\n\x13SystemStatsResponse\x12\x11\n\tmem_total\x18\x01 \x01(\x04\x12\x10\n\x08mem_used\x18\x02 \x01(\x04\x12\x11\n\tcpu_cores\x18\x03 \x01(\x04\x12\x11\n\tcpu_usage\x18\x04 \x01(\x01\x12 \n\x18incoming_bandwidth_speed\x18\x05 \x01(\x04\x12 \n\x18outgoing_bandwidth_speed\x18\x06 \x01(\x04\x12\x0e\n\x06uptime\x18\x07 \x01(\x04\"\x13\n\x05Vmess\x12\n\n\x02id\x18\x01 \x01(\t\"!\n\x05Vless\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04\x66low\x18\x02 \x01(\t\"\x1a\n\x06Trojan\x12\x10\n\x08password\x18\x01 \x01(\t\"/\n\x0bShadowsocks\x12\x10\n\x08password\x18\x01 \x01(\t\x12\x0e\n\x06method\x18\x02 \x01(\t\"1\n\tWireguard\x12\x12\n\npublic_key\x18\x01 \x01(\t\x12\x10\n\x08peer_ips\x18\x02 \x03(\t\"\x18\n\x08Hysteria\x12\x0c\n\x04\x61uth\x18\x01 \x01(\t\"\xdd\x01\n\x05Proxy\x12\x1d\n\x05vmess\x18\x01 \x01(\x0b\x32\x0e.service.Vmess\x12\x1d\n\x05vless\x18\x02 \x01(\x0b\x32\x0e.service.Vless\x12\x1f\n\x06trojan\x18\x03 \x01(\x0b\x32\x0f.service.Trojan\x12)\n\x0bshadowsocks\x18\x04 \x01(\x0b\x32\x14.service.Shadowsocks\x12%\n\twireguard\x18\x05 \x01(\x0b\x32\x12.service.Wireguard\x12#\n\x08hysteria\x18\x06 \x01(\x0b\x32\x11.service.Hysteria\"H\n\x04User\x12\r\n\x05\x65mail\x18\x01 \x01(\t\x12\x1f\n\x07proxies\x18\x02 \x01(\x0b\x32\x0e.service.Proxy\x12\x10\n\x08inbounds\x18\x03 \x03(\t\"%\n\x05Users\x12\x1c\n\x05users\x18\x01 \x03(\x0b\x32\r.service.User\"G\n\nUsersChunk\x12\x1c\n\x05users\x18\x01 \x03(\x0b\x32\r.service.User\x12\r\n\x05index\x18\x02 \x01(\x04\x12\x0c\n\x04last\x18\x03 \x01(\x08\"5\n\x0bRoutingRule\x12\x14\n\x0coutbound_tag\x18\x01 \x01(\t\x12\x10\n\x08rule_tag\x18\x02 \x01(\t\";\n\x14RoutingRulesResponse\x12#\n\x05rules\x18\x01 \x03(\x0b\x32\x14.service.RoutingRule\"\"\n\x13\x42\x61lancerInfoRequest\x12\x0b\n\x03tag\x18\x01 \x01(\t\"I\n\x14\x42\x61lancerInfoResponse\x12\x17\n\x0foverride_target\x18\x01 \x01(\t\x12\x18\n\x10principle_target\x18\x02 \x03(\t\"\xba\x02\n\x10TestRouteRequest\x12\x13\n\x0binbound_tag\x18\x01 \x01(\t\x12\x0f\n\x07network\x18\x02 \x01(\t\x12\x11\n\ttarget_ip\x18\x03 \x01(\t\x12\x15\n\rtarget_domain\x18\x04 \x01(\t\x12\x13\n\x0btarget_port\x18\x05 \x01(\r\x12\x10\n\x08protocol\x18\x06 \x01(\t\x12\x0c\n\x04user\x18\x07 \x01(\t\x12=\n\nattributes\x18\x08 \x03(\x0b\x32).service.TestRouteRequest.AttributesEntry\x12\x17\n\x0f\x66ield_selectors\x18\t \x03(\t\x12\x16\n\x0epublish_result\x18\n \x01(\x08\x1a\x31\n\x0f\x41ttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"}\n\x0bRouteResult\x12\x14\n\x0coutbound_tag\x18\x01 \x01(\t\x12\x1b\n\x13outbound_group_tags\x18\x02 \x03(\t\x12\x13\n\x0binbound_tag\x18\x03 \x01(\t\x12\x0f\n\x07network\x18\x04 \x01(\t\x12\x15\n\rtarget_domain\x18\x05 \x01(\t\";\n\x15\x41\x64\x64RoutingRuleRequest\x12\x0c\n\x04rule\x18\x01 \x01(\t\x12\x14\n\x0cshould_reset\x18\x02 \x01(\x08\",\n\x18RemoveRoutingRuleRequest\x12\x10\n\x08rule_tag\x18\x01 \x01(\t\"E\n\x1dOverrideBalancerTargetRequest\x12\x14\n\x0c\x62\x61lancer_tag\x18\x01 \x01(\t\x12\x0e\n\x06target\x18\x02 \x01(\t*&\n\x0b\x42\x61\x63kendType\x12\x08\n\x04XRAY\x10\x00\x12\r\n\tWIREGUARD\x10\x01*_\n\x08StatType\x12\r\n\tOutbounds\x10\x00\x12\x0c\n\x08Outbound\x10\x01\x12\x0c\n\x08Inbounds\x10\x02\x12\x0b\n\x07Inbound\x10\x03\x12\r\n\tUsersStat\x10\x04\x12\x0c\n\x08UserStat\x10\x05\x32\xd5\n\n\x0bNodeService\x12\x36\n\x05Start\x12\x10.service.Backend\x1a\x19.service.BaseInfoResponse\"\x00\x12(\n\x04Stop\x12\x0e.service.Empty\x1a\x0e.service.Empty\"\x00\x12:\n\x0bGetBaseInfo\x12\x0e.service.Empty\x1a\x19.service.BaseInfoResponse\"\x00\x12+\n\x07GetLogs\x12\x0e.service.Empty\x1a\x0c.service.Log\"\x00\x30\x01\x12@\n\x0eGetSystemStats\x12\x0e.service.Empty\x1a\x1c.service.SystemStatsResponse\"\x00\x12\x42\n\x0fGetBackendStats\x12\x0e.service.Empty\x1a\x1d.service.BackendStatsResponse\"\x00\x12\x39\n\x08GetStats\x12\x14.service.StatRequest\x1a\x15.service.StatResponse\"\x00\x12>\n\x0c\x43ollectUsage\x12\x15.service.UsageRequest\x1a\x15.service.UsageReceipt\"\x00\x12\x37\n\x10\x41\x63knowledgeUsage\x12\x11.service.UsageAck\x1a\x0e.service.Empty\"\x00\x12J\n\x13GetOutboundsLatency\x12\x17.service.LatencyRequest\x1a\x18.service.LatencyResponse\"\x00\x12I\n\x12GetUserOnlineStats\x12\x14.service.StatRequest\x1a\x1b.service.OnlineStatResponse\"\x00\x12V\n\x18GetUserOnlineIpListStats\x12\x14.service.StatRequest\x1a\".service.StatsOnlineIpListResponse\"\x00\x12-\n\x08SyncUser\x12\r.service.User\x1a\x0e.service.Empty\"\x00(\x01\x12-\n\tSyncUsers\x12\x0e.service.Users\x1a\x0e.service.Empty\"\x00\x12;\n\x10SyncUsersChunked\x12\x13.service.UsersChunk\x1a\x0e.service.Empty\"\x00(\x01\x12\x43\n\x10ListRoutingRules\x12\x0e.service.Empty\x1a\x1d.service.RoutingRulesResponse\"\x00\x12P\n\x0fGetBalancerInfo\x12\x1c.service.BalancerInfoRequest\x1a\x1d.service.BalancerInfoResponse\"\x00\x12>\n\tTestRoute\x12\x19.service.TestRouteRequest\x1a\x14.service.RouteResult\"\x00\x12\x42\n\x0e\x41\x64\x64RoutingRule\x12\x1e.service.AddRoutingRuleRequest\x1a\x0e.service.Empty\"\x00\x12H\n\x11RemoveRoutingRule\x12!.service.RemoveRoutingRuleRequest\x1a\x0e.service.Empty\"\x00\x12R\n\x16OverrideBalancerTarget\x12&.service.OverrideBalancerTargetRequest\x1a\x0e.service.Empty\"\x00\x42#Z!github.com/pasarguard/node/commonb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -36,10 +36,10 @@ _globals['_STATSONLINEIPLISTRESPONSE_IPSENTRY']._serialized_options = b'8\001' _globals['_TESTROUTEREQUEST_ATTRIBUTESENTRY']._loaded_options = None _globals['_TESTROUTEREQUEST_ATTRIBUTESENTRY']._serialized_options = b'8\001' - _globals['_BACKENDTYPE']._serialized_start=2772 - _globals['_BACKENDTYPE']._serialized_end=2810 - _globals['_STATTYPE']._serialized_start=2812 - _globals['_STATTYPE']._serialized_end=2907 + _globals['_BACKENDTYPE']._serialized_start=2974 + _globals['_BACKENDTYPE']._serialized_end=3012 + _globals['_STATTYPE']._serialized_start=3014 + _globals['_STATTYPE']._serialized_end=3109 _globals['_EMPTY']._serialized_start=54 _globals['_EMPTY']._serialized_end=61 _globals['_BASEINFORESPONSE']._serialized_start=63 @@ -54,62 +54,68 @@ _globals['_STATRESPONSE']._serialized_end=416 _globals['_STATREQUEST']._serialized_start=418 _globals['_STATREQUEST']._serialized_end=493 - _globals['_ONLINESTATRESPONSE']._serialized_start=495 - _globals['_ONLINESTATRESPONSE']._serialized_end=544 - _globals['_STATSONLINEIPLISTRESPONSE']._serialized_start=547 - _globals['_STATSONLINEIPLISTRESPONSE']._serialized_end=690 - _globals['_STATSONLINEIPLISTRESPONSE_IPSENTRY']._serialized_start=648 - _globals['_STATSONLINEIPLISTRESPONSE_IPSENTRY']._serialized_end=690 - _globals['_LATENCY']._serialized_start=693 - _globals['_LATENCY']._serialized_end=823 - _globals['_LATENCYREQUEST']._serialized_start=825 - _globals['_LATENCYREQUEST']._serialized_end=855 - _globals['_LATENCYRESPONSE']._serialized_start=857 - _globals['_LATENCYRESPONSE']._serialized_end=911 - _globals['_BACKENDSTATSRESPONSE']._serialized_start=914 - _globals['_BACKENDSTATSRESPONSE']._serialized_end=1118 - _globals['_SYSTEMSTATSRESPONSE']._serialized_start=1121 - _globals['_SYSTEMSTATSRESPONSE']._serialized_end=1301 - _globals['_VMESS']._serialized_start=1303 - _globals['_VMESS']._serialized_end=1322 - _globals['_VLESS']._serialized_start=1324 - _globals['_VLESS']._serialized_end=1357 - _globals['_TROJAN']._serialized_start=1359 - _globals['_TROJAN']._serialized_end=1385 - _globals['_SHADOWSOCKS']._serialized_start=1387 - _globals['_SHADOWSOCKS']._serialized_end=1434 - _globals['_WIREGUARD']._serialized_start=1436 - _globals['_WIREGUARD']._serialized_end=1485 - _globals['_HYSTERIA']._serialized_start=1487 - _globals['_HYSTERIA']._serialized_end=1511 - _globals['_PROXY']._serialized_start=1514 - _globals['_PROXY']._serialized_end=1735 - _globals['_USER']._serialized_start=1737 - _globals['_USER']._serialized_end=1809 - _globals['_USERS']._serialized_start=1811 - _globals['_USERS']._serialized_end=1848 - _globals['_USERSCHUNK']._serialized_start=1850 - _globals['_USERSCHUNK']._serialized_end=1921 - _globals['_ROUTINGRULE']._serialized_start=1923 - _globals['_ROUTINGRULE']._serialized_end=1976 - _globals['_ROUTINGRULESRESPONSE']._serialized_start=1978 - _globals['_ROUTINGRULESRESPONSE']._serialized_end=2037 - _globals['_BALANCERINFOREQUEST']._serialized_start=2039 - _globals['_BALANCERINFOREQUEST']._serialized_end=2073 - _globals['_BALANCERINFORESPONSE']._serialized_start=2075 - _globals['_BALANCERINFORESPONSE']._serialized_end=2148 - _globals['_TESTROUTEREQUEST']._serialized_start=2151 - _globals['_TESTROUTEREQUEST']._serialized_end=2465 - _globals['_TESTROUTEREQUEST_ATTRIBUTESENTRY']._serialized_start=2416 - _globals['_TESTROUTEREQUEST_ATTRIBUTESENTRY']._serialized_end=2465 - _globals['_ROUTERESULT']._serialized_start=2467 - _globals['_ROUTERESULT']._serialized_end=2592 - _globals['_ADDROUTINGRULEREQUEST']._serialized_start=2594 - _globals['_ADDROUTINGRULEREQUEST']._serialized_end=2653 - _globals['_REMOVEROUTINGRULEREQUEST']._serialized_start=2655 - _globals['_REMOVEROUTINGRULEREQUEST']._serialized_end=2699 - _globals['_OVERRIDEBALANCERTARGETREQUEST']._serialized_start=2701 - _globals['_OVERRIDEBALANCERTARGETREQUEST']._serialized_end=2770 - _globals['_NODESERVICE']._serialized_start=2910 - _globals['_NODESERVICE']._serialized_end=4154 + _globals['_USAGEREQUEST']._serialized_start=495 + _globals['_USAGEREQUEST']._serialized_end=542 + _globals['_USAGERECEIPT']._serialized_start=544 + _globals['_USAGERECEIPT']._serialized_end=630 + _globals['_USAGEACK']._serialized_start=632 + _globals['_USAGEACK']._serialized_end=695 + _globals['_ONLINESTATRESPONSE']._serialized_start=697 + _globals['_ONLINESTATRESPONSE']._serialized_end=746 + _globals['_STATSONLINEIPLISTRESPONSE']._serialized_start=749 + _globals['_STATSONLINEIPLISTRESPONSE']._serialized_end=892 + _globals['_STATSONLINEIPLISTRESPONSE_IPSENTRY']._serialized_start=850 + _globals['_STATSONLINEIPLISTRESPONSE_IPSENTRY']._serialized_end=892 + _globals['_LATENCY']._serialized_start=895 + _globals['_LATENCY']._serialized_end=1025 + _globals['_LATENCYREQUEST']._serialized_start=1027 + _globals['_LATENCYREQUEST']._serialized_end=1057 + _globals['_LATENCYRESPONSE']._serialized_start=1059 + _globals['_LATENCYRESPONSE']._serialized_end=1113 + _globals['_BACKENDSTATSRESPONSE']._serialized_start=1116 + _globals['_BACKENDSTATSRESPONSE']._serialized_end=1320 + _globals['_SYSTEMSTATSRESPONSE']._serialized_start=1323 + _globals['_SYSTEMSTATSRESPONSE']._serialized_end=1503 + _globals['_VMESS']._serialized_start=1505 + _globals['_VMESS']._serialized_end=1524 + _globals['_VLESS']._serialized_start=1526 + _globals['_VLESS']._serialized_end=1559 + _globals['_TROJAN']._serialized_start=1561 + _globals['_TROJAN']._serialized_end=1587 + _globals['_SHADOWSOCKS']._serialized_start=1589 + _globals['_SHADOWSOCKS']._serialized_end=1636 + _globals['_WIREGUARD']._serialized_start=1638 + _globals['_WIREGUARD']._serialized_end=1687 + _globals['_HYSTERIA']._serialized_start=1689 + _globals['_HYSTERIA']._serialized_end=1713 + _globals['_PROXY']._serialized_start=1716 + _globals['_PROXY']._serialized_end=1937 + _globals['_USER']._serialized_start=1939 + _globals['_USER']._serialized_end=2011 + _globals['_USERS']._serialized_start=2013 + _globals['_USERS']._serialized_end=2050 + _globals['_USERSCHUNK']._serialized_start=2052 + _globals['_USERSCHUNK']._serialized_end=2123 + _globals['_ROUTINGRULE']._serialized_start=2125 + _globals['_ROUTINGRULE']._serialized_end=2178 + _globals['_ROUTINGRULESRESPONSE']._serialized_start=2180 + _globals['_ROUTINGRULESRESPONSE']._serialized_end=2239 + _globals['_BALANCERINFOREQUEST']._serialized_start=2241 + _globals['_BALANCERINFOREQUEST']._serialized_end=2275 + _globals['_BALANCERINFORESPONSE']._serialized_start=2277 + _globals['_BALANCERINFORESPONSE']._serialized_end=2350 + _globals['_TESTROUTEREQUEST']._serialized_start=2353 + _globals['_TESTROUTEREQUEST']._serialized_end=2667 + _globals['_TESTROUTEREQUEST_ATTRIBUTESENTRY']._serialized_start=2618 + _globals['_TESTROUTEREQUEST_ATTRIBUTESENTRY']._serialized_end=2667 + _globals['_ROUTERESULT']._serialized_start=2669 + _globals['_ROUTERESULT']._serialized_end=2794 + _globals['_ADDROUTINGRULEREQUEST']._serialized_start=2796 + _globals['_ADDROUTINGRULEREQUEST']._serialized_end=2855 + _globals['_REMOVEROUTINGRULEREQUEST']._serialized_start=2857 + _globals['_REMOVEROUTINGRULEREQUEST']._serialized_end=2901 + _globals['_OVERRIDEBALANCERTARGETREQUEST']._serialized_start=2903 + _globals['_OVERRIDEBALANCERTARGETREQUEST']._serialized_end=2972 + _globals['_NODESERVICE']._serialized_start=3112 + _globals['_NODESERVICE']._serialized_end=4477 # @@protoc_insertion_point(module_scope) diff --git a/PasarGuardNodeBridge/common/service_pb2.pyi b/PasarGuardNodeBridge/common/service_pb2.pyi index e488a5b..cf646a3 100644 --- a/PasarGuardNodeBridge/common/service_pb2.pyi +++ b/PasarGuardNodeBridge/common/service_pb2.pyi @@ -91,6 +91,30 @@ class StatRequest(_message.Message): type: StatType def __init__(self, name: _Optional[str] = ..., reset: bool = ..., type: _Optional[_Union[StatType, str]] = ...) -> None: ... +class UsageRequest(_message.Message): + __slots__ = ("type",) + TYPE_FIELD_NUMBER: _ClassVar[int] + type: StatType + def __init__(self, type: _Optional[_Union[StatType, str]] = ...) -> None: ... + +class UsageReceipt(_message.Message): + __slots__ = ("receipt_id", "collected_at", "stats") + RECEIPT_ID_FIELD_NUMBER: _ClassVar[int] + COLLECTED_AT_FIELD_NUMBER: _ClassVar[int] + STATS_FIELD_NUMBER: _ClassVar[int] + receipt_id: str + collected_at: int + stats: _containers.RepeatedCompositeFieldContainer[Stat] + def __init__(self, receipt_id: _Optional[str] = ..., collected_at: _Optional[int] = ..., stats: _Optional[_Iterable[_Union[Stat, _Mapping]]] = ...) -> None: ... + +class UsageAck(_message.Message): + __slots__ = ("type", "receipt_id") + TYPE_FIELD_NUMBER: _ClassVar[int] + RECEIPT_ID_FIELD_NUMBER: _ClassVar[int] + type: StatType + receipt_id: str + def __init__(self, type: _Optional[_Union[StatType, str]] = ..., receipt_id: _Optional[str] = ...) -> None: ... + class OnlineStatResponse(_message.Message): __slots__ = ("name", "value") NAME_FIELD_NUMBER: _ClassVar[int] diff --git a/PasarGuardNodeBridge/grpclib.py b/PasarGuardNodeBridge/grpclib.py index 9acb247..42ddd98 100644 --- a/PasarGuardNodeBridge/grpclib.py +++ b/PasarGuardNodeBridge/grpclib.py @@ -211,9 +211,7 @@ async def stop(self, timeout: int | None = None) -> None: self.logger.debug( f"[{self.name}] Best-effort Stop request failed | Error: {type(e).__name__} - {e!s}" ) - await self._release_lifecycle_lease( - lease, LifecycleStatus.STOPPED, desired=LifecycleStatus.STOPPED - ) + await self._release_lifecycle_lease(lease, LifecycleStatus.STOPPED, desired=LifecycleStatus.STOPPED) except BaseException: await self._release_lifecycle_lease(lease, LifecycleStatus.BROKEN, desired=LifecycleStatus.STOPPED) raise @@ -254,6 +252,24 @@ async def get_stats( timeout=timeout, ) + async def collect_usage(self, stat_type: service.StatType, timeout: int | None = None) -> service.UsageReceipt: + """Collect or replay a node-owned durable usage receipt.""" + return await self._handle_grpc_request( + method=self._client.CollectUsage, + request=service.UsageRequest(type=stat_type), + timeout=timeout or self._default_timeout, + ) + + async def acknowledge_usage( + self, stat_type: service.StatType, receipt_id: str, timeout: int | None = None + ) -> service.Empty: + """ACK only after durable storage; a stale ACK cannot clear a newer receipt.""" + return await self._handle_grpc_request( + method=self._client.AcknowledgeUsage, + request=service.UsageAck(type=stat_type, receipt_id=receipt_id), + timeout=timeout or self._default_timeout, + ) + async def get_outbounds_latency(self, name: str = "", timeout: int | None = None) -> service.LatencyResponse | None: timeout = timeout or self._default_timeout return await self._handle_grpc_request( diff --git a/PasarGuardNodeBridge/rest.py b/PasarGuardNodeBridge/rest.py index 3df5543..9d34777 100644 --- a/PasarGuardNodeBridge/rest.py +++ b/PasarGuardNodeBridge/rest.py @@ -232,9 +232,7 @@ async def stop(self, timeout: int | None = None) -> None: self.logger.debug( f"[{self.name}] Best-effort stop request failed | Error: {type(e).__name__} - {e!s}" ) - await self._release_lifecycle_lease( - lease, LifecycleStatus.STOPPED, desired=LifecycleStatus.STOPPED - ) + await self._release_lifecycle_lease(lease, LifecycleStatus.STOPPED, desired=LifecycleStatus.STOPPED) except BaseException: await self._release_lifecycle_lease(lease, LifecycleStatus.BROKEN, desired=LifecycleStatus.STOPPED) raise @@ -272,6 +270,28 @@ async def get_stats( proto_response_class=service.StatResponse, ) + async def collect_usage(self, stat_type: service.StatType, timeout: int | None = None) -> service.UsageReceipt: + """Collect or replay a node-owned durable usage receipt.""" + return await self._make_request( + method="POST", + endpoint="usage/collect", + timeout=timeout or self._default_timeout, + proto_message=service.UsageRequest(type=stat_type), + proto_response_class=service.UsageReceipt, + ) + + async def acknowledge_usage( + self, stat_type: service.StatType, receipt_id: str, timeout: int | None = None + ) -> service.Empty: + """ACK only after durable storage; a stale ACK cannot clear a newer receipt.""" + return await self._make_request( + method="POST", + endpoint="usage/ack", + timeout=timeout or self._default_timeout, + proto_message=service.UsageAck(type=stat_type, receipt_id=receipt_id), + proto_response_class=service.Empty, + ) + async def get_outbounds_latency(self, name: str = "", timeout: int | None = None) -> service.LatencyResponse | None: timeout = timeout or self._default_timeout return await self._make_request(