Автентифікація зондів за токеном, побудова TaskPlan із детермінованим schedule_offset, видача розшифрованих креденшелів із TTL, запис телеметрії в гіпертаблиці, резолвер сусідів LLDP/CDP у topo.links, прийом конфігів. Ізоляція тенантів робиться двічі — RLS плюс явний предикат tenant_id, бо RLS не працює на гіпертаблях, а саме туди йде вся телеметрія. Агент: -token і передача його в метаданих; MarkAllPending() перереєстровує серії на початку сесії замість обнуляти нумерацію й губити буфер. Перевірено на Debian 13 / PG 17.11 / TimescaleDB 2.29.1: 11 інтеграційних тестів проти живої БД (-race), плюс живий прогін справжнього агента проти справжнього сервера — телеметрія, статус пристрою, heartbeat у базі. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
164 lines
5.8 KiB
Go
164 lines
5.8 KiB
Go
// Package config — налаштування агента з прапорців і змінних оточення.
|
||
//
|
||
// Конфіг-файлу навмисно немає: у зонда рівно жменя параметрів, а
|
||
// відсутність YAML економить залежність і кілька сотень кілобайт
|
||
// бінарника. Усе інше агент отримує від сервера в Welcome і ModuleControl.
|
||
package config
|
||
|
||
import (
|
||
"crypto/tls"
|
||
"crypto/x509"
|
||
"errors"
|
||
"flag"
|
||
"fmt"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
type Config struct {
|
||
// Куди підключатись: host:port.
|
||
Endpoint string
|
||
AgentID string
|
||
Hostname string
|
||
// Токен зонда, виданий при Enroll. Визначає, ЯКИЙ це зонд;
|
||
// сертифікат mTLS — що він узагалі має право говорити з сервером.
|
||
Token string
|
||
|
||
// mTLS. Порожні шляхи + Insecure = локальний стенд без шифрування.
|
||
CertFile string
|
||
KeyFile string
|
||
CAFile string
|
||
Insecure bool
|
||
|
||
MaxConcurrency int
|
||
BufferMaxItems int
|
||
BufferMaxBytes int
|
||
|
||
MinBackoff time.Duration
|
||
MaxBackoff time.Duration
|
||
|
||
LogLevel string
|
||
LogJSON bool
|
||
|
||
// Модулі, увімкнені до першої відповіді сервера.
|
||
DefaultModules []string
|
||
}
|
||
|
||
func envOr(key, def string) string {
|
||
if v := os.Getenv(key); v != "" {
|
||
return v
|
||
}
|
||
return def
|
||
}
|
||
|
||
func envInt(key string, def int) int {
|
||
if v := os.Getenv(key); v != "" {
|
||
if n, err := strconv.Atoi(v); err == nil {
|
||
return n
|
||
}
|
||
}
|
||
return def
|
||
}
|
||
|
||
// Parse читає прапорці поверх змінних оточення (NETPULSE_*).
|
||
func Parse(args []string) (*Config, error) {
|
||
host, _ := os.Hostname()
|
||
|
||
c := &Config{}
|
||
fs := flag.NewFlagSet("netpulse-agent", flag.ContinueOnError)
|
||
|
||
fs.StringVar(&c.Endpoint, "server", envOr("NETPULSE_SERVER", ""), "адреса сервера, host:port")
|
||
fs.StringVar(&c.AgentID, "agent-id", envOr("NETPULSE_AGENT_ID", ""), "ідентифікатор зонда")
|
||
fs.StringVar(&c.Hostname, "hostname", envOr("NETPULSE_HOSTNAME", host), "ім'я хоста для журналу сервера")
|
||
|
||
fs.StringVar(&c.Token, "token", envOr("NETPULSE_TOKEN", ""), "токен зонда з Enroll")
|
||
fs.StringVar(&c.CertFile, "cert", envOr("NETPULSE_CERT", ""), "клієнтський сертифікат (mTLS)")
|
||
fs.StringVar(&c.KeyFile, "key", envOr("NETPULSE_KEY", ""), "приватний ключ")
|
||
fs.StringVar(&c.CAFile, "ca", envOr("NETPULSE_CA", ""), "CA сервера")
|
||
fs.BoolVar(&c.Insecure, "insecure", os.Getenv("NETPULSE_INSECURE") == "1",
|
||
"без TLS — лише для локального стенду")
|
||
|
||
fs.IntVar(&c.MaxConcurrency, "max-concurrency", envInt("NETPULSE_MAX_CONCURRENCY", 64),
|
||
"скільки чеків виконувати паралельно")
|
||
fs.IntVar(&c.BufferMaxItems, "buffer-items", envInt("NETPULSE_BUFFER_ITEMS", 50_000),
|
||
"ліміт записів у буфері телеметрії")
|
||
fs.IntVar(&c.BufferMaxBytes, "buffer-bytes", envInt("NETPULSE_BUFFER_BYTES", 8<<20),
|
||
"оціночний ліміт пам'яті буфера в байтах")
|
||
|
||
fs.DurationVar(&c.MinBackoff, "min-backoff", time.Second, "мінімальна пауза перед реконектом")
|
||
fs.DurationVar(&c.MaxBackoff, "max-backoff", 2*time.Minute, "максимальна пауза перед реконектом")
|
||
|
||
fs.StringVar(&c.LogLevel, "log-level", envOr("NETPULSE_LOG_LEVEL", "info"), "debug|info|warn|error")
|
||
fs.BoolVar(&c.LogJSON, "log-json", os.Getenv("NETPULSE_LOG_JSON") == "1", "журнал у JSON")
|
||
|
||
mods := fs.String("modules", envOr("NETPULSE_MODULES", "icmp"),
|
||
"модулі, увімкнені до відповіді сервера (через кому)")
|
||
|
||
if err := fs.Parse(args); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
for _, m := range strings.Split(*mods, ",") {
|
||
if m = strings.TrimSpace(m); m != "" {
|
||
c.DefaultModules = append(c.DefaultModules, m)
|
||
}
|
||
}
|
||
|
||
return c, c.validate()
|
||
}
|
||
|
||
func (c *Config) validate() error {
|
||
if c.Endpoint == "" {
|
||
return errors.New("не вказано адресу сервера (-server або NETPULSE_SERVER)")
|
||
}
|
||
if c.AgentID == "" {
|
||
return errors.New("не вказано -agent-id: зонд має бути зареєстрований через Enroll")
|
||
}
|
||
if c.Token == "" {
|
||
return errors.New("не вказано -token (або NETPULSE_TOKEN)")
|
||
}
|
||
if !c.Insecure && (c.CertFile == "" || c.KeyFile == "") {
|
||
return errors.New("потрібні -cert і -key для mTLS (або -insecure для локального стенду)")
|
||
}
|
||
if c.MaxConcurrency <= 0 {
|
||
return errors.New("-max-concurrency має бути додатним")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// TLSConfig збирає mTLS-конфігурацію.
|
||
//
|
||
// Перевірка сертифіката сервера обов'язкова й не вимикається прапорцем:
|
||
// зонд ходить через інтернет, і «тимчасово без перевірки» — це той
|
||
// самий канал, яким їдуть паролі від усіх комутаторів клієнта.
|
||
func (c *Config) TLSConfig() (*tls.Config, error) {
|
||
if c.Insecure {
|
||
return nil, nil
|
||
}
|
||
|
||
cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("клієнтський сертифікат: %w", err)
|
||
}
|
||
|
||
tc := &tls.Config{
|
||
Certificates: []tls.Certificate{cert},
|
||
MinVersion: tls.VersionTLS13,
|
||
}
|
||
|
||
if c.CAFile != "" {
|
||
pem, err := os.ReadFile(c.CAFile)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("CA: %w", err)
|
||
}
|
||
pool := x509.NewCertPool()
|
||
if !pool.AppendCertsFromPEM(pem) {
|
||
return nil, errors.New("CA: не вдалося розібрати PEM")
|
||
}
|
||
tc.RootCAs = pool
|
||
}
|
||
|
||
return tc, nil
|
||
}
|