Приймач на зонді лежав без діла — тепер під'єднаний. Окремий стрім StreamLogs, а не контрольний канал: сплеск логів під час аварії не має заважати heartbeat і командам. Хост зіставляється за адресою джерела на зонді: у сервера немає контексту мережі клієнта, а один приватний діапазон трапляється в десятках кабінетів. Невідома адреса не привід викинути подію. Подія, що збіглася зі зразком у ncm.device_policies.syslog_match, ставить позачерговий збір конфігу. Типовий зразок покриває Cisco, HP/Huawei, Juniper і MikroTik — навмисно широкий: зайвий бекап коштує секунд, пропущений — цілої зміни. Заразом увесь репозиторій прогнано через gofmt: CI, написаний два кроки тому, перевіряє це і впав би на 29 файлах із порушеннями, накопиченими за весь проєкт. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
67 lines
3 KiB
Go
67 lines
3 KiB
Go
package grpcapi
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"strings"
|
||
|
||
npv1 "github.com/netpulse/netpulse/gen/go/netpulse/v1"
|
||
"github.com/netpulse/netpulse/server/internal/store"
|
||
"google.golang.org/grpc/codes"
|
||
"google.golang.org/grpc/status"
|
||
)
|
||
|
||
// EnrollService реалізує npv1.EnrollmentServiceServer.
|
||
//
|
||
// Окремий тип, а не метод на Service: Enroll — єдиний виклик, який іде
|
||
// БЕЗ токена зонда, тож він не має проходити через інтерсептор
|
||
// автентифікації AgentService. Різні сервіси в одному gRPC-сервері
|
||
// дозволяють це розвести без винятків усередині інтерсептора.
|
||
type EnrollService struct {
|
||
npv1.UnimplementedEnrollmentServiceServer
|
||
|
||
store *store.Store
|
||
// Куди підключатись після реєстрації. Може відрізнятись від адреси,
|
||
// на яку прийшов Enroll: балансувальник, регіональний шлюз.
|
||
controlEndpoint string
|
||
}
|
||
|
||
func NewEnrollService(st *store.Store, controlEndpoint string) *EnrollService {
|
||
return &EnrollService{store: st, controlEndpoint: controlEndpoint}
|
||
}
|
||
|
||
// Enroll обмінює одноразове запрошення на постійний зонд.
|
||
//
|
||
// Сертифікат у відповіді поки порожній. mTLS у продукті є (сервер
|
||
// приймає -client-ca), але він налаштовується окремо від реєстрації:
|
||
// власний CA з ротацією — це друга система, і вдавати, що вона вже
|
||
// працює, означало б віддати агенту порожній сертифікат і зламати
|
||
// з'єднання там, де воно щойно працювало.
|
||
func (s *EnrollService) Enroll(ctx context.Context, req *npv1.EnrollRequest) (*npv1.EnrollResponse, error) {
|
||
token := strings.TrimSpace(req.GetEnrollmentToken())
|
||
if token == "" {
|
||
return nil, status.Error(codes.InvalidArgument, "потрібен токен запрошення")
|
||
}
|
||
|
||
b := req.GetBuild()
|
||
agent, err := s.store.RedeemEnrollment(ctx, token,
|
||
req.GetHostname(), req.GetRequestedName(),
|
||
b.GetVersion(), b.GetOs(), b.GetArch())
|
||
|
||
if errors.Is(err, store.ErrEnrollmentInvalid) {
|
||
// Одна відповідь на «немає», «згоріло» і «вже використано»:
|
||
// розрізняти їх означало б підказувати, наскільки близько той,
|
||
// хто підбирає токени.
|
||
return nil, status.Error(codes.PermissionDenied, "запрошення недійсне")
|
||
}
|
||
if err != nil {
|
||
return nil, status.Errorf(codes.Internal, "реєстрація: %v", err)
|
||
}
|
||
|
||
return &npv1.EnrollResponse{
|
||
AgentId: agent.ID,
|
||
AgentName: agent.Name,
|
||
ControlEndpoint: s.controlEndpoint,
|
||
AgentToken: agent.Token,
|
||
}, nil
|
||
}
|