diff --git a/lib/network/allocate.go b/lib/network/allocate.go index 8f6badfc..40107874 100644 --- a/lib/network/allocate.go +++ b/lib/network/allocate.go @@ -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) @@ -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) } @@ -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 @@ -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)) @@ -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) { diff --git a/lib/network/bridge_linux.go b/lib/network/bridge_linux.go index b05a595d..0473c115 100644 --- a/lib/network/bridge_linux.go +++ b/lib/network/bridge_linux.go @@ -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) @@ -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) } }