merge: maestro/tsk__R8M4jEw43JR [tsk__R8M4jEw43JR]
This commit is contained in:
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{}
|
||||
Reference in New Issue
Block a user