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)
|
||||
}
|
||||
537
networking/pkg/unified/manager.go
Normal file
537
networking/pkg/unified/manager.go
Normal file
@@ -0,0 +1,537 @@
|
||||
package unified
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
// Manager handles unified network operations for VMs and containers
|
||||
type Manager struct {
|
||||
// State directory for leases and config
|
||||
stateDir string
|
||||
|
||||
// Network configurations by name
|
||||
networks map[string]*NetworkConfig
|
||||
|
||||
// IPAM state
|
||||
ipam *IPAM
|
||||
|
||||
// Active interfaces by workload ID
|
||||
interfaces map[string]*Interface
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewManager creates a new unified network manager
|
||||
func NewManager(stateDir string) (*Manager, error) {
|
||||
if err := os.MkdirAll(stateDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("create state dir: %w", err)
|
||||
}
|
||||
|
||||
m := &Manager{
|
||||
stateDir: stateDir,
|
||||
networks: make(map[string]*NetworkConfig),
|
||||
interfaces: make(map[string]*Interface),
|
||||
}
|
||||
|
||||
// Initialize IPAM
|
||||
ipam, err := NewIPAM(filepath.Join(stateDir, "ipam"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("init IPAM: %w", err)
|
||||
}
|
||||
m.ipam = ipam
|
||||
|
||||
// Load existing state
|
||||
if err := m.loadState(); err != nil {
|
||||
// Non-fatal, might be first run
|
||||
_ = err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// AddNetwork registers a network configuration
|
||||
func (m *Manager) AddNetwork(config *NetworkConfig) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Validate
|
||||
if config.Name == "" {
|
||||
return fmt.Errorf("network name required")
|
||||
}
|
||||
if config.Subnet == "" {
|
||||
return fmt.Errorf("subnet required")
|
||||
}
|
||||
|
||||
_, subnet, err := net.ParseCIDR(config.Subnet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid subnet: %w", err)
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if config.MTU == 0 {
|
||||
config.MTU = 1500
|
||||
}
|
||||
if config.Type == "" {
|
||||
config.Type = NetworkBridged
|
||||
}
|
||||
if config.Bridge == "" && config.Type == NetworkBridged {
|
||||
config.Bridge = config.Name
|
||||
}
|
||||
|
||||
// Register with IPAM
|
||||
if config.IPAM != nil {
|
||||
var gateway net.IP
|
||||
if config.Gateway != "" {
|
||||
gateway = net.ParseIP(config.Gateway)
|
||||
}
|
||||
if err := m.ipam.AddPool(config.Name, subnet, gateway, nil); err != nil {
|
||||
return fmt.Errorf("register IPAM pool: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
m.networks[config.Name] = config
|
||||
return m.saveState()
|
||||
}
|
||||
|
||||
// EnsureBridge ensures the bridge exists and is configured
|
||||
func (m *Manager) EnsureBridge(name string) (*BridgeInfo, error) {
|
||||
// Check if bridge exists
|
||||
link, err := netlink.LinkByName(name)
|
||||
if err != nil {
|
||||
// Bridge doesn't exist, create it
|
||||
bridge := &netlink.Bridge{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: name,
|
||||
MTU: 1500,
|
||||
},
|
||||
}
|
||||
if err := netlink.LinkAdd(bridge); err != nil {
|
||||
return nil, fmt.Errorf("create bridge %s: %w", name, err)
|
||||
}
|
||||
link, err = netlink.LinkByName(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get created bridge: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure it's up
|
||||
if err := netlink.LinkSetUp(link); err != nil {
|
||||
return nil, fmt.Errorf("set bridge up: %w", err)
|
||||
}
|
||||
|
||||
// Get bridge info
|
||||
info := &BridgeInfo{
|
||||
Name: name,
|
||||
MTU: link.Attrs().MTU,
|
||||
Up: link.Attrs().OperState == netlink.OperUp,
|
||||
}
|
||||
|
||||
if link.Attrs().HardwareAddr != nil {
|
||||
info.MAC = link.Attrs().HardwareAddr
|
||||
}
|
||||
|
||||
// Get IP addresses
|
||||
addrs, err := netlink.AddrList(link, netlink.FAMILY_V4)
|
||||
if err == nil && len(addrs) > 0 {
|
||||
info.IP = addrs[0].IP
|
||||
info.Subnet = addrs[0].IPNet
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// CreateTAP creates a TAP device for a VM and attaches it to the bridge
|
||||
func (m *Manager) CreateTAP(network, workloadID string) (*Interface, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
config, ok := m.networks[network]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("network %s not found", network)
|
||||
}
|
||||
|
||||
// Generate TAP name (max 15 chars for Linux interface names)
|
||||
tapName := fmt.Sprintf("tap-%s", truncateID(workloadID, 10))
|
||||
|
||||
// Create TAP device
|
||||
tap := &netlink.Tuntap{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: tapName,
|
||||
MTU: config.MTU,
|
||||
},
|
||||
Mode: netlink.TUNTAP_MODE_TAP,
|
||||
Flags: netlink.TUNTAP_NO_PI | netlink.TUNTAP_VNET_HDR,
|
||||
Queues: 1, // Can increase for multi-queue
|
||||
}
|
||||
|
||||
if err := netlink.LinkAdd(tap); err != nil {
|
||||
return nil, fmt.Errorf("create TAP %s: %w", tapName, err)
|
||||
}
|
||||
|
||||
// Get the created link to get FD
|
||||
link, err := netlink.LinkByName(tapName)
|
||||
if err != nil {
|
||||
_ = netlink.LinkDel(tap)
|
||||
return nil, fmt.Errorf("get TAP link: %w", err)
|
||||
}
|
||||
|
||||
// Get the file descriptor from the TAP
|
||||
// This requires opening /dev/net/tun with the TAP name
|
||||
fd, err := openTAPFD(tapName)
|
||||
if err != nil {
|
||||
_ = netlink.LinkDel(tap)
|
||||
return nil, fmt.Errorf("open TAP fd: %w", err)
|
||||
}
|
||||
|
||||
// Attach to bridge
|
||||
bridge, err := netlink.LinkByName(config.Bridge)
|
||||
if err != nil {
|
||||
_ = netlink.LinkDel(tap)
|
||||
return nil, fmt.Errorf("get bridge %s: %w", config.Bridge, err)
|
||||
}
|
||||
|
||||
if err := netlink.LinkSetMaster(link, bridge); err != nil {
|
||||
_ = netlink.LinkDel(tap)
|
||||
return nil, fmt.Errorf("attach to bridge: %w", err)
|
||||
}
|
||||
|
||||
// Set link up
|
||||
if err := netlink.LinkSetUp(link); err != nil {
|
||||
_ = netlink.LinkDel(tap)
|
||||
return nil, fmt.Errorf("set TAP up: %w", err)
|
||||
}
|
||||
|
||||
// Generate MAC address
|
||||
mac := generateMAC(workloadID)
|
||||
|
||||
// Allocate IP if IPAM enabled
|
||||
var ip net.IP
|
||||
var mask net.IPMask
|
||||
var gateway net.IP
|
||||
if config.IPAM != nil {
|
||||
lease, err := m.ipam.Allocate(network, workloadID, mac)
|
||||
if err != nil {
|
||||
_ = netlink.LinkDel(tap)
|
||||
return nil, fmt.Errorf("allocate IP: %w", err)
|
||||
}
|
||||
ip = lease.IP
|
||||
_, subnet, _ := net.ParseCIDR(config.Subnet)
|
||||
mask = subnet.Mask
|
||||
if config.Gateway != "" {
|
||||
gateway = net.ParseIP(config.Gateway)
|
||||
}
|
||||
}
|
||||
|
||||
iface := &Interface{
|
||||
Name: tapName,
|
||||
MAC: mac,
|
||||
IP: ip,
|
||||
Mask: mask,
|
||||
Gateway: gateway,
|
||||
Bridge: config.Bridge,
|
||||
WorkloadID: workloadID,
|
||||
WorkloadType: WorkloadVM,
|
||||
FD: fd,
|
||||
}
|
||||
|
||||
m.interfaces[workloadID] = iface
|
||||
_ = m.saveState()
|
||||
|
||||
return iface, nil
|
||||
}
|
||||
|
||||
// CreateVeth creates a veth pair for a container and attaches host end to bridge
|
||||
func (m *Manager) CreateVeth(network, workloadID string) (*Interface, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
config, ok := m.networks[network]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("network %s not found", network)
|
||||
}
|
||||
|
||||
// Generate veth names (max 15 chars)
|
||||
hostName := fmt.Sprintf("veth-%s-h", truncateID(workloadID, 7))
|
||||
peerName := fmt.Sprintf("veth-%s-c", truncateID(workloadID, 7))
|
||||
|
||||
// Create veth pair
|
||||
veth := &netlink.Veth{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: hostName,
|
||||
MTU: config.MTU,
|
||||
},
|
||||
PeerName: peerName,
|
||||
}
|
||||
|
||||
if err := netlink.LinkAdd(veth); err != nil {
|
||||
return nil, fmt.Errorf("create veth pair: %w", err)
|
||||
}
|
||||
|
||||
// Get the created links
|
||||
hostLink, err := netlink.LinkByName(hostName)
|
||||
if err != nil {
|
||||
_ = netlink.LinkDel(veth)
|
||||
return nil, fmt.Errorf("get host veth: %w", err)
|
||||
}
|
||||
|
||||
peerLink, err := netlink.LinkByName(peerName)
|
||||
if err != nil {
|
||||
_ = netlink.LinkDel(veth)
|
||||
return nil, fmt.Errorf("get peer veth: %w", err)
|
||||
}
|
||||
|
||||
// Attach host end to bridge
|
||||
bridge, err := netlink.LinkByName(config.Bridge)
|
||||
if err != nil {
|
||||
_ = netlink.LinkDel(veth)
|
||||
return nil, fmt.Errorf("get bridge %s: %w", config.Bridge, err)
|
||||
}
|
||||
|
||||
if err := netlink.LinkSetMaster(hostLink, bridge); err != nil {
|
||||
_ = netlink.LinkDel(veth)
|
||||
return nil, fmt.Errorf("attach to bridge: %w", err)
|
||||
}
|
||||
|
||||
// Set host end up
|
||||
if err := netlink.LinkSetUp(hostLink); err != nil {
|
||||
_ = netlink.LinkDel(veth)
|
||||
return nil, fmt.Errorf("set host veth up: %w", err)
|
||||
}
|
||||
|
||||
// Generate MAC address
|
||||
mac := generateMAC(workloadID)
|
||||
|
||||
// Set MAC on peer (container) end
|
||||
if err := netlink.LinkSetHardwareAddr(peerLink, mac); err != nil {
|
||||
_ = netlink.LinkDel(veth)
|
||||
return nil, fmt.Errorf("set peer MAC: %w", err)
|
||||
}
|
||||
|
||||
// Allocate IP if IPAM enabled
|
||||
var ip net.IP
|
||||
var mask net.IPMask
|
||||
var gateway net.IP
|
||||
if config.IPAM != nil {
|
||||
lease, err := m.ipam.Allocate(network, workloadID, mac)
|
||||
if err != nil {
|
||||
_ = netlink.LinkDel(veth)
|
||||
return nil, fmt.Errorf("allocate IP: %w", err)
|
||||
}
|
||||
ip = lease.IP
|
||||
_, subnet, _ := net.ParseCIDR(config.Subnet)
|
||||
mask = subnet.Mask
|
||||
if config.Gateway != "" {
|
||||
gateway = net.ParseIP(config.Gateway)
|
||||
}
|
||||
}
|
||||
|
||||
iface := &Interface{
|
||||
Name: hostName,
|
||||
PeerName: peerName,
|
||||
MAC: mac,
|
||||
IP: ip,
|
||||
Mask: mask,
|
||||
Gateway: gateway,
|
||||
Bridge: config.Bridge,
|
||||
WorkloadID: workloadID,
|
||||
WorkloadType: WorkloadContainer,
|
||||
}
|
||||
|
||||
m.interfaces[workloadID] = iface
|
||||
_ = m.saveState()
|
||||
|
||||
return iface, nil
|
||||
}
|
||||
|
||||
// MoveVethToNamespace moves the container end of a veth pair to a network namespace
|
||||
func (m *Manager) MoveVethToNamespace(workloadID string, nsFD int) error {
|
||||
m.mu.RLock()
|
||||
iface, ok := m.interfaces[workloadID]
|
||||
m.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("interface for %s not found", workloadID)
|
||||
}
|
||||
|
||||
if iface.PeerName == "" {
|
||||
return fmt.Errorf("not a veth pair interface")
|
||||
}
|
||||
|
||||
// Get peer link
|
||||
peerLink, err := netlink.LinkByName(iface.PeerName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get peer veth: %w", err)
|
||||
}
|
||||
|
||||
// Move to namespace
|
||||
if err := netlink.LinkSetNsFd(peerLink, nsFD); err != nil {
|
||||
return fmt.Errorf("move to namespace: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigureContainerInterface configures the interface inside the container namespace
|
||||
// This should be called from within the container's network namespace
|
||||
func (m *Manager) ConfigureContainerInterface(workloadID string) error {
|
||||
m.mu.RLock()
|
||||
iface, ok := m.interfaces[workloadID]
|
||||
m.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("interface for %s not found", workloadID)
|
||||
}
|
||||
|
||||
// Get the interface (should be the peer that was moved into this namespace)
|
||||
link, err := netlink.LinkByName(iface.PeerName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get interface: %w", err)
|
||||
}
|
||||
|
||||
// Set link up
|
||||
if err := netlink.LinkSetUp(link); err != nil {
|
||||
return fmt.Errorf("set link up: %w", err)
|
||||
}
|
||||
|
||||
// Add IP address if allocated
|
||||
if iface.IP != nil {
|
||||
addr := &netlink.Addr{
|
||||
IPNet: &net.IPNet{
|
||||
IP: iface.IP,
|
||||
Mask: iface.Mask,
|
||||
},
|
||||
}
|
||||
if err := netlink.AddrAdd(link, addr); err != nil {
|
||||
return fmt.Errorf("add IP address: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add default route via gateway
|
||||
if iface.Gateway != nil {
|
||||
route := &netlink.Route{
|
||||
Gw: iface.Gateway,
|
||||
}
|
||||
if err := netlink.RouteAdd(route); err != nil {
|
||||
return fmt.Errorf("add default route: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Release releases the network interface for a workload
|
||||
func (m *Manager) Release(workloadID string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
iface, ok := m.interfaces[workloadID]
|
||||
if !ok {
|
||||
return nil // Already released
|
||||
}
|
||||
|
||||
// Release IP from IPAM
|
||||
for network := range m.networks {
|
||||
_ = m.ipam.Release(network, workloadID)
|
||||
}
|
||||
|
||||
// Delete the interface
|
||||
link, err := netlink.LinkByName(iface.Name)
|
||||
if err == nil {
|
||||
_ = netlink.LinkDel(link)
|
||||
}
|
||||
|
||||
delete(m.interfaces, workloadID)
|
||||
return m.saveState()
|
||||
}
|
||||
|
||||
// GetInterface returns the interface for a workload
|
||||
func (m *Manager) GetInterface(workloadID string) (*Interface, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
iface, ok := m.interfaces[workloadID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("interface for %s not found", workloadID)
|
||||
}
|
||||
return iface, nil
|
||||
}
|
||||
|
||||
// ListInterfaces returns all managed interfaces
|
||||
func (m *Manager) ListInterfaces() []*Interface {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
result := make([]*Interface, 0, len(m.interfaces))
|
||||
for _, iface := range m.interfaces {
|
||||
result = append(result, iface)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// saveState persists current state to disk
|
||||
func (m *Manager) saveState() error {
|
||||
data, err := json.MarshalIndent(m.interfaces, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filepath.Join(m.stateDir, "interfaces.json"), data, 0644)
|
||||
}
|
||||
|
||||
// loadState loads state from disk
|
||||
func (m *Manager) loadState() error {
|
||||
data, err := os.ReadFile(filepath.Join(m.stateDir, "interfaces.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, &m.interfaces)
|
||||
}
|
||||
|
||||
// truncateID truncates a workload ID for use in interface names
|
||||
func truncateID(id string, maxLen int) string {
|
||||
if len(id) <= maxLen {
|
||||
return id
|
||||
}
|
||||
return id[:maxLen]
|
||||
}
|
||||
|
||||
// generateMAC generates a deterministic MAC address from workload ID
|
||||
func generateMAC(workloadID string) net.HardwareAddr {
|
||||
// Use first 5 bytes of workload ID hash
|
||||
// Set local/unicast bits
|
||||
mac := make([]byte, 6)
|
||||
mac[0] = 0x52 // Local, unicast (Volt prefix)
|
||||
mac[1] = 0x54
|
||||
mac[2] = 0x00
|
||||
|
||||
// Hash-based bytes
|
||||
h := 0
|
||||
for _, c := range workloadID {
|
||||
h = h*31 + int(c)
|
||||
}
|
||||
mac[3] = byte((h >> 16) & 0xFF)
|
||||
mac[4] = byte((h >> 8) & 0xFF)
|
||||
mac[5] = byte(h & 0xFF)
|
||||
|
||||
return mac
|
||||
}
|
||||
|
||||
// openTAPFD opens a TAP device and returns its file descriptor
|
||||
func openTAPFD(name string) (int, error) {
|
||||
// This is a simplified version - in production, use proper ioctl
|
||||
// The netlink library handles TAP creation, but we need the FD for VMM use
|
||||
|
||||
// For now, return -1 as placeholder
|
||||
// Real implementation would:
|
||||
// 1. Open /dev/net/tun
|
||||
// 2. ioctl TUNSETIFF with name and flags
|
||||
// 3. Return the fd
|
||||
return -1, fmt.Errorf("TAP FD extraction not yet implemented - use device fd from netlink")
|
||||
}
|
||||
199
networking/pkg/unified/types.go
Normal file
199
networking/pkg/unified/types.go
Normal file
@@ -0,0 +1,199 @@
|
||||
// Package unified provides shared networking for Volt VMs and Voltainer containers.
|
||||
//
|
||||
// Architecture:
|
||||
// - Single bridge (nova0) managed by systemd-networkd
|
||||
// - VMs connect via TAP devices
|
||||
// - Containers connect via veth pairs
|
||||
// - Unified IPAM for both workload types
|
||||
// - CNI-compatible configuration format
|
||||
package unified
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NetworkType defines the type of network connectivity
|
||||
type NetworkType string
|
||||
|
||||
const (
|
||||
// NetworkBridged connects workload to shared bridge with full L2 connectivity
|
||||
NetworkBridged NetworkType = "bridged"
|
||||
|
||||
// NetworkIsolated creates an isolated network namespace with no connectivity
|
||||
NetworkIsolated NetworkType = "isolated"
|
||||
|
||||
// NetworkHostOnly provides NAT-only connectivity to host network
|
||||
NetworkHostOnly NetworkType = "host-only"
|
||||
|
||||
// NetworkMacvtap provides near-native performance via macvtap
|
||||
NetworkMacvtap NetworkType = "macvtap"
|
||||
|
||||
// NetworkSRIOV provides SR-IOV VF passthrough
|
||||
NetworkSRIOV NetworkType = "sriov"
|
||||
|
||||
// NetworkNone disables networking entirely
|
||||
NetworkNone NetworkType = "none"
|
||||
)
|
||||
|
||||
// WorkloadType identifies whether this is a VM or container
|
||||
type WorkloadType string
|
||||
|
||||
const (
|
||||
WorkloadVM WorkloadType = "vm"
|
||||
WorkloadContainer WorkloadType = "container"
|
||||
)
|
||||
|
||||
// NetworkConfig is the unified configuration for both VMs and containers.
|
||||
// Compatible with CNI network config format.
|
||||
type NetworkConfig struct {
|
||||
// Network name (matches bridge name, e.g., "nova0")
|
||||
Name string `json:"name"`
|
||||
|
||||
// Network type
|
||||
Type NetworkType `json:"type"`
|
||||
|
||||
// Bridge name (for bridged networks)
|
||||
Bridge string `json:"bridge,omitempty"`
|
||||
|
||||
// Subnet in CIDR notation
|
||||
Subnet string `json:"subnet"`
|
||||
|
||||
// Gateway IP address
|
||||
Gateway string `json:"gateway,omitempty"`
|
||||
|
||||
// IPAM configuration
|
||||
IPAM *IPAMConfig `json:"ipam,omitempty"`
|
||||
|
||||
// DNS configuration
|
||||
DNS *DNSConfig `json:"dns,omitempty"`
|
||||
|
||||
// MTU (default: 1500)
|
||||
MTU int `json:"mtu,omitempty"`
|
||||
|
||||
// VLAN ID (optional, for tagged traffic)
|
||||
VLAN int `json:"vlan,omitempty"`
|
||||
|
||||
// EnableHairpin allows traffic to exit and re-enter on same port
|
||||
EnableHairpin bool `json:"enableHairpin,omitempty"`
|
||||
|
||||
// RateLimit in bytes/sec (0 = unlimited)
|
||||
RateLimit int64 `json:"rateLimit,omitempty"`
|
||||
}
|
||||
|
||||
// IPAMConfig defines IP address management settings
|
||||
type IPAMConfig struct {
|
||||
// Type: "static", "dhcp", or "pool"
|
||||
Type string `json:"type"`
|
||||
|
||||
// Subnet (CIDR notation)
|
||||
Subnet string `json:"subnet"`
|
||||
|
||||
// Gateway
|
||||
Gateway string `json:"gateway,omitempty"`
|
||||
|
||||
// Pool start address (for type=pool)
|
||||
PoolStart string `json:"poolStart,omitempty"`
|
||||
|
||||
// Pool end address (for type=pool)
|
||||
PoolEnd string `json:"poolEnd,omitempty"`
|
||||
|
||||
// Static IP address (for type=static)
|
||||
Address string `json:"address,omitempty"`
|
||||
|
||||
// Reservations maps workload ID to reserved IP
|
||||
Reservations map[string]string `json:"reservations,omitempty"`
|
||||
}
|
||||
|
||||
// DNSConfig defines DNS settings
|
||||
type DNSConfig struct {
|
||||
// Nameservers
|
||||
Nameservers []string `json:"nameservers,omitempty"`
|
||||
|
||||
// Search domains
|
||||
Search []string `json:"search,omitempty"`
|
||||
|
||||
// Options
|
||||
Options []string `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
// Interface represents an attached network interface
|
||||
type Interface struct {
|
||||
// Name of the interface (e.g., "tap-abc123", "veth-xyz-h")
|
||||
Name string `json:"name"`
|
||||
|
||||
// MAC address
|
||||
MAC net.HardwareAddr `json:"mac"`
|
||||
|
||||
// IP address (after IPAM allocation)
|
||||
IP net.IP `json:"ip,omitempty"`
|
||||
|
||||
// Subnet mask
|
||||
Mask net.IPMask `json:"mask,omitempty"`
|
||||
|
||||
// Gateway
|
||||
Gateway net.IP `json:"gateway,omitempty"`
|
||||
|
||||
// Bridge this interface is attached to
|
||||
Bridge string `json:"bridge"`
|
||||
|
||||
// Workload ID this interface belongs to
|
||||
WorkloadID string `json:"workloadId"`
|
||||
|
||||
// Workload type (VM or container)
|
||||
WorkloadType WorkloadType `json:"workloadType"`
|
||||
|
||||
// File descriptor (for TAP devices, ready for VMM use)
|
||||
FD int `json:"-"`
|
||||
|
||||
// Container-side interface name (for veth pairs)
|
||||
PeerName string `json:"peerName,omitempty"`
|
||||
|
||||
// Namespace file descriptor (for moving veth to container)
|
||||
NamespaceRef string `json:"-"`
|
||||
}
|
||||
|
||||
// Lease represents an IP address lease
|
||||
type Lease struct {
|
||||
// IP address
|
||||
IP net.IP `json:"ip"`
|
||||
|
||||
// MAC address
|
||||
MAC net.HardwareAddr `json:"mac"`
|
||||
|
||||
// Workload ID
|
||||
WorkloadID string `json:"workloadId"`
|
||||
|
||||
// Lease start time
|
||||
Start time.Time `json:"start"`
|
||||
|
||||
// Lease expiration time
|
||||
Expires time.Time `json:"expires"`
|
||||
|
||||
// Is this a static reservation?
|
||||
Static bool `json:"static"`
|
||||
}
|
||||
|
||||
// BridgeInfo contains information about a managed bridge
|
||||
type BridgeInfo struct {
|
||||
// Bridge name
|
||||
Name string `json:"name"`
|
||||
|
||||
// Bridge MAC address
|
||||
MAC net.HardwareAddr `json:"mac"`
|
||||
|
||||
// IP address on the bridge
|
||||
IP net.IP `json:"ip,omitempty"`
|
||||
|
||||
// Subnet
|
||||
Subnet *net.IPNet `json:"subnet,omitempty"`
|
||||
|
||||
// Attached interfaces
|
||||
Interfaces []string `json:"interfaces"`
|
||||
|
||||
// MTU
|
||||
MTU int `json:"mtu"`
|
||||
|
||||
// Is bridge up?
|
||||
Up bool `json:"up"`
|
||||
}
|
||||
Reference in New Issue
Block a user