| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- package vrrp
- import (
- "encoding/json"
- "fmt"
- "net"
- "os"
- "os/exec"
- "path/filepath"
- "sort"
- "strings"
- "sync"
- "time"
- "github.com/vishvananda/netlink"
- )
- const (
- VRRPStorePath = "/etc/proxmox-router/vrrp.json"
- KeepalivedConfPath = "/etc/keepalived/keepalived.conf"
- VRRPStatusDir = "/run/proxmox-router/vrrp"
- VRRPNotifyScriptPath = "/usr/local/bin/vrrp-notify.sh"
- )
- type Manager struct {
- mu sync.RWMutex
- instances map[string]VRRPInstance
- storePath string
- confPath string
- }
- func NewManager() *Manager {
- m := &Manager{
- instances: make(map[string]VRRPInstance),
- storePath: VRRPStorePath,
- confPath: KeepalivedConfPath,
- }
- _ = os.MkdirAll(filepath.Dir(m.storePath), 0755)
- _ = os.MkdirAll(filepath.Dir(m.confPath), 0755)
- _ = os.MkdirAll(VRRPStatusDir, 0755)
- m.ensureNotifyScript()
- m.load()
- // Apply config on startup if instances exist
- _ = m.applyConfigLocked()
- return m
- }
- func (m *Manager) ensureNotifyScript() {
- scriptContent := `#!/bin/bash
- STATE="$1"
- NAME="$2"
- mkdir -p /run/proxmox-router/vrrp
- echo "$STATE $(date -Iseconds)" > "/run/proxmox-router/vrrp/${NAME}.state"
- `
- _ = os.WriteFile(VRRPNotifyScriptPath, []byte(scriptContent), 0755)
- }
- func (m *Manager) load() {
- m.mu.Lock()
- defer m.mu.Unlock()
- data, err := os.ReadFile(m.storePath)
- if err != nil {
- return
- }
- var store VRRPConfigStore
- if err := json.Unmarshal(data, &store); err != nil {
- return
- }
- for _, inst := range store.Instances {
- m.instances[inst.ID] = inst
- }
- }
- func (m *Manager) saveLocked() error {
- store := VRRPConfigStore{
- Instances: make([]VRRPInstance, 0, len(m.instances)),
- }
- for _, inst := range m.instances {
- store.Instances = append(store.Instances, inst)
- }
- data, err := json.MarshalIndent(store, "", " ")
- if err != nil {
- return err
- }
- return os.WriteFile(m.storePath, data, 0644)
- }
- func (m *Manager) List() []VRRPInstance {
- m.mu.RLock()
- defer m.mu.RUnlock()
- isDaemonActive := m.isKeepalivedRunning()
- list := make([]VRRPInstance, 0, len(m.instances))
- for _, inst := range m.instances {
- copyInst := inst
- copyInst.VirtualMAC = fmt.Sprintf("00:00:5E:00:01:%02X", copyInst.VRID)
- if copyInst.Disabled {
- copyInst.State = "DISABLED"
- copyInst.Running = false
- } else {
- copyInst.State, copyInst.Running, copyInst.LastTransition = m.detectInstanceState(copyInst, isDaemonActive)
- }
- list = append(list, copyInst)
- }
- sort.Slice(list, func(i, j int) bool {
- if list[i].Interface != list[j].Interface {
- return list[i].Interface < list[j].Interface
- }
- return list[i].VRID < list[j].VRID
- })
- return list
- }
- func (m *Manager) detectInstanceState(inst VRRPInstance, daemonActive bool) (string, bool, time.Time) {
- link, err := netlink.LinkByName(inst.Interface)
- if err != nil || link.Attrs().OperState == netlink.OperDown {
- return "FAULT", false, time.Time{}
- }
- // 1. Direct Kernel Check: Does this interface currently hold any of the virtual IPs?
- if len(inst.VirtualIPs) > 0 {
- addrs, err := netlink.AddrList(link, netlink.FAMILY_V4)
- if err == nil {
- for _, a := range addrs {
- for _, vip := range inst.VirtualIPs {
- cleanVIP := strings.Split(vip, "/")[0]
- if a.IP.String() == cleanVIP {
- return "MASTER", true, time.Time{}
- }
- }
- }
- }
- }
- // 2. Check notify state file
- stateFile := filepath.Join(VRRPStatusDir, fmt.Sprintf("%s.state", sanitizeName(inst.Name)))
- if data, err := os.ReadFile(stateFile); err == nil {
- parts := strings.Fields(string(data))
- if len(parts) >= 1 {
- st := parts[0]
- var t time.Time
- if len(parts) >= 2 {
- t, _ = time.Parse(time.RFC3339, parts[1])
- }
- if st == "MASTER" {
- return "MASTER", true, t
- }
- if st == "FAULT" {
- return "FAULT", false, t
- }
- if st == "BACKUP" && daemonActive {
- return "BACKUP", true, t
- }
- }
- }
- if daemonActive {
- return "BACKUP", true, time.Time{}
- }
- return "FAULT", false, time.Time{}
- }
- func (m *Manager) isKeepalivedRunning() bool {
- out, err := exec.Command("systemctl", "is-active", "keepalived").Output()
- return err == nil && strings.TrimSpace(string(out)) == "active"
- }
- func (m *Manager) Get(id string) (VRRPInstance, bool) {
- m.mu.RLock()
- defer m.mu.RUnlock()
- inst, ok := m.instances[id]
- if !ok {
- return VRRPInstance{}, false
- }
- inst.VirtualMAC = fmt.Sprintf("00:00:5E:00:01:%02X", inst.VRID)
- if inst.Disabled {
- inst.State = "DISABLED"
- inst.Running = false
- } else {
- inst.State, inst.Running, inst.LastTransition = m.detectInstanceState(inst, m.isKeepalivedRunning())
- }
- return inst, true
- }
- func (m *Manager) Save(inst VRRPInstance) (VRRPInstance, error) {
- m.mu.Lock()
- defer m.mu.Unlock()
- if inst.Name == "" || inst.Interface == "" {
- return inst, fmt.Errorf("name and interface are required")
- }
- if inst.VRID < 1 || inst.VRID > 255 {
- return inst, fmt.Errorf("VRID must be between 1 and 255")
- }
- if inst.Priority < 1 || inst.Priority > 255 {
- inst.Priority = 100
- }
- if inst.Interval < 1 {
- inst.Interval = 1
- }
- if inst.Version != 2 && inst.Version != 3 {
- inst.Version = 3
- }
- if len(inst.VirtualIPs) == 0 {
- return inst, fmt.Errorf("at least one Virtual IP (VIP) is required")
- }
- // Validate VIP formatting
- for _, vip := range inst.VirtualIPs {
- ipStr := strings.Split(vip, "/")[0]
- if net.ParseIP(strings.TrimSpace(ipStr)) == nil {
- return inst, fmt.Errorf("invalid virtual IP address: %s", vip)
- }
- }
- if inst.ID == "" {
- inst.ID = fmt.Sprintf("vrrp-%s", sanitizeName(inst.Name))
- }
- m.instances[inst.ID] = inst
- if err := m.saveLocked(); err != nil {
- return inst, err
- }
- if err := m.applyConfigLocked(); err != nil {
- return inst, err
- }
- return inst, nil
- }
- func (m *Manager) Delete(id string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- inst, ok := m.instances[id]
- if !ok {
- return fmt.Errorf("VRRP instance not found: %s", id)
- }
- delete(m.instances, id)
- _ = os.Remove(filepath.Join(VRRPStatusDir, fmt.Sprintf("%s.state", sanitizeName(inst.Name))))
- if err := m.saveLocked(); err != nil {
- return err
- }
- return m.applyConfigLocked()
- }
- func (m *Manager) ToggleState(id string, disabled bool) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- inst, ok := m.instances[id]
- if !ok {
- return fmt.Errorf("VRRP instance not found: %s", id)
- }
- inst.Disabled = disabled
- m.instances[id] = inst
- if err := m.saveLocked(); err != nil {
- return err
- }
- return m.applyConfigLocked()
- }
- func (m *Manager) GetSummary() VRRPSummary {
- list := m.List()
- s := VRRPSummary{
- TotalInstances: len(list),
- }
- for _, inst := range list {
- if inst.State == "MASTER" {
- s.MasterInstances++
- } else if inst.State == "BACKUP" {
- s.BackupInstances++
- } else if inst.State == "FAULT" {
- s.FaultInstances++
- }
- }
- return s
- }
- func (m *Manager) applyConfigLocked() error {
- m.ensureNotifyScript()
- activeCount := 0
- for _, inst := range m.instances {
- if !inst.Disabled {
- activeCount++
- }
- }
- if activeCount == 0 {
- _ = exec.Command("systemctl", "stop", "keepalived").Run()
- _ = os.Remove(m.confPath)
- return nil
- }
- var sb strings.Builder
- sb.WriteString("# ==================================================================\n")
- sb.WriteString("# Enterprise VRRP (Virtual Router Redundancy Protocol) Configuration\n")
- sb.WriteString("# Managed by DebTik Network Appliance Controller. DO NOT EDIT DIRECTLY.\n")
- sb.WriteString("# ==================================================================\n\n")
- sb.WriteString("global_defs {\n")
- sb.WriteString(" router_id proxmox-router\n")
- sb.WriteString(" vrrp_version 3\n")
- sb.WriteString(" script_user root\n")
- sb.WriteString(" enable_script_security\n")
- sb.WriteString("}\n\n")
- for _, inst := range m.instances {
- if inst.Disabled {
- continue
- }
- instTag := sanitizeName(inst.Name)
- sb.WriteString(fmt.Sprintf("# --- VRRP Instance: %s on %s (VRID: %d) ---\n", inst.Name, inst.Interface, inst.VRID))
- sb.WriteString(fmt.Sprintf("vrrp_instance %s {\n", instTag))
- sb.WriteString(" state BACKUP\n")
- sb.WriteString(fmt.Sprintf(" interface %s\n", inst.Interface))
- sb.WriteString(fmt.Sprintf(" virtual_router_id %d\n", inst.VRID))
- sb.WriteString(fmt.Sprintf(" priority %d\n", inst.Priority))
- sb.WriteString(fmt.Sprintf(" advert_int %d\n", inst.Interval))
- if inst.Preempt {
- sb.WriteString(" preempt\n")
- } else {
- sb.WriteString(" nopreempt\n")
- }
- if inst.Version == 2 {
- sb.WriteString(" version 2\n")
- }
- if inst.AuthType == "pass" && inst.AuthPassword != "" {
- sb.WriteString(" authentication {\n")
- sb.WriteString(" auth_type PASS\n")
- sb.WriteString(fmt.Sprintf(" auth_pass %s\n", inst.AuthPassword))
- sb.WriteString(" }\n")
- }
- sb.WriteString(" virtual_ipaddress {\n")
- for _, vip := range inst.VirtualIPs {
- vipTrim := strings.TrimSpace(vip)
- if vipTrim != "" {
- if strings.Contains(vipTrim, "/") {
- sb.WriteString(fmt.Sprintf(" %s dev %s\n", vipTrim, inst.Interface))
- } else {
- sb.WriteString(fmt.Sprintf(" %s/32 dev %s\n", vipTrim, inst.Interface))
- }
- }
- }
- sb.WriteString(" }\n")
- sb.WriteString(fmt.Sprintf(" notify_master \"%s MASTER %s\"\n", VRRPNotifyScriptPath, instTag))
- sb.WriteString(fmt.Sprintf(" notify_backup \"%s BACKUP %s\"\n", VRRPNotifyScriptPath, instTag))
- sb.WriteString(fmt.Sprintf(" notify_fault \"%s FAULT %s\"\n", VRRPNotifyScriptPath, instTag))
- sb.WriteString(fmt.Sprintf(" notify_stop \"%s STOP %s\"\n", VRRPNotifyScriptPath, instTag))
- sb.WriteString("}\n\n")
- }
- if err := os.WriteFile(m.confPath, []byte(sb.String()), 0644); err != nil {
- return fmt.Errorf("failed to write keepalived.conf: %w", err)
- }
- cmd := exec.Command("systemctl", "restart", "keepalived")
- if out, err := cmd.CombinedOutput(); err != nil {
- return fmt.Errorf("failed to restart keepalived: %s", string(out))
- }
- return nil
- }
- // RenameInterface updates VRRP instance interface bindings when an interface is renamed.
- func (m *Manager) RenameInterface(oldName, newName string) {
- m.mu.Lock()
- defer m.mu.Unlock()
- changed := false
- for id, inst := range m.instances {
- if inst.Interface == oldName {
- inst.Interface = newName
- m.instances[id] = inst
- changed = true
- }
- }
- if changed {
- _ = m.saveLocked()
- _ = m.applyConfigLocked()
- }
- }
- func sanitizeName(n string) string {
- var res strings.Builder
- for _, r := range n {
- if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {
- res.WriteRune(r)
- } else {
- res.WriteRune('_')
- }
- }
- s := res.String()
- if s == "" {
- return "vrrp1"
- }
- return s
- }
|