Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 32 additions & 12 deletions lib/network/allocate.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ func (m *manager) CreateAllocation(ctx context.Context, req AllocateRequest) (*N
m.mu.Lock()
defer m.mu.Unlock()

// 1. Check name uniqueness (exclude current instance to allow restarts)
exists, err := m.NameExists(ctx, req.InstanceName, req.InstanceID)
allocations, err := m.ListAllocations(ctx)
if err != nil {
return nil, fmt.Errorf("check name exists: %w", err)
return nil, fmt.Errorf("list allocations: %w", err)
}

// 1. Check name uniqueness (exclude current instance to allow restarts)
exists := nameExistsInAllocations(allocations, req.InstanceName, req.InstanceID)
if exists {
return nil, fmt.Errorf("%w: instance name '%s' already exists, can't assign into same network: %s",
ErrNameExists, req.InstanceName, network.Name)
Expand All @@ -44,13 +46,13 @@ func (m *manager) CreateAllocation(ctx context.Context, req AllocateRequest) (*N
// Random selection reduces predictability and helps distribute IPs across the subnet.
// This is especially useful for large /16 networks and reduces conflicts when
// moving standby VMs across hosts.
ip, err := m.allocateNextIP(ctx, network.Subnet)
ip, err := allocateNextIPFromAllocations(network.Subnet, allocations)
if err != nil {
return nil, fmt.Errorf("allocate IP: %w", err)
}

// 3. Generate unused MAC (02:00:00:... format - locally administered)
mac, err := m.allocateUniqueMAC(ctx)
mac, err := allocateUniqueMACFromAllocations(allocations, generateMAC)
if err != nil {
return nil, fmt.Errorf("allocate MAC: %w", err)
}
Expand Down Expand Up @@ -225,16 +227,18 @@ func (m *manager) getOrInitDefaultNetwork(ctx context.Context) (*Network, error)
// allocateNextIP picks a random available IP in the subnet
// Retries up to 5 times if conflicts occur
func (m *manager) allocateNextIP(ctx context.Context, subnet string) (string, error) {
// Parse subnet
_, ipNet, err := net.ParseCIDR(subnet)
allocations, err := m.ListAllocations(ctx)
if err != nil {
return "", fmt.Errorf("parse subnet: %w", err)
return "", fmt.Errorf("list allocations: %w", err)
}
return allocateNextIPFromAllocations(subnet, allocations)
}

// Get all currently allocated IPs
allocations, err := m.ListAllocations(ctx)
func allocateNextIPFromAllocations(subnet string, allocations []Allocation) (string, error) {
// Parse subnet
_, ipNet, err := net.ParseCIDR(subnet)
if err != nil {
return "", fmt.Errorf("list allocations: %w", err)
return "", fmt.Errorf("parse subnet: %w", err)
}

// Build set of used IPs
Expand Down Expand Up @@ -324,6 +328,10 @@ func (m *manager) allocateUniqueMAC(ctx context.Context) (string, error) {
return "", fmt.Errorf("list allocations: %w", err)
}

return allocateUniqueMACFromAllocations(allocations, generateMAC)
}

func allocateUniqueMACFromAllocations(allocations []Allocation, generate func() (string, error)) (string, error) {
usedMACs := make(map[string]bool)
for _, alloc := range allocations {
mac := strings.ToLower(strings.TrimSpace(alloc.MAC))
Expand All @@ -332,7 +340,19 @@ func (m *manager) allocateUniqueMAC(ctx context.Context) (string, error) {
}
}

return allocateUniqueMACFromSet(usedMACs, generateMAC)
return allocateUniqueMACFromSet(usedMACs, generate)
}

func nameExistsInAllocations(allocations []Allocation, name string, excludeInstanceID string) bool {
for _, alloc := range allocations {
if excludeInstanceID != "" && alloc.InstanceID == excludeInstanceID {
continue
}
if alloc.InstanceName == name {
return true
}
}
return false
}

func allocateUniqueMACFromSet(usedMACs map[string]bool, generate func() (string, error)) (string, error) {
Expand Down
17 changes: 3 additions & 14 deletions lib/network/bridge_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -526,10 +526,7 @@ func (m *manager) createTAPDevice(ctx context.Context, tapName, bridgeName strin
}

// 3. Set TAP up
tapLink, err := netlink.LinkByName(tapName)
if err != nil {
return "", fmt.Errorf("get TAP link: %w", err)
}
tapLink := tap

if err := netlink.LinkSetUp(tapLink); err != nil {
return "", fmt.Errorf("set TAP up: %w", err)
Expand All @@ -547,16 +544,8 @@ func (m *manager) createTAPDevice(ctx context.Context, tapName, bridgeName strin

// 5. Enable port isolation so isolated TAPs can't directly talk to each other (requires kernel support and capabilities)
if isolated {
// Use shell command for bridge_slave isolated flag
// netlink library doesn't expose this flag yet
cmd := exec.Command("ip", "link", "set", tapName, "type", "bridge_slave", "isolated", "on")
// Enable ambient capabilities so child process inherits CAP_NET_ADMIN
cmd.SysProcAttr = &syscall.SysProcAttr{
AmbientCaps: []uintptr{unix.CAP_NET_ADMIN},
}
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("set isolation mode: %w (output: %s)", err, string(output))
if err := netlink.LinkSetIsolated(tapLink, true); err != nil {
return "", fmt.Errorf("set isolation mode: %w", err)
}
}

Expand Down
Loading