Files
EvoBGP/internal/agentserver/server.go
T
Denozordec 927e27640a
quality / commitlint (push) Skipped
quality / changes (push) Successful in 8s
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 26s
quality / web (push) Successful in 1m27s
quality / go (push) Successful in 1m18s
quality / bird2 (push) Successful in 16s
CD / quality (push) Successful in 3m43s
CD / publish (push) Successful in 3m11s
feat(docs): update speaker installation instructions and logging details
- Enhanced the speaker installation documentation to clarify the use of TCP port 179 and the logging commands for monitoring BIRD and evobgp-agent.
- Updated the speaker form dialog to include additional information about MikroTik connections and logging commands.
- Modified the BIRD configuration to include logging to stderr for better visibility during operations.
- Adjusted the Docker Compose configuration to ensure proper network settings and sysctl configurations for BGP functionality.
2026-08-21 16:11:28 +07:00

248 lines
7.3 KiB
Go

package agentserver
import (
"context"
"crypto/subtle"
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"os"
"strings"
"time"
"evobgp/internal/birdfmt"
"evobgp/internal/nodecli"
)
const upstreamErrorDetail = "upstream request failed"
// Config holds evobgp-agent serve settings.
type Config struct {
Listen string
Secret string
ControlPlaneURL string
NodeToken string
SpeakerID string
PubKeyB64 string
PubKeyHex string
ExtractDir string
BirdBin string
BirdcBin string
Socket string
SyncTimeout time.Duration
LastSync func() (revisionID string, at time.Time)
OnSyncSuccess func(revisionID string)
}
// Server serves Panel→Node internal API (Remnawave-style wake-up).
type Server struct {
cfg Config
mux *http.ServeMux
firewall *firewallAgent
}
// New builds an agent HTTP server.
func New(cfg Config) *Server {
s := &Server{cfg: cfg, mux: http.NewServeMux(), firewall: newFirewallAgent(cfg)}
s.mux.HandleFunc("GET /v1/agent/health", s.handleHealth)
s.mux.HandleFunc("GET /v1/agent/bird/protocols", s.handleBirdProtocols)
s.mux.HandleFunc("POST /v1/agent/sync", s.handleSync)
if s.firewall.enabled {
s.mux.HandleFunc("GET /v1/firewall/blocklist", s.firewall.handleBlocklist)
s.mux.HandleFunc("POST /v1/firewall/apply-report", s.firewall.handleApplyReportForward)
s.mux.HandleFunc("POST /v1/agent/firewall-replicate", s.handleFirewallReplicate)
}
return s
}
// Handler returns the root HTTP handler.
func (s *Server) Handler() http.Handler {
return s.mux
}
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
if !s.authorize(r) {
writeProblem(w, http.StatusUnauthorized, "missing or invalid Authorization")
return
}
body := map[string]any{
"ok": true,
"speaker_id": strings.TrimSpace(s.cfg.SpeakerID),
}
if s.cfg.LastSync != nil {
if rev, at := s.cfg.LastSync(); rev != "" {
body["last_applied_revision_id"] = rev
body["last_sync_at"] = at.UTC().Format(time.RFC3339Nano)
}
}
writeJSON(w, http.StatusOK, body)
}
func (s *Server) handleBirdProtocols(w http.ResponseWriter, r *http.Request) {
if !s.authorize(r) {
writeProblem(w, http.StatusUnauthorized, "missing or invalid Authorization")
return
}
sock := strings.TrimSpace(s.cfg.Socket)
if sock == "" {
writeProblem(w, http.StatusServiceUnavailable, "EVOBGP_BIRDC_SOCKET not configured")
return
}
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
out, err := birdfmt.ShowProtocols(ctx, sock, strings.TrimSpace(s.cfg.BirdcBin))
if err != nil {
log.Printf("agentserver: bird protocols: %v", err)
writeProblem(w, http.StatusBadGateway, upstreamErrorDetail)
return
}
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
"sessions": birdfmt.ParseBGPSessions(out),
})
}
func (s *Server) handleSync(w http.ResponseWriter, r *http.Request) {
if !s.authorize(r) {
writeProblem(w, http.StatusUnauthorized, "missing or invalid Authorization")
return
}
var req struct {
RevisionID string `json:"revision_id"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
timeout := s.cfg.SyncTimeout
if timeout <= 0 {
timeout = 45 * time.Second
}
revID := strings.TrimSpace(req.RevisionID)
log.Printf("agentserver: sync start speaker_id=%s revision_id=%q control_plane=%s",
strings.TrimSpace(s.cfg.SpeakerID), revID, controlPlaneHost(s.cfg.ControlPlaneURL))
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
res, err := nodecli.SyncBundle(ctx, nodecli.SyncConfig{
BaseURL: s.cfg.ControlPlaneURL,
Token: s.cfg.NodeToken,
SpeakerID: s.cfg.SpeakerID,
RevisionID: revID,
PubKeyB64: s.cfg.PubKeyB64,
PubKeyHex: s.cfg.PubKeyHex,
ExtractDir: s.cfg.ExtractDir,
BirdBin: s.cfg.BirdBin,
BirdcBin: s.cfg.BirdcBin,
Socket: s.cfg.Socket,
Timeout: timeout,
})
if err != nil {
log.Printf("agentserver: sync failed speaker_id=%s revision_id=%q err=%v",
strings.TrimSpace(s.cfg.SpeakerID), revID, err)
writeProblem(w, http.StatusBadGateway, upstreamErrorDetail)
return
}
if s.cfg.OnSyncSuccess != nil {
s.cfg.OnSyncSuccess(res.RevisionID)
}
log.Printf("agentserver: sync ok speaker_id=%s applied_revision_id=%s",
strings.TrimSpace(s.cfg.SpeakerID), res.RevisionID)
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
"applied_revision_id": res.RevisionID,
"main_config": res.MainConfig,
})
}
func controlPlaneHost(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
u, err := url.Parse(raw)
if err != nil || strings.TrimSpace(u.Host) == "" {
return raw
}
return u.Host
}
func (s *Server) authorize(r *http.Request) bool {
secret := strings.TrimSpace(s.cfg.Secret)
if secret == "" {
return false
}
h := r.Header.Get("Authorization")
const prefix = "Bearer "
if !strings.HasPrefix(h, prefix) {
return false
}
got := strings.TrimSpace(h[len(prefix):])
return subtle.ConstantTimeCompare([]byte(got), []byte(secret)) == 1
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
func writeProblem(w http.ResponseWriter, status int, detail string) {
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]any{
"title": http.StatusText(status),
"status": status,
"detail": detail,
})
}
// ListenAndServe starts the agent HTTP server on cfg.Listen.
func ListenAndServe(cfg Config) error {
if strings.TrimSpace(cfg.Listen) == "" {
cfg.Listen = ":8443"
}
srv := &http.Server{
Addr: cfg.Listen,
Handler: New(cfg).Handler(),
ReadHeaderTimeout: 10 * time.Second,
}
log.Printf("evobgp-agent serve: listening on %s speaker=%s", cfg.Listen, cfg.SpeakerID)
return srv.ListenAndServe()
}
// ConfigFromEnv builds Config from EVOBGP_* environment variables.
func ConfigFromEnv() (Config, error) {
cfg := Config{
Listen: envOr("EVOBGP_AGENT_LISTEN", ":8443"),
Secret: strings.TrimSpace(os.Getenv("EVOBGP_AGENT_SECRET")),
ControlPlaneURL: strings.TrimSpace(os.Getenv("EVOBGP_CONTROL_PLANE_URL")),
NodeToken: strings.TrimSpace(os.Getenv("EVOBGP_NODE_TOKEN")),
SpeakerID: strings.TrimSpace(os.Getenv("EVOBGP_SPEAKER_ID")),
PubKeyB64: strings.TrimSpace(os.Getenv("EVOBGP_BUNDLE_PUBKEY_BASE64")),
PubKeyHex: strings.TrimSpace(os.Getenv("EVOBGP_BUNDLE_PUBKEY_HEX")),
ExtractDir: envOr("EVOBGP_BIRD_EXTRACT_DIR", "/etc/bird"),
BirdBin: strings.TrimSpace(os.Getenv("EVOBGP_BIRD_BIN")),
BirdcBin: strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_BIN")),
Socket: strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_SOCKET")),
SyncTimeout: 45 * time.Second,
}
if cfg.Secret == "" {
return cfg, fmt.Errorf("agentserver: EVOBGP_AGENT_SECRET required")
}
if cfg.ControlPlaneURL == "" || cfg.NodeToken == "" || cfg.SpeakerID == "" {
return cfg, fmt.Errorf("agentserver: EVOBGP_CONTROL_PLANE_URL, EVOBGP_NODE_TOKEN, EVOBGP_SPEAKER_ID required")
}
if cfg.PubKeyB64 == "" && cfg.PubKeyHex == "" {
return cfg, fmt.Errorf("agentserver: EVOBGP_BUNDLE_PUBKEY_BASE64 or EVOBGP_BUNDLE_PUBKEY_HEX required")
}
return cfg, nil
}
func envOr(key, def string) string {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v
}
return def
}