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) }