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
65 changes: 63 additions & 2 deletions pkg/nmlite/interface_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,59 @@ package nmlite

import (
"fmt"
"strings"
"time"

"github.com/jetkvm/kvm/internal/network/types"
"github.com/jetkvm/kvm/pkg/nmlite/link"
"github.com/vishvananda/netlink"
)

type IfStateChangeReason uint

const (
IfStateOperStateChanged IfStateChangeReason = 1
IfStateOnlineStateChanged IfStateChangeReason = 2
IfStateMACAddressChanged IfStateChangeReason = 3
IfStateIPAddressesChanged IfStateChangeReason = 4
)

type IfStateChangeReasons []IfStateChangeReason

func (r IfStateChangeReason) String() string {
switch r {
case IfStateOperStateChanged:
return "oper state changed"
case IfStateOnlineStateChanged:
return "online state changed"
case IfStateMACAddressChanged:
return "MAC address changed"
case IfStateIPAddressesChanged:
return "IP addresses changed"
default:
return fmt.Sprintf("unknown change reason %d", r)
}
}

func (rs IfStateChangeReasons) String() string {
reasons := []string{}
for _, r := range rs {
reasons = append(reasons, r.String())
}
return strings.Join(reasons, ", ")
}

// updateInterfaceState updates the current interface state
func (im *InterfaceManager) updateInterfaceState() error {
nl, err := im.link()
if err != nil {
return fmt.Errorf("failed to get interface: %w", err)
}

var stateChanged bool
var (
stateChanged bool
changeReasons IfStateChangeReasons
)

attrs := nl.Attrs()

Expand All @@ -29,34 +67,41 @@ func (im *InterfaceManager) updateInterfaceState() error {
if im.state.Up != isUp {
im.state.Up = isUp
stateChanged = true
changeReasons = append(changeReasons, IfStateOperStateChanged)
}

// Check if the interface is online
isOnline := isUp && nl.HasGlobalUnicastAddress()
if im.state.Online != isOnline {
im.state.Online = isOnline
stateChanged = true
changeReasons = append(changeReasons, IfStateOnlineStateChanged)
}

// Check if the MAC address has changed
if im.state.MACAddress != attrs.HardwareAddr.String() {
im.state.MACAddress = attrs.HardwareAddr.String()
stateChanged = true
changeReasons = append(changeReasons, IfStateMACAddressChanged)
}

// Update IP addresses
if ipChanged, err := im.updateInterfaceStateAddresses(nl); err != nil {
im.logger.Error().Err(err).Msg("failed to update IP addresses")
} else if ipChanged {
stateChanged = true
changeReasons = append(changeReasons, IfStateIPAddressesChanged)
}

im.state.LastUpdated = time.Now()
im.stateMu.Unlock()

// Notify callback if state changed
if stateChanged && im.onStateChange != nil {
im.logger.Debug().Interface("state", im.state).Msg("notifying state change")
im.logger.Debug().
Stringer("changeReasons", changeReasons).
Interface("state", im.state).
Msg("notifying state change")
im.onStateChange(*im.state)
}

Expand All @@ -80,6 +125,7 @@ func (im *InterfaceManager) updateInterfaceStateAddresses(nl *link.Link) (bool,
ipv6Gateway string
ipv4Ready, ipv6Ready = false, false
stateChanged = false
stateChangeReason string
)

routes, _ := mgr.ListDefaultRoutes(link.AfInet6)
Expand Down Expand Up @@ -123,40 +169,55 @@ func (im *InterfaceManager) updateInterfaceStateAddresses(nl *link.Link) (bool,
if !sortAndCompareStringSlices(im.state.IPv4Addresses, ipv4Addresses) {
im.state.IPv4Addresses = ipv4Addresses
stateChanged = true
stateChangeReason = "IPv4 addresses changed"
}

if !sortAndCompareIPv6AddressSlices(im.state.IPv6Addresses, ipv6Addresses) {
im.state.IPv6Addresses = ipv6Addresses
stateChanged = true
stateChangeReason = "IPv6 addresses changed"
}

if im.state.IPv4Address != ipv4Addr {
im.state.IPv4Address = ipv4Addr
stateChanged = true
stateChangeReason = "IPv4 address changed"
}

if im.state.IPv6Address != ipv6Addr {
im.state.IPv6Address = ipv6Addr
stateChanged = true
stateChangeReason = "IPv6 address changed"
}
if im.state.IPv6LinkLocal != ipv6LinkLocal {
im.state.IPv6LinkLocal = ipv6LinkLocal
stateChanged = true
stateChangeReason = "IPv6 link local address changed"
}

if im.state.IPv6Gateway != ipv6Gateway {
im.state.IPv6Gateway = ipv6Gateway
stateChanged = true
stateChangeReason = "IPv6 gateway changed"
}

if im.state.IPv4Ready != ipv4Ready {
im.state.IPv4Ready = ipv4Ready
stateChanged = true
stateChangeReason = "IPv4 ready state changed"
}

if im.state.IPv6Ready != ipv6Ready {
im.state.IPv6Ready = ipv6Ready
stateChanged = true
stateChangeReason = "IPv6 ready state changed"
}
Comment on lines 123 to +214
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When multiple state changes occur in the same update cycle, only the last change reason will be preserved and logged. For example, if both IPv4 and IPv6 addresses change, only "IPv6 addresses changed" will be logged. Consider accumulating all change reasons (e.g., using a slice) or logging each change individually to aid in debugging.

Copilot uses AI. Check for mistakes.

if stateChanged {
im.logger.Trace().
Str("changeReason", stateChangeReason).
Interface("state", im.state).
Msg("interface state changed")
}

return stateChanged, nil
Expand Down
14 changes: 8 additions & 6 deletions pkg/nmlite/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,19 @@ func sortAndCompareStringSlices(a, b []string) bool {
return true
}

func sortIPv6AddressSlicesStable(a []types.IPv6Address) {
sort.SliceStable(a, func(i, j int) bool {
return a[i].Address.String() < a[j].Address.String()
})
}

func sortAndCompareIPv6AddressSlices(a, b []types.IPv6Address) bool {
if len(a) != len(b) {
return false
}

sort.SliceStable(a, func(i, j int) bool {
return a[i].Address.String() < b[j].Address.String()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... so should have been
return a[i].Address.String() < a[j].Address.String()
(that b should have been a)... oops

})
sort.SliceStable(b, func(i, j int) bool {
return b[i].Address.String() < a[j].Address.String()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... so should have been
return a[i].Address.String() < a[j].Address.String()
(that a should have been b)... oops

})
sortIPv6AddressSlicesStable(a)
sortIPv6AddressSlicesStable(b)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I didn't notice this error :(


for i := range a {
if a[i].Address.String() != b[i].Address.String() {
Expand Down