Skip to content
Merged
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
50 changes: 44 additions & 6 deletions lib/instances/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ func (m *manager) restoreFromSnapshot(
}

func reconfigureGuestNetwork(ctx context.Context, stored *StoredMetadata, alloc *network.Allocation) error {
prefix, err := netmaskToPrefix(alloc.Netmask)
cmd, err := guestNetworkReconfigureCommand(alloc)
if err != nil {
return err
}
Expand All @@ -389,11 +389,6 @@ func reconfigureGuestNetwork(ctx context.Context, stored *StoredMetadata, alloc
return fmt.Errorf("create vsock dialer: %w", err)
}

cmd := fmt.Sprintf(
"ip -4 addr flush dev eth0 scope global && ip addr add %s/%d dev eth0 && ip link set dev eth0 up && ip route replace default via %s dev eth0",
alloc.IP, prefix, alloc.Gateway,
)

var stdout, stderr bytes.Buffer
exit, err := guest.ExecIntoInstance(ctx, dialer, guest.ExecOptions{
Command: []string{"sh", "-c", cmd},
Expand All @@ -411,6 +406,49 @@ func reconfigureGuestNetwork(ctx context.Context, stored *StoredMetadata, alloc
return nil
}

func guestNetworkReconfigureCommand(alloc *network.Allocation) (string, error) {
if alloc == nil {
return "", fmt.Errorf("missing network allocation")
}
ip := strings.TrimSpace(alloc.IP)
if ip == "" {
return "", fmt.Errorf("missing network allocation IP")
}
mac := strings.ToLower(strings.TrimSpace(alloc.MAC))
if mac == "" {
return "", fmt.Errorf("missing network allocation MAC")
}
if _, err := net.ParseMAC(mac); err != nil {
return "", fmt.Errorf("invalid network allocation MAC %q: %w", alloc.MAC, err)
}
gateway := strings.TrimSpace(alloc.Gateway)
if gateway == "" {
return "", fmt.Errorf("missing network allocation gateway")
}
prefix, err := netmaskToPrefix(alloc.Netmask)
if err != nil {
return "", err
}

return fmt.Sprintf(
// Bring eth0 down so Linux permits changing the interface MAC.
"ip link set dev eth0 down && "+
// Replace the snapshotted MAC with the MAC allocated for this fork.
"ip link set dev eth0 address %s && "+
// Remove the snapshotted IPv4 address from the source/starter guest.
"ip -4 addr flush dev eth0 scope global && "+
// Add the IPv4 address allocated for this fork.
"ip addr add %s/%d dev eth0 && "+
// Bring the interface back up after applying the new identity.
"ip link set dev eth0 up && "+
// Ensure outbound traffic uses the fork's allocated gateway.
"ip route replace default via %s dev eth0 && "+
// Drop snapshotted ARP/neighbor entries so peers are rediscovered.
"(ip neigh flush dev eth0 || true)",
mac, ip, prefix, gateway,
), nil
}

func networkConfigFromAllocation(alloc *network.Allocation) *network.NetworkConfig {
if alloc == nil {
return nil
Expand Down
32 changes: 32 additions & 0 deletions lib/instances/restore_egress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,38 @@ func TestNetworkConfigFromAllocation_PreservesDNS(t *testing.T) {
assert.Equal(t, alloc.TAPDevice, cfg.TAPDevice)
}

func TestGuestNetworkReconfigureCommand_AppliesAllocatedMAC(t *testing.T) {
t.Parallel()

alloc := &network.Allocation{
IP: "10.102.146.62",
MAC: "02:00:00:85:17:c8",
Gateway: "10.102.0.1",
Netmask: "255.255.0.0",
}

cmd, err := guestNetworkReconfigureCommand(alloc)
require.NoError(t, err)
assert.Contains(t, cmd, "ip link set dev eth0 down")
assert.Contains(t, cmd, "ip link set dev eth0 address 02:00:00:85:17:c8")
assert.Contains(t, cmd, "ip addr add 10.102.146.62/16 dev eth0")
assert.Contains(t, cmd, "ip route replace default via 10.102.0.1 dev eth0")
assert.Contains(t, cmd, "(ip neigh flush dev eth0 || true)")
assert.NotContains(t, cmd, "cat /sys/class/net/eth0/address")
}

func TestGuestNetworkReconfigureCommand_RequiresAllocatedMAC(t *testing.T) {
t.Parallel()

_, err := guestNetworkReconfigureCommand(&network.Allocation{
IP: "10.102.146.62",
Gateway: "10.102.0.1",
Netmask: "255.255.0.0",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "missing network allocation MAC")
}

func TestRequiresRestoreConfigDiskRefresh(t *testing.T) {
t.Parallel()

Expand Down
2 changes: 1 addition & 1 deletion lib/network/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ sudo setcap 'cap_net_admin,cap_net_bind_service=+eip' /path/to/hypeman
1. Get default network details
2. Check name uniqueness globally
3. Allocate next available IP (starting from .2, after gateway at .1)
4. Generate MAC (02:00:00:... format - locally administered)
4. Generate unused MAC (02:00:00:... format - locally administered)
5. Generate TAP name (tap-{first8chars-of-instance-id})
6. Create TAP device and attach to bridge

Expand Down
56 changes: 53 additions & 3 deletions lib/network/allocate.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ func (m *manager) CreateAllocation(ctx context.Context, req AllocateRequest) (*N
return nil, fmt.Errorf("allocate IP: %w", err)
}

// 3. Generate MAC (02:00:00:... format - locally administered)
mac, err := generateMAC()
// 3. Generate unused MAC (02:00:00:... format - locally administered)
mac, err := m.allocateUniqueMAC(ctx)
if err != nil {
return nil, fmt.Errorf("generate MAC: %w", err)
return nil, fmt.Errorf("allocate MAC: %w", err)
}

// 4. Generate TAP name (tap-{first8chars-of-id})
Expand Down Expand Up @@ -312,6 +312,51 @@ func incrementIP(ip net.IP, n int) net.IP {
return result
}

const (
macAllocationRandomAttempts = 5
macSuffixSpace = 1 << 24
)

// allocateUniqueMAC picks an unused locally administered MAC address.
func (m *manager) allocateUniqueMAC(ctx context.Context) (string, error) {
Comment thread
sjmiller609 marked this conversation as resolved.
allocations, err := m.ListAllocations(ctx)
if err != nil {
return "", fmt.Errorf("list allocations: %w", err)
}

usedMACs := make(map[string]bool)
for _, alloc := range allocations {
mac := strings.ToLower(strings.TrimSpace(alloc.MAC))
if mac != "" {
usedMACs[mac] = true
}
}

return allocateUniqueMACFromSet(usedMACs, generateMAC)
}

func allocateUniqueMACFromSet(usedMACs map[string]bool, generate func() (string, error)) (string, error) {
for attempt := 0; attempt < macAllocationRandomAttempts; attempt++ {
mac, err := generate()
if err != nil {
return "", err
}
mac = strings.ToLower(strings.TrimSpace(mac))
if !usedMACs[mac] {
return mac, nil
}
}

for suffix := 0; suffix < macSuffixSpace; suffix++ {
mac := formatMACSuffix(suffix)
if !usedMACs[mac] {
return mac, nil
}
}

return "", fmt.Errorf("no available MAC addresses after %d random attempts and full scan", macAllocationRandomAttempts)
}

// generateMAC generates a random MAC address with local administration bit set
func generateMAC() (string, error) {
// Generate 6 random bytes
Expand All @@ -331,6 +376,11 @@ func generateMAC() (string, error) {
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]), nil
}

func formatMACSuffix(suffix int) string {
return fmt.Sprintf("02:00:00:%02x:%02x:%02x",
byte(suffix>>16), byte(suffix>>8), byte(suffix))
}

// saveClassID persists the tc class ID for an instance so it survives restarts.
func (m *manager) saveClassID(instanceID, classID string) error {
return os.WriteFile(m.paths.InstanceDir(instanceID)+"/classid", []byte(classID), 0644)
Expand Down
38 changes: 38 additions & 0 deletions lib/network/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,44 @@ func TestGenerateMAC(t *testing.T) {
}
}

func TestAllocateUniqueMACFromSetRetriesCollisions(t *testing.T) {
used := map[string]bool{
"02:00:00:00:00:01": true,
}
candidates := []string{
"02:00:00:00:00:01",
"02:00:00:00:00:02",
}
calls := 0

mac, err := allocateUniqueMACFromSet(used, func() (string, error) {
candidate := candidates[calls]
calls++
return candidate, nil
})

require.NoError(t, err)
assert.Equal(t, "02:00:00:00:00:02", mac)
assert.Equal(t, 2, calls)
}

func TestAllocateUniqueMACFromSetFallsBackToSequentialScan(t *testing.T) {
used := map[string]bool{
"02:00:00:00:00:00": true,
"02:00:00:00:00:01": true,
}
calls := 0

mac, err := allocateUniqueMACFromSet(used, func() (string, error) {
calls++
return "02:00:00:00:00:01", nil
})

require.NoError(t, err)
assert.Equal(t, "02:00:00:00:00:02", mac)
assert.Equal(t, macAllocationRandomAttempts, calls)
}

func TestGenerateTAPName(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading