Volt VMM (Neutron Stardust): source-available under AGPSL v5.0
KVM-based microVMM for the Volt platform: - Sub-second VM boot times - Minimal memory footprint - Landlock LSM + seccomp security - Virtio device support - Custom kernel management Copyright (c) Armored Gates LLC. All rights reserved. Licensed under AGPSL v5.0
This commit is contained in:
349
networking/pkg/unified/ipam.go
Normal file
349
networking/pkg/unified/ipam.go
Normal file
@@ -0,0 +1,349 @@
|
||||
package unified
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// IPAM manages IP address allocation for networks
|
||||
type IPAM struct {
|
||||
stateDir string
|
||||
pools map[string]*Pool
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Pool represents an IP address pool for a network
|
||||
type Pool struct {
|
||||
// Network name
|
||||
Name string `json:"name"`
|
||||
|
||||
// Subnet
|
||||
Subnet *net.IPNet `json:"subnet"`
|
||||
|
||||
// Gateway address
|
||||
Gateway net.IP `json:"gateway"`
|
||||
|
||||
// Pool start (first allocatable address)
|
||||
Start net.IP `json:"start"`
|
||||
|
||||
// Pool end (last allocatable address)
|
||||
End net.IP `json:"end"`
|
||||
|
||||
// Static reservations (workloadID -> IP)
|
||||
Reservations map[string]net.IP `json:"reservations"`
|
||||
|
||||
// Active leases
|
||||
Leases map[string]*Lease `json:"leases"`
|
||||
|
||||
// Free IPs (bitmap for fast allocation)
|
||||
allocated map[uint32]bool
|
||||
}
|
||||
|
||||
// NewIPAM creates a new IPAM instance
|
||||
func NewIPAM(stateDir string) (*IPAM, error) {
|
||||
if err := os.MkdirAll(stateDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("create IPAM state dir: %w", err)
|
||||
}
|
||||
|
||||
ipam := &IPAM{
|
||||
stateDir: stateDir,
|
||||
pools: make(map[string]*Pool),
|
||||
}
|
||||
|
||||
// Load existing state
|
||||
if err := ipam.loadState(); err != nil {
|
||||
// Non-fatal, might be first run
|
||||
_ = err
|
||||
}
|
||||
|
||||
return ipam, nil
|
||||
}
|
||||
|
||||
// AddPool adds a new IP pool for a network
|
||||
func (i *IPAM) AddPool(name string, subnet *net.IPNet, gateway net.IP, reservations map[string]net.IP) error {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
|
||||
// Calculate pool range
|
||||
start := nextIP(subnet.IP)
|
||||
if gateway != nil && gateway.Equal(start) {
|
||||
start = nextIP(start)
|
||||
}
|
||||
|
||||
// Broadcast address is last in subnet
|
||||
end := lastIP(subnet)
|
||||
|
||||
pool := &Pool{
|
||||
Name: name,
|
||||
Subnet: subnet,
|
||||
Gateway: gateway,
|
||||
Start: start,
|
||||
End: end,
|
||||
Reservations: reservations,
|
||||
Leases: make(map[string]*Lease),
|
||||
allocated: make(map[uint32]bool),
|
||||
}
|
||||
|
||||
// Mark gateway as allocated
|
||||
if gateway != nil {
|
||||
pool.allocated[ipToUint32(gateway)] = true
|
||||
}
|
||||
|
||||
// Mark reservations as allocated
|
||||
for _, ip := range reservations {
|
||||
pool.allocated[ipToUint32(ip)] = true
|
||||
}
|
||||
|
||||
i.pools[name] = pool
|
||||
return i.saveState()
|
||||
}
|
||||
|
||||
// Allocate allocates an IP address for a workload
|
||||
func (i *IPAM) Allocate(network, workloadID string, mac net.HardwareAddr) (*Lease, error) {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
|
||||
pool, ok := i.pools[network]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("network %s not found", network)
|
||||
}
|
||||
|
||||
// Check if workload already has a lease
|
||||
if lease, ok := pool.Leases[workloadID]; ok {
|
||||
return lease, nil
|
||||
}
|
||||
|
||||
// Check for static reservation
|
||||
if ip, ok := pool.Reservations[workloadID]; ok {
|
||||
lease := &Lease{
|
||||
IP: ip,
|
||||
MAC: mac,
|
||||
WorkloadID: workloadID,
|
||||
Start: time.Now(),
|
||||
Expires: time.Now().Add(365 * 24 * time.Hour), // Long lease for static
|
||||
Static: true,
|
||||
}
|
||||
pool.Leases[workloadID] = lease
|
||||
pool.allocated[ipToUint32(ip)] = true
|
||||
_ = i.saveState()
|
||||
return lease, nil
|
||||
}
|
||||
|
||||
// Find free IP in pool
|
||||
ip, err := pool.findFreeIP()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lease := &Lease{
|
||||
IP: ip,
|
||||
MAC: mac,
|
||||
WorkloadID: workloadID,
|
||||
Start: time.Now(),
|
||||
Expires: time.Now().Add(24 * time.Hour), // Default 24h lease
|
||||
Static: false,
|
||||
}
|
||||
|
||||
pool.Leases[workloadID] = lease
|
||||
pool.allocated[ipToUint32(ip)] = true
|
||||
_ = i.saveState()
|
||||
|
||||
return lease, nil
|
||||
}
|
||||
|
||||
// Release releases an IP address allocation
|
||||
func (i *IPAM) Release(network, workloadID string) error {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
|
||||
pool, ok := i.pools[network]
|
||||
if !ok {
|
||||
return nil // Network doesn't exist, nothing to release
|
||||
}
|
||||
|
||||
lease, ok := pool.Leases[workloadID]
|
||||
if !ok {
|
||||
return nil // No lease, nothing to release
|
||||
}
|
||||
|
||||
// Don't release static reservations from allocated map
|
||||
if !lease.Static {
|
||||
delete(pool.allocated, ipToUint32(lease.IP))
|
||||
}
|
||||
|
||||
delete(pool.Leases, workloadID)
|
||||
return i.saveState()
|
||||
}
|
||||
|
||||
// GetLease returns the current lease for a workload
|
||||
func (i *IPAM) GetLease(network, workloadID string) (*Lease, error) {
|
||||
i.mu.RLock()
|
||||
defer i.mu.RUnlock()
|
||||
|
||||
pool, ok := i.pools[network]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("network %s not found", network)
|
||||
}
|
||||
|
||||
lease, ok := pool.Leases[workloadID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no lease for %s", workloadID)
|
||||
}
|
||||
|
||||
return lease, nil
|
||||
}
|
||||
|
||||
// ListLeases returns all active leases for a network
|
||||
func (i *IPAM) ListLeases(network string) ([]*Lease, error) {
|
||||
i.mu.RLock()
|
||||
defer i.mu.RUnlock()
|
||||
|
||||
pool, ok := i.pools[network]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("network %s not found", network)
|
||||
}
|
||||
|
||||
result := make([]*Lease, 0, len(pool.Leases))
|
||||
for _, lease := range pool.Leases {
|
||||
result = append(result, lease)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Reserve creates a static IP reservation
|
||||
func (i *IPAM) Reserve(network, workloadID string, ip net.IP) error {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
|
||||
pool, ok := i.pools[network]
|
||||
if !ok {
|
||||
return fmt.Errorf("network %s not found", network)
|
||||
}
|
||||
|
||||
// Check if IP is in subnet
|
||||
if !pool.Subnet.Contains(ip) {
|
||||
return fmt.Errorf("IP %s not in subnet %s", ip, pool.Subnet)
|
||||
}
|
||||
|
||||
// Check if already allocated
|
||||
if pool.allocated[ipToUint32(ip)] {
|
||||
return fmt.Errorf("IP %s already allocated", ip)
|
||||
}
|
||||
|
||||
if pool.Reservations == nil {
|
||||
pool.Reservations = make(map[string]net.IP)
|
||||
}
|
||||
pool.Reservations[workloadID] = ip
|
||||
pool.allocated[ipToUint32(ip)] = true
|
||||
|
||||
return i.saveState()
|
||||
}
|
||||
|
||||
// Unreserve removes a static IP reservation
|
||||
func (i *IPAM) Unreserve(network, workloadID string) error {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
|
||||
pool, ok := i.pools[network]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ip, ok := pool.Reservations[workloadID]; ok {
|
||||
delete(pool.allocated, ipToUint32(ip))
|
||||
delete(pool.Reservations, workloadID)
|
||||
return i.saveState()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findFreeIP finds the next available IP in the pool
|
||||
func (p *Pool) findFreeIP() (net.IP, error) {
|
||||
startUint := ipToUint32(p.Start)
|
||||
endUint := ipToUint32(p.End)
|
||||
|
||||
for ip := startUint; ip <= endUint; ip++ {
|
||||
if !p.allocated[ip] {
|
||||
return uint32ToIP(ip), nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no free IPs in pool %s", p.Name)
|
||||
}
|
||||
|
||||
// saveState persists IPAM state to disk
|
||||
func (i *IPAM) saveState() error {
|
||||
data, err := json.MarshalIndent(i.pools, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filepath.Join(i.stateDir, "pools.json"), data, 0644)
|
||||
}
|
||||
|
||||
// loadState loads IPAM state from disk
|
||||
func (i *IPAM) loadState() error {
|
||||
data, err := os.ReadFile(filepath.Join(i.stateDir, "pools.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &i.pools); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Rebuild allocated maps
|
||||
for _, pool := range i.pools {
|
||||
pool.allocated = make(map[uint32]bool)
|
||||
if pool.Gateway != nil {
|
||||
pool.allocated[ipToUint32(pool.Gateway)] = true
|
||||
}
|
||||
for _, ip := range pool.Reservations {
|
||||
pool.allocated[ipToUint32(ip)] = true
|
||||
}
|
||||
for _, lease := range pool.Leases {
|
||||
pool.allocated[ipToUint32(lease.IP)] = true
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper functions for IP math
|
||||
|
||||
func ipToUint32(ip net.IP) uint32 {
|
||||
ip = ip.To4()
|
||||
if ip == nil {
|
||||
return 0
|
||||
}
|
||||
return binary.BigEndian.Uint32(ip)
|
||||
}
|
||||
|
||||
func uint32ToIP(n uint32) net.IP {
|
||||
ip := make(net.IP, 4)
|
||||
binary.BigEndian.PutUint32(ip, n)
|
||||
return ip
|
||||
}
|
||||
|
||||
func nextIP(ip net.IP) net.IP {
|
||||
return uint32ToIP(ipToUint32(ip) + 1)
|
||||
}
|
||||
|
||||
func lastIP(subnet *net.IPNet) net.IP {
|
||||
// Get the broadcast address (last IP in subnet)
|
||||
ip := subnet.IP.To4()
|
||||
mask := subnet.Mask
|
||||
broadcast := make(net.IP, 4)
|
||||
for i := range ip {
|
||||
broadcast[i] = ip[i] | ^mask[i]
|
||||
}
|
||||
// Return one before broadcast (last usable)
|
||||
return uint32ToIP(ipToUint32(broadcast) - 1)
|
||||
}
|
||||
Reference in New Issue
Block a user