| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490 |
- package vpn
- import (
- "crypto/rand"
- "encoding/json"
- "fmt"
- "net"
- "os"
- "os/exec"
- "path/filepath"
- "sort"
- "strconv"
- "strings"
- "sync"
- "time"
- )
- const DefaultVXLANStorePath = "/etc/debtik/vpn/vxlan.json"
- // VXLANVtep represents a remote VTEP (Virtual Tunnel Endpoint) for a VXLAN interface.
- type VXLANVtep struct {
- ID string `json:"id"`
- Interface string `json:"interface"` // Name of the parent VXLAN interface
- RemoteIP string `json:"remote_ip"` // Unicast IPv4 address of remote VTEP
- Port int `json:"port,omitempty"` // UDP port, defaults to parent VXLAN port
- Comment string `json:"comment,omitempty"`
- }
- // VXLANInstance represents a Linux/RouterOS VXLAN network interface.
- type VXLANInstance struct {
- ID string `json:"id"`
- Name string `json:"name"` // Interface name (e.g. "vxlan1", "vxlan100")
- VNI int `json:"vni"` // VXLAN Network Identifier (1 - 16777215)
- Port int `json:"port"` // UDP Destination Port (RFC 7348 standard: 4789)
- MTU int `json:"mtu"` // Interface MTU (default 1500)
- MACAddress string `json:"mac_address,omitempty"` // Hardware MAC address
- IPAddress string `json:"ip_address,omitempty"` // Overlay IP/CIDR (e.g. "10.200.0.1/24")
- ParentInterface string `json:"parent_interface,omitempty"` // Underlay interface (e.g. "eth0")
- LocalAddress string `json:"local_address,omitempty"` // VTEP local source IP
- Group string `json:"group,omitempty"` // Multicast group (e.g. "239.1.1.1")
- RemoteAddress string `json:"remote_address,omitempty"` // Point-to-point remote unicast IP
- Learning bool `json:"learning"` // MAC address learning (default: true)
- Comment string `json:"comment,omitempty"`
- Disabled bool `json:"disabled"`
- Running bool `json:"running"` // True if link is created and active in kernel
- VTEPs []*VXLANVtep `json:"vteps,omitempty"`
- RxPackets uint64 `json:"rx_packets"`
- TxPackets uint64 `json:"tx_packets"`
- RxBytes uint64 `json:"rx_bytes"`
- TxBytes uint64 `json:"tx_bytes"`
- RxHuman string `json:"rx_human,omitempty"`
- TxHuman string `json:"tx_human,omitempty"`
- LastError string `json:"last_error,omitempty"`
- }
- // VXLANManager manages persistent VXLAN interfaces and kernel links.
- type VXLANManager struct {
- mu sync.RWMutex
- storePath string
- tunnels map[string]*VXLANInstance
- }
- // NewVXLANManager creates a new manager with default or customized storage path.
- func NewVXLANManager(customPath ...string) *VXLANManager {
- store := DefaultVXLANStorePath
- if len(customPath) > 0 && customPath[0] != "" {
- store = customPath[0]
- }
- mgr := &VXLANManager{
- storePath: store,
- tunnels: make(map[string]*VXLANInstance),
- }
- mgr.load()
- mgr.SyncAll()
- return mgr
- }
- func (m *VXLANManager) load() {
- m.mu.Lock()
- defer m.mu.Unlock()
- data, err := os.ReadFile(m.storePath)
- if err != nil {
- return
- }
- var list []*VXLANInstance
- if err := json.Unmarshal(data, &list); err == nil {
- for _, t := range list {
- m.tunnels[t.ID] = t
- }
- }
- }
- func (m *VXLANManager) save() error {
- _ = os.MkdirAll(filepath.Dir(m.storePath), 0755)
- list := make([]*VXLANInstance, 0, len(m.tunnels))
- for _, t := range m.tunnels {
- list = append(list, t)
- }
- data, err := json.MarshalIndent(list, "", " ")
- if err != nil {
- return err
- }
- return os.WriteFile(m.storePath, data, 0644)
- }
- // List returns all configured VXLAN tunnels with updated statistics.
- func (m *VXLANManager) List() []*VXLANInstance {
- m.mu.Lock()
- defer m.mu.Unlock()
- list := make([]*VXLANInstance, 0, len(m.tunnels))
- for _, t := range m.tunnels {
- m.updateStats(t)
- list = append(list, t)
- }
- sort.Slice(list, func(i, j int) bool {
- if list[i].VNI != list[j].VNI {
- return list[i].VNI < list[j].VNI
- }
- return list[i].Name < list[j].Name
- })
- return list
- }
- // Get returns a single VXLAN instance by ID or interface name.
- func (m *VXLANManager) Get(idOrName string) (*VXLANInstance, bool) {
- m.mu.Lock()
- defer m.mu.Unlock()
- if t, ok := m.tunnels[idOrName]; ok {
- m.updateStats(t)
- return t, true
- }
- for _, t := range m.tunnels {
- if t.Name == idOrName {
- m.updateStats(t)
- return t, true
- }
- }
- return nil, false
- }
- // Save validates, stores, and synchronizes a VXLAN tunnel configuration into the kernel.
- func (m *VXLANManager) Save(inst *VXLANInstance) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- if inst.VNI <= 0 || inst.VNI > 16777215 {
- return fmt.Errorf("vni must be between 1 and 16777215")
- }
- if inst.Name == "" {
- inst.Name = fmt.Sprintf("vxlan%d", inst.VNI)
- }
- if inst.Port <= 0 {
- inst.Port = 4789 // RFC 7348 standard
- }
- if inst.MTU <= 0 {
- inst.MTU = 1500
- }
- if inst.ID == "" {
- inst.ID = fmt.Sprintf("vxlan-%d", time.Now().UnixNano())
- }
- if inst.MACAddress == "" {
- inst.MACAddress = generateVXLANMAC()
- }
- // Check for name or VNI collisions with existing different instances
- for _, existing := range m.tunnels {
- if existing.ID != inst.ID {
- if existing.Name == inst.Name {
- return fmt.Errorf("interface name %q already in use", inst.Name)
- }
- if existing.VNI == inst.VNI && existing.Port == inst.Port {
- return fmt.Errorf("vni %d on port %d already exists", inst.VNI, inst.Port)
- }
- }
- }
- // Sync link in kernel
- if err := m.syncKernelLink(inst); err != nil {
- return err
- }
- m.tunnels[inst.ID] = inst
- return m.save()
- }
- // SetState enables or disables a VXLAN interface.
- func (m *VXLANManager) SetState(idOrName string, disabled bool) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- inst := m.findLocked(idOrName)
- if inst == nil {
- return fmt.Errorf("vxlan interface %q not found", idOrName)
- }
- inst.Disabled = disabled
- if err := m.syncKernelLink(inst); err != nil {
- return err
- }
- return m.save()
- }
- // Delete removes a VXLAN interface from the kernel and persistent store.
- func (m *VXLANManager) Delete(idOrName string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- inst := m.findLocked(idOrName)
- if inst == nil {
- return fmt.Errorf("vxlan interface %q not found", idOrName)
- }
- // Delete kernel link
- _ = exec.Command("ip", "link", "del", "dev", inst.Name).Run()
- delete(m.tunnels, inst.ID)
- return m.save()
- }
- // AddVtep adds a remote VTEP peer to a VXLAN interface.
- func (m *VXLANManager) AddVtep(vxlanIDOrName string, vtep *VXLANVtep) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- inst := m.findLocked(vxlanIDOrName)
- if inst == nil {
- return fmt.Errorf("vxlan interface %q not found", vxlanIDOrName)
- }
- if vtep.RemoteIP == "" {
- return fmt.Errorf("remote_ip is required")
- }
- if net.ParseIP(vtep.RemoteIP) == nil {
- return fmt.Errorf("invalid remote IP %q", vtep.RemoteIP)
- }
- if vtep.Port <= 0 {
- vtep.Port = inst.Port
- }
- if vtep.ID == "" {
- vtep.ID = fmt.Sprintf("vtep-%d", time.Now().UnixNano())
- }
- vtep.Interface = inst.Name
- // Check duplicate remote IP
- for _, existing := range inst.VTEPs {
- if existing.RemoteIP == vtep.RemoteIP {
- return fmt.Errorf("vtep with remote-ip %s already exists for %s", vtep.RemoteIP, inst.Name)
- }
- }
- inst.VTEPs = append(inst.VTEPs, vtep)
- // Apply bridge fdb flood entry in kernel
- if !inst.Disabled {
- m.applyVtepKernel(inst.Name, vtep)
- }
- return m.save()
- }
- // DeleteVtep removes a remote VTEP peer from a VXLAN interface.
- func (m *VXLANManager) DeleteVtep(vxlanIDOrName, vtepIDOrIP string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- inst := m.findLocked(vxlanIDOrName)
- if inst == nil {
- return fmt.Errorf("vxlan interface %q not found", vxlanIDOrName)
- }
- idx := -1
- var targetVtep *VXLANVtep
- for i, v := range inst.VTEPs {
- if v.ID == vtepIDOrIP || v.RemoteIP == vtepIDOrIP {
- idx = i
- targetVtep = v
- break
- }
- }
- if idx == -1 {
- return fmt.Errorf("vtep %q not found on interface %s", vtepIDOrIP, inst.Name)
- }
- if !inst.Disabled && targetVtep != nil {
- m.removeVtepKernel(inst.Name, targetVtep)
- }
- inst.VTEPs = append(inst.VTEPs[:idx], inst.VTEPs[idx+1:]...)
- return m.save()
- }
- // ListVteps returns a slice of all configured VTEPs across interfaces.
- func (m *VXLANManager) ListVteps(filterIface string) []*VXLANVtep {
- m.mu.RLock()
- defer m.mu.RUnlock()
- var list []*VXLANVtep
- for _, inst := range m.tunnels {
- if filterIface != "" && inst.Name != filterIface && inst.ID != filterIface {
- continue
- }
- for _, v := range inst.VTEPs {
- list = append(list, v)
- }
- }
- 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].RemoteIP < list[j].RemoteIP
- })
- return list
- }
- // SyncAll reconciles all stored VXLAN interfaces into the kernel.
- func (m *VXLANManager) SyncAll() {
- m.mu.Lock()
- defer m.mu.Unlock()
- for _, inst := range m.tunnels {
- _ = m.syncKernelLink(inst)
- }
- }
- // RenameInterface updates parent underlay references when a link is renamed.
- func (m *VXLANManager) RenameInterface(oldName, newName string) {
- m.mu.Lock()
- defer m.mu.Unlock()
- changed := false
- for _, inst := range m.tunnels {
- if inst.ParentInterface == oldName {
- inst.ParentInterface = newName
- changed = true
- }
- }
- if changed {
- _ = m.save()
- }
- }
- func (m *VXLANManager) findLocked(idOrName string) *VXLANInstance {
- if t, ok := m.tunnels[idOrName]; ok {
- return t
- }
- for _, t := range m.tunnels {
- if t.Name == idOrName {
- return t
- }
- }
- return nil
- }
- func (m *VXLANManager) syncKernelLink(inst *VXLANInstance) error {
- // Remove existing link if present
- _ = exec.Command("ip", "link", "del", "dev", inst.Name).Run()
- if inst.Disabled {
- inst.Running = false
- return nil
- }
- args := []string{"link", "add", "dev", inst.Name, "type", "vxlan", "id", strconv.Itoa(inst.VNI)}
- dstPort := inst.Port
- if dstPort <= 0 {
- dstPort = 4789
- }
- args = append(args, "dstport", strconv.Itoa(dstPort))
- if inst.LocalAddress != "" {
- args = append(args, "local", inst.LocalAddress)
- }
- if inst.RemoteAddress != "" {
- args = append(args, "remote", inst.RemoteAddress)
- }
- if inst.Group != "" {
- args = append(args, "group", inst.Group)
- }
- if inst.ParentInterface != "" {
- args = append(args, "dev", inst.ParentInterface)
- }
- if !inst.Learning {
- args = append(args, "nolearning")
- }
- args = append(args, "ttl", "auto", "ageing", "300")
- out, err := exec.Command("ip", args...).CombinedOutput()
- if err != nil {
- inst.LastError = string(out)
- inst.Running = false
- return fmt.Errorf("failed to create vxlan %s: %s (%w)", inst.Name, strings.TrimSpace(string(out)), err)
- }
- if inst.MACAddress != "" {
- _ = exec.Command("ip", "link", "set", "dev", inst.Name, "address", inst.MACAddress).Run()
- }
- mtu := inst.MTU
- if mtu <= 0 {
- mtu = 1500
- }
- _ = exec.Command("ip", "link", "set", "dev", inst.Name, "mtu", strconv.Itoa(mtu)).Run()
- if out, err := exec.Command("ip", "link", "set", "dev", inst.Name, "up").CombinedOutput(); err != nil {
- inst.LastError = string(out)
- } else {
- inst.Running = true
- inst.LastError = ""
- }
- if inst.IPAddress != "" {
- _ = exec.Command("ip", "addr", "add", inst.IPAddress, "dev", inst.Name).Run()
- }
- for _, vtep := range inst.VTEPs {
- m.applyVtepKernel(inst.Name, vtep)
- }
- return nil
- }
- func (m *VXLANManager) applyVtepKernel(iface string, vtep *VXLANVtep) {
- if vtep.RemoteIP == "" {
- return
- }
- args := []string{"fdb", "append", "00:00:00:00:00:00", "dev", iface, "dst", vtep.RemoteIP}
- if vtep.Port > 0 && vtep.Port != 4789 {
- args = append(args, "port", strconv.Itoa(vtep.Port))
- }
- _ = exec.Command("bridge", args...).Run()
- }
- func (m *VXLANManager) removeVtepKernel(iface string, vtep *VXLANVtep) {
- if vtep.RemoteIP == "" {
- return
- }
- args := []string{"fdb", "del", "00:00:00:00:00:00", "dev", iface, "dst", vtep.RemoteIP}
- _ = exec.Command("bridge", args...).Run()
- }
- func (m *VXLANManager) updateStats(inst *VXLANInstance) {
- if inst.Name == "" {
- return
- }
- rxBytes, _ := readUint64File(fmt.Sprintf("/sys/class/net/%s/statistics/rx_bytes", inst.Name))
- txBytes, _ := readUint64File(fmt.Sprintf("/sys/class/net/%s/statistics/tx_bytes", inst.Name))
- rxPackets, _ := readUint64File(fmt.Sprintf("/sys/class/net/%s/statistics/rx_packets", inst.Name))
- txPackets, _ := readUint64File(fmt.Sprintf("/sys/class/net/%s/statistics/tx_packets", inst.Name))
- operstate, _ := os.ReadFile(fmt.Sprintf("/sys/class/net/%s/operstate", inst.Name))
- inst.RxBytes = rxBytes
- inst.TxBytes = txBytes
- inst.RxPackets = rxPackets
- inst.TxPackets = txPackets
- inst.RxHuman = formatBytes(rxBytes)
- inst.TxHuman = formatBytes(txBytes)
- if !inst.Disabled {
- stateStr := strings.TrimSpace(string(operstate))
- inst.Running = (stateStr == "up" || stateStr == "unknown")
- } else {
- inst.Running = false
- }
- }
- func readUint64File(path string) (uint64, error) {
- data, err := os.ReadFile(path)
- if err != nil {
- return 0, err
- }
- return strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64)
- }
- func generateVXLANMAC() string {
- buf := make([]byte, 3)
- _, _ = rand.Read(buf)
- return fmt.Sprintf("02:58:%02x:%02x:%02x:%02x", buf[0], buf[1], buf[2], time.Now().UnixNano()%256)
- }
|