merge: maestro/tsk__R8M4jEw43JR [tsk__R8M4jEw43JR]

This commit is contained in:
wangjia
2026-06-13 17:04:17 +08:00
25 changed files with 2963 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
package agentv1
import (
"encoding/json"
"google.golang.org/grpc/encoding"
)
// CodecName is the gRPC content-subtype used by the agent contract. Because this
// repository has no protoc pipeline yet, messages are encoded as JSON rather than
// protobuf wire format. Clients select it with grpc.CallContentSubtype(CodecName)
// (wired automatically by the client/server helpers in service.go); the server
// echoes the same subtype on responses.
const CodecName = "pangolinagentjson"
func init() { encoding.RegisterCodec(jsonCodec{}) }
// jsonCodec implements google.golang.org/grpc/encoding.Codec over encoding/json.
type jsonCodec struct{}
func (jsonCodec) Marshal(v any) ([]byte, error) { return json.Marshal(v) }
func (jsonCodec) Unmarshal(data []byte, v any) error {
if len(data) == 0 {
return nil // empty message (e.g. AckResponse / UsageAck)
}
return json.Unmarshal(data, v)
}
func (jsonCodec) Name() string { return CodecName }
+238
View File
@@ -0,0 +1,238 @@
package agentv1
import (
"context"
"google.golang.org/grpc"
)
// ServiceName is the fully-qualified gRPC service name. It MUST match the value
// the mTLS interceptor whitelists for Enroll (server/internal/mtls/identity.go).
const ServiceName = "pangolin.agent.v1.AgentService"
// Fully-qualified method names.
const (
MethodEnroll = "/" + ServiceName + "/Enroll"
MethodRegister = "/" + ServiceName + "/Register"
MethodHeartbeat = "/" + ServiceName + "/Heartbeat"
MethodSubscribe = "/" + ServiceName + "/Subscribe"
MethodAck = "/" + ServiceName + "/Ack"
MethodReportUsage = "/" + ServiceName + "/ReportUsage"
)
// withCodec forces the JSON content-subtype on every call so the contract does
// not depend on the caller remembering to set dial-level call options.
func withCodec(opts []grpc.CallOption) []grpc.CallOption {
return append([]grpc.CallOption{grpc.CallContentSubtype(CodecName)}, opts...)
}
// ─── server interface ──────────────────────────────────────────────────────────
// AgentServiceServer is implemented by the control plane.
type AgentServiceServer interface {
Enroll(context.Context, *EnrollRequest) (*EnrollResponse, error)
Register(context.Context, *RegisterRequest) (*ConfigSnapshot, error)
Heartbeat(context.Context, *HeartbeatRequest) (*HeartbeatResponse, error)
Subscribe(*SubscribeRequest, AgentService_SubscribeServer) error
Ack(context.Context, *AckRequest) (*AckResponse, error)
ReportUsage(context.Context, *UsageReport) (*UsageAck, error)
}
// AgentService_SubscribeServer is the server side of the Command stream.
type AgentService_SubscribeServer interface {
Send(*Command) error
grpc.ServerStream
}
type subscribeServer struct{ grpc.ServerStream }
func (s *subscribeServer) Send(m *Command) error { return s.ServerStream.SendMsg(m) }
// RegisterAgentServiceServer wires srv into a gRPC server (or any ServiceRegistrar).
func RegisterAgentServiceServer(s grpc.ServiceRegistrar, srv AgentServiceServer) {
s.RegisterService(&serviceDesc, srv)
}
func handlerEnroll(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
in := new(EnrollRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AgentServiceServer).Enroll(ctx, in)
}
info := &grpc.UnaryServerInfo{Server: srv, FullMethod: MethodEnroll}
return interceptor(ctx, in, info, func(ctx context.Context, req any) (any, error) {
return srv.(AgentServiceServer).Enroll(ctx, req.(*EnrollRequest))
})
}
func handlerRegister(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
in := new(RegisterRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AgentServiceServer).Register(ctx, in)
}
info := &grpc.UnaryServerInfo{Server: srv, FullMethod: MethodRegister}
return interceptor(ctx, in, info, func(ctx context.Context, req any) (any, error) {
return srv.(AgentServiceServer).Register(ctx, req.(*RegisterRequest))
})
}
func handlerHeartbeat(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
in := new(HeartbeatRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AgentServiceServer).Heartbeat(ctx, in)
}
info := &grpc.UnaryServerInfo{Server: srv, FullMethod: MethodHeartbeat}
return interceptor(ctx, in, info, func(ctx context.Context, req any) (any, error) {
return srv.(AgentServiceServer).Heartbeat(ctx, req.(*HeartbeatRequest))
})
}
func handlerAck(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
in := new(AckRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AgentServiceServer).Ack(ctx, in)
}
info := &grpc.UnaryServerInfo{Server: srv, FullMethod: MethodAck}
return interceptor(ctx, in, info, func(ctx context.Context, req any) (any, error) {
return srv.(AgentServiceServer).Ack(ctx, req.(*AckRequest))
})
}
func handlerReportUsage(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
in := new(UsageReport)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AgentServiceServer).ReportUsage(ctx, in)
}
info := &grpc.UnaryServerInfo{Server: srv, FullMethod: MethodReportUsage}
return interceptor(ctx, in, info, func(ctx context.Context, req any) (any, error) {
return srv.(AgentServiceServer).ReportUsage(ctx, req.(*UsageReport))
})
}
func handlerSubscribe(srv any, stream grpc.ServerStream) error {
m := new(SubscribeRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(AgentServiceServer).Subscribe(m, &subscribeServer{stream})
}
var serviceDesc = grpc.ServiceDesc{
ServiceName: ServiceName,
HandlerType: (*AgentServiceServer)(nil),
Methods: []grpc.MethodDesc{
{MethodName: "Enroll", Handler: handlerEnroll},
{MethodName: "Register", Handler: handlerRegister},
{MethodName: "Heartbeat", Handler: handlerHeartbeat},
{MethodName: "Ack", Handler: handlerAck},
{MethodName: "ReportUsage", Handler: handlerReportUsage},
},
Streams: []grpc.StreamDesc{
{StreamName: "Subscribe", Handler: handlerSubscribe, ServerStreams: true},
},
Metadata: "proto/agent/v1/agent.proto",
}
// ─── client ─────────────────────────────────────────────────────────────────────
// AgentServiceClient is consumed by the node agent.
type AgentServiceClient interface {
Enroll(ctx context.Context, in *EnrollRequest, opts ...grpc.CallOption) (*EnrollResponse, error)
Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*ConfigSnapshot, error)
Heartbeat(ctx context.Context, in *HeartbeatRequest, opts ...grpc.CallOption) (*HeartbeatResponse, error)
Subscribe(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (AgentService_SubscribeClient, error)
Ack(ctx context.Context, in *AckRequest, opts ...grpc.CallOption) (*AckResponse, error)
ReportUsage(ctx context.Context, in *UsageReport, opts ...grpc.CallOption) (*UsageAck, error)
}
// AgentService_SubscribeClient is the client side of the Command stream.
type AgentService_SubscribeClient interface {
Recv() (*Command, error)
grpc.ClientStream
}
type subscribeClient struct{ grpc.ClientStream }
func (c *subscribeClient) Recv() (*Command, error) {
m := new(Command)
if err := c.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
type agentServiceClient struct{ cc grpc.ClientConnInterface }
// NewAgentServiceClient returns a client bound to cc. All calls use the JSON codec.
func NewAgentServiceClient(cc grpc.ClientConnInterface) AgentServiceClient {
return &agentServiceClient{cc}
}
func (c *agentServiceClient) Enroll(ctx context.Context, in *EnrollRequest, opts ...grpc.CallOption) (*EnrollResponse, error) {
out := new(EnrollResponse)
if err := c.cc.Invoke(ctx, MethodEnroll, in, out, withCodec(opts)...); err != nil {
return nil, err
}
return out, nil
}
func (c *agentServiceClient) Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*ConfigSnapshot, error) {
out := new(ConfigSnapshot)
if err := c.cc.Invoke(ctx, MethodRegister, in, out, withCodec(opts)...); err != nil {
return nil, err
}
return out, nil
}
func (c *agentServiceClient) Heartbeat(ctx context.Context, in *HeartbeatRequest, opts ...grpc.CallOption) (*HeartbeatResponse, error) {
out := new(HeartbeatResponse)
if err := c.cc.Invoke(ctx, MethodHeartbeat, in, out, withCodec(opts)...); err != nil {
return nil, err
}
return out, nil
}
func (c *agentServiceClient) Ack(ctx context.Context, in *AckRequest, opts ...grpc.CallOption) (*AckResponse, error) {
out := new(AckResponse)
if err := c.cc.Invoke(ctx, MethodAck, in, out, withCodec(opts)...); err != nil {
return nil, err
}
return out, nil
}
func (c *agentServiceClient) ReportUsage(ctx context.Context, in *UsageReport, opts ...grpc.CallOption) (*UsageAck, error) {
out := new(UsageAck)
if err := c.cc.Invoke(ctx, MethodReportUsage, in, out, withCodec(opts)...); err != nil {
return nil, err
}
return out, nil
}
func (c *agentServiceClient) Subscribe(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (AgentService_SubscribeClient, error) {
stream, err := c.cc.NewStream(ctx, &serviceDesc.Streams[0], MethodSubscribe, withCodec(opts)...)
if err != nil {
return nil, err
}
x := &subscribeClient{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
+176
View File
@@ -0,0 +1,176 @@
// Package agentv1 is the control-plane <-> node-agent gRPC contract.
//
// These types mirror server/proto/agent/v1/agent.proto 1:1. The repository has no
// protoc/buf pipeline wired yet (Makefile `generate` is a stub from task #5), so
// the messages are hand-written Go structs and the gRPC stubs (service.go) use a
// JSON codec (codec.go) instead of protobuf wire format. The JSON field names are
// the proto field names so a future protoc-generated package is drop-in
// compatible at the API surface.
//
// Data-plane red line: nothing in this package carries user_id, email, device id,
// destination address or DNS data — only the opaque dp_uuid and aggregate counters.
package agentv1
// Protocol selects which sing-box inbound(s) a credential is provisioned on.
type Protocol int32
const (
ProtocolUnspecified Protocol = 0
ProtocolReality Protocol = 1
ProtocolHy2 Protocol = 2
ProtocolBoth Protocol = 3
)
// CommandType discriminates the Command payload.
type CommandType int32
const (
CommandTypeUnspecified CommandType = 0
CommandTypeUpsert CommandType = 1
CommandTypeRevoke CommandType = 2
CommandTypeRotateCredential CommandType = 3
CommandTypeApplyConfig CommandType = 4
CommandTypeLifecycle CommandType = 5
)
// LifecycleAction is the verb of a LIFECYCLE command.
type LifecycleAction int32
const (
LifecycleActionUnspecified LifecycleAction = 0
LifecycleActionDrain LifecycleAction = 1
LifecycleActionResume LifecycleAction = 2
LifecycleActionShutdown LifecycleAction = 3
)
// ─── enroll ──────────────────────────────────────────────────────────────────
type EnrollRequest struct {
BootstrapToken string `json:"bootstrap_token,omitempty"`
CSRPEM []byte `json:"csr_pem,omitempty"`
AgentVersion string `json:"agent_version,omitempty"`
}
type EnrollResponse struct {
NodeUUID string `json:"node_uuid,omitempty"`
CertPEM []byte `json:"cert_pem,omitempty"`
CAPEM []byte `json:"ca_pem,omitempty"`
NotAfterUnix int64 `json:"not_after_unix,omitempty"`
}
// ─── register / config ─────────────────────────────────────────────────────────
type RegisterRequest struct {
NodeUUID string `json:"node_uuid,omitempty"`
AgentVersion string `json:"agent_version,omitempty"`
LocalConfigVersion int64 `json:"local_config_version,omitempty"`
}
// Credential is the only per-subscriber object a node ever sees.
type Credential struct {
DpUUID string `json:"dp_uuid,omitempty"`
Protocol Protocol `json:"protocol,omitempty"`
Flow string `json:"flow,omitempty"`
ExpiresAtUnix int64 `json:"expires_at_unix,omitempty"`
}
type ConfigSnapshot struct {
ConfigVersion int64 `json:"config_version,omitempty"`
Credentials []*Credential `json:"credentials,omitempty"`
Reality *RealityInbound `json:"reality,omitempty"`
Hy2 *Hy2Inbound `json:"hy2,omitempty"`
LastCommandID int64 `json:"last_command_id,omitempty"`
}
type RealityInbound struct {
ListenPort int32 `json:"listen_port,omitempty"`
PrivateKey string `json:"private_key,omitempty"`
ShortID string `json:"short_id,omitempty"`
ServerName string `json:"server_name,omitempty"`
HandshakeServer string `json:"handshake_server,omitempty"`
HandshakePort int32 `json:"handshake_port,omitempty"`
}
type Hy2Inbound struct {
ListenPort int32 `json:"listen_port,omitempty"`
Masquerade string `json:"masquerade,omitempty"`
CertPath string `json:"cert_path,omitempty"`
KeyPath string `json:"key_path,omitempty"`
}
// ─── heartbeat ───────────────────────────────────────────────────────────────
type HeartbeatRequest struct {
NodeUUID string `json:"node_uuid,omitempty"`
ConfigVersion int64 `json:"config_version,omitempty"`
OnlinePeers int32 `json:"online_peers,omitempty"`
BandwidthUpBps int64 `json:"bandwidth_up_bps,omitempty"`
BandwidthDownBps int64 `json:"bandwidth_down_bps,omitempty"`
CPUPercent float64 `json:"cpu_percent,omitempty"`
TimestampUnix int64 `json:"timestamp_unix,omitempty"`
}
type HeartbeatResponse struct {
NeedFullResync bool `json:"need_full_resync,omitempty"`
ServerTimeUnix int64 `json:"server_time_unix,omitempty"`
}
// ─── command stream ──────────────────────────────────────────────────────────
type Command struct {
CommandID int64 `json:"command_id,omitempty"`
Type CommandType `json:"type,omitempty"`
Upsert *UpsertPayload `json:"upsert,omitempty"`
Revoke *RevokePayload `json:"revoke,omitempty"`
Rotate *RotatePayload `json:"rotate,omitempty"`
ApplyConfig *ConfigSnapshot `json:"apply_config,omitempty"`
Lifecycle *LifecyclePayload `json:"lifecycle,omitempty"`
}
type UpsertPayload struct {
Credential *Credential `json:"credential,omitempty"`
}
type RevokePayload struct {
DpUUID string `json:"dp_uuid,omitempty"`
}
type RotatePayload struct {
OldDpUUID string `json:"old_dp_uuid,omitempty"`
NewCredential *Credential `json:"new_credential,omitempty"`
GraceUntilUnix int64 `json:"grace_until_unix,omitempty"`
}
type LifecyclePayload struct {
Action LifecycleAction `json:"action,omitempty"`
}
type SubscribeRequest struct {
NodeUUID string `json:"node_uuid,omitempty"`
LastCommandID int64 `json:"last_command_id,omitempty"`
}
type AckRequest struct {
NodeUUID string `json:"node_uuid,omitempty"`
CommandID int64 `json:"command_id,omitempty"`
}
type AckResponse struct{}
// ─── usage ───────────────────────────────────────────────────────────────────
type UsageEntry struct {
DpUUID string `json:"dp_uuid,omitempty"`
BytesUp int64 `json:"bytes_up,omitempty"`
BytesDown int64 `json:"bytes_down,omitempty"`
SessionMinutes int64 `json:"session_minutes,omitempty"`
}
type UsageReport struct {
NodeUUID string `json:"node_uuid,omitempty"`
WindowStartUnix int64 `json:"window_start_unix,omitempty"`
WindowEndUnix int64 `json:"window_end_unix,omitempty"`
Entries []*UsageEntry `json:"entries,omitempty"`
}
type UsageAck struct{}