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 8be1703..8b8dacf 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; @@ -225,6 +242,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 2fe26b8..38b1870 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\"\x16\n\x06Masque\x12\x0c\n\x04pass\x18\x01 \x01(\t\"\xfe\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\x12\x1f\n\x06masque\x18\x07 \x01(\x0b\x32\x0f.service.Masque\"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\"\x16\n\x06Masque\x12\x0c\n\x04pass\x18\x01 \x01(\t\"\xfe\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\x12\x1f\n\x06masque\x18\x07 \x01(\x0b\x32\x0f.service.Masque\"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=2829 - _globals['_BACKENDTYPE']._serialized_end=2867 - _globals['_STATTYPE']._serialized_start=2869 - _globals['_STATTYPE']._serialized_end=2964 + _globals['_BACKENDTYPE']._serialized_start=3031 + _globals['_BACKENDTYPE']._serialized_end=3069 + _globals['_STATTYPE']._serialized_start=3071 + _globals['_STATTYPE']._serialized_end=3166 _globals['_EMPTY']._serialized_start=54 _globals['_EMPTY']._serialized_end=61 _globals['_BASEINFORESPONSE']._serialized_start=63 @@ -54,64 +54,70 @@ _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['_MASQUE']._serialized_start=1513 - _globals['_MASQUE']._serialized_end=1535 - _globals['_PROXY']._serialized_start=1538 - _globals['_PROXY']._serialized_end=1792 - _globals['_USER']._serialized_start=1794 - _globals['_USER']._serialized_end=1866 - _globals['_USERS']._serialized_start=1868 - _globals['_USERS']._serialized_end=1905 - _globals['_USERSCHUNK']._serialized_start=1907 - _globals['_USERSCHUNK']._serialized_end=1978 - _globals['_ROUTINGRULE']._serialized_start=1980 - _globals['_ROUTINGRULE']._serialized_end=2033 - _globals['_ROUTINGRULESRESPONSE']._serialized_start=2035 - _globals['_ROUTINGRULESRESPONSE']._serialized_end=2094 - _globals['_BALANCERINFOREQUEST']._serialized_start=2096 - _globals['_BALANCERINFOREQUEST']._serialized_end=2130 - _globals['_BALANCERINFORESPONSE']._serialized_start=2132 - _globals['_BALANCERINFORESPONSE']._serialized_end=2205 - _globals['_TESTROUTEREQUEST']._serialized_start=2208 - _globals['_TESTROUTEREQUEST']._serialized_end=2522 - _globals['_TESTROUTEREQUEST_ATTRIBUTESENTRY']._serialized_start=2473 - _globals['_TESTROUTEREQUEST_ATTRIBUTESENTRY']._serialized_end=2522 - _globals['_ROUTERESULT']._serialized_start=2524 - _globals['_ROUTERESULT']._serialized_end=2649 - _globals['_ADDROUTINGRULEREQUEST']._serialized_start=2651 - _globals['_ADDROUTINGRULEREQUEST']._serialized_end=2710 - _globals['_REMOVEROUTINGRULEREQUEST']._serialized_start=2712 - _globals['_REMOVEROUTINGRULEREQUEST']._serialized_end=2756 - _globals['_OVERRIDEBALANCERTARGETREQUEST']._serialized_start=2758 - _globals['_OVERRIDEBALANCERTARGETREQUEST']._serialized_end=2827 - _globals['_NODESERVICE']._serialized_start=2967 - _globals['_NODESERVICE']._serialized_end=4211 + _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['_MASQUE']._serialized_start=1715 + _globals['_MASQUE']._serialized_end=1737 + _globals['_PROXY']._serialized_start=1740 + _globals['_PROXY']._serialized_end=1994 + _globals['_USER']._serialized_start=1996 + _globals['_USER']._serialized_end=2068 + _globals['_USERS']._serialized_start=2070 + _globals['_USERS']._serialized_end=2107 + _globals['_USERSCHUNK']._serialized_start=2109 + _globals['_USERSCHUNK']._serialized_end=2180 + _globals['_ROUTINGRULE']._serialized_start=2182 + _globals['_ROUTINGRULE']._serialized_end=2235 + _globals['_ROUTINGRULESRESPONSE']._serialized_start=2237 + _globals['_ROUTINGRULESRESPONSE']._serialized_end=2296 + _globals['_BALANCERINFOREQUEST']._serialized_start=2298 + _globals['_BALANCERINFOREQUEST']._serialized_end=2332 + _globals['_BALANCERINFORESPONSE']._serialized_start=2334 + _globals['_BALANCERINFORESPONSE']._serialized_end=2407 + _globals['_TESTROUTEREQUEST']._serialized_start=2410 + _globals['_TESTROUTEREQUEST']._serialized_end=2724 + _globals['_TESTROUTEREQUEST_ATTRIBUTESENTRY']._serialized_start=2675 + _globals['_TESTROUTEREQUEST_ATTRIBUTESENTRY']._serialized_end=2724 + _globals['_ROUTERESULT']._serialized_start=2726 + _globals['_ROUTERESULT']._serialized_end=2851 + _globals['_ADDROUTINGRULEREQUEST']._serialized_start=2853 + _globals['_ADDROUTINGRULEREQUEST']._serialized_end=2912 + _globals['_REMOVEROUTINGRULEREQUEST']._serialized_start=2914 + _globals['_REMOVEROUTINGRULEREQUEST']._serialized_end=2958 + _globals['_OVERRIDEBALANCERTARGETREQUEST']._serialized_start=2960 + _globals['_OVERRIDEBALANCERTARGETREQUEST']._serialized_end=3029 + _globals['_NODESERVICE']._serialized_start=3169 + _globals['_NODESERVICE']._serialized_end=4534 # @@protoc_insertion_point(module_scope) diff --git a/PasarGuardNodeBridge/common/service_pb2.pyi b/PasarGuardNodeBridge/common/service_pb2.pyi index 2328afd..ea61053 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 68f250f..9611363 100644 --- a/PasarGuardNodeBridge/grpclib.py +++ b/PasarGuardNodeBridge/grpclib.py @@ -213,9 +213,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 @@ -256,6 +254,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 ae57aca..33cc901 100644 --- a/PasarGuardNodeBridge/rest.py +++ b/PasarGuardNodeBridge/rest.py @@ -233,9 +233,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 @@ -273,6 +271,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(