From f452e6b4c4eb7146f2746aeb13fbf5b35108b41c Mon Sep 17 00:00:00 2001 From: Siyuan Date: Wed, 8 Oct 2025 09:47:15 +0000 Subject: [PATCH] fix: link addr not updated --- pkg/nmlite/hostname.go | 9 +- pkg/nmlite/interface.go | 21 +- pkg/nmlite/link/consts.go | 13 + pkg/nmlite/link/manager.go | 406 +++++++++++++++++++++++++++ pkg/nmlite/link/netlink.go | 545 ++----------------------------------- pkg/nmlite/link/sysctl.go | 52 ++++ pkg/nmlite/link/utils.go | 87 ++++++ pkg/nmlite/static.go | 8 +- 8 files changed, 599 insertions(+), 542 deletions(-) create mode 100644 pkg/nmlite/link/consts.go create mode 100644 pkg/nmlite/link/manager.go create mode 100644 pkg/nmlite/link/sysctl.go create mode 100644 pkg/nmlite/link/utils.go diff --git a/pkg/nmlite/hostname.go b/pkg/nmlite/hostname.go index 6bf06991..812fa533 100644 --- a/pkg/nmlite/hostname.go +++ b/pkg/nmlite/hostname.go @@ -18,13 +18,10 @@ const ( hostsPath = "/etc/hosts" ) -var ( - hostnameLock sync.Mutex -) - // HostnameManager manages system hostname and /etc/hosts type HostnameManager struct { logger *zerolog.Logger + mu sync.Mutex } // NewHostnameManager creates a new hostname manager @@ -41,8 +38,8 @@ func NewHostnameManager(logger *zerolog.Logger) *HostnameManager { // SetHostname sets the system hostname and updates /etc/hosts func (hm *HostnameManager) SetHostname(hostname, fqdn string) error { - hostnameLock.Lock() - defer hostnameLock.Unlock() + hm.mu.Lock() + defer hm.mu.Unlock() hostname = ToValidHostname(strings.TrimSpace(hostname)) fqdn = ToValidHostname(strings.TrimSpace(fqdn)) diff --git a/pkg/nmlite/interface.go b/pkg/nmlite/interface.go index 1bae6599..fb3b5304 100644 --- a/pkg/nmlite/interface.go +++ b/pkg/nmlite/interface.go @@ -111,7 +111,7 @@ func (im *InterfaceManager) Start() error { go im.monitorInterfaceState() nl := getNetlinkManager() - nl.AddLinkStateCallback(im.ifaceName, link.LinkStateCallback{ + nl.AddStateChangeCallback(im.ifaceName, link.StateChangeCallback{ Async: true, Func: func(link *link.Link) { im.handleLinkStateChange(link) @@ -160,6 +160,7 @@ func (im *InterfaceManager) IsUp() bool { return im.state.Up } +// IsOnline returns true if the interface is online func (im *InterfaceManager) IsOnline() bool { im.stateMu.RLock() defer im.stateMu.RUnlock() @@ -167,6 +168,7 @@ func (im *InterfaceManager) IsOnline() bool { return im.state.Online } +// IPv4Ready returns true if the interface has an IPv4 address func (im *InterfaceManager) IPv4Ready() bool { im.stateMu.RLock() defer im.stateMu.RUnlock() @@ -174,6 +176,7 @@ func (im *InterfaceManager) IPv4Ready() bool { return im.state.IPv4Ready } +// IPv6Ready returns true if the interface has an IPv6 address func (im *InterfaceManager) IPv6Ready() bool { im.stateMu.RLock() defer im.stateMu.RUnlock() @@ -181,6 +184,7 @@ func (im *InterfaceManager) IPv6Ready() bool { return im.state.IPv6Ready } +// GetIPv4Addresses returns the IPv4 addresses of the interface func (im *InterfaceManager) GetIPv4Addresses() []string { im.stateMu.RLock() defer im.stateMu.RUnlock() @@ -188,6 +192,7 @@ func (im *InterfaceManager) GetIPv4Addresses() []string { return im.state.IPv4Addresses } +// GetIPv4Address returns the IPv4 address of the interface func (im *InterfaceManager) GetIPv4Address() string { im.stateMu.RLock() defer im.stateMu.RUnlock() @@ -195,6 +200,7 @@ func (im *InterfaceManager) GetIPv4Address() string { return im.state.IPv4Address } +// GetIPv6Address returns the IPv6 address of the interface func (im *InterfaceManager) GetIPv6Address() string { im.stateMu.RLock() defer im.stateMu.RUnlock() @@ -202,6 +208,7 @@ func (im *InterfaceManager) GetIPv6Address() string { return im.state.IPv6Address } +// GetIPv6Addresses returns the IPv6 addresses of the interface func (im *InterfaceManager) GetIPv6Addresses() []string { im.stateMu.RLock() defer im.stateMu.RUnlock() @@ -214,6 +221,7 @@ func (im *InterfaceManager) GetIPv6Addresses() []string { return []string{} } +// GetMACAddress returns the MAC address of the interface func (im *InterfaceManager) GetMACAddress() string { return im.state.MACAddress } @@ -230,7 +238,11 @@ func (im *InterfaceManager) GetState() *types.InterfaceState { return &state } +// NTPServers returns the NTP servers of the interface func (im *InterfaceManager) NTPServers() []net.IP { + im.stateMu.RLock() + defer im.stateMu.RUnlock() + return im.state.NTPServers } @@ -241,6 +253,7 @@ func (im *InterfaceManager) GetConfig() *types.NetworkConfig { return &config } +// ApplyConfiguration applies the current configuration to the interface func (im *InterfaceManager) ApplyConfiguration() error { return im.applyConfiguration() } @@ -641,6 +654,10 @@ func (im *InterfaceManager) updateInterfaceState() error { // updateIPAddresses updates the IP addresses in the state func (im *InterfaceManager) updateIPAddresses(nl *link.Link) error { + if err := nl.Refresh(); err != nil { + return fmt.Errorf("failed to refresh link: %w", err) + } + addrs, err := nl.AddrList(link.AfUnspec) if err != nil { return fmt.Errorf("failed to get addresses: %w", err) @@ -713,7 +730,7 @@ func (im *InterfaceManager) ReconcileLinkAddrs(addrs []*types.IPAddress) error { if link == nil { return fmt.Errorf("failed to get interface: %w", err) } - return nl.ReconcileLinkAddrs(link, addrs) + return nl.ReconcileLink(link, addrs) } // applyDHCPLease applies DHCP lease configuration using ReconcileLinkAddrs diff --git a/pkg/nmlite/link/consts.go b/pkg/nmlite/link/consts.go new file mode 100644 index 00000000..c226583b --- /dev/null +++ b/pkg/nmlite/link/consts.go @@ -0,0 +1,13 @@ +package link + +const ( + // AfUnspec is the unspecified address family constant + AfUnspec = 0 + // AfInet is the IPv4 address family constant + AfInet = 2 + // AfInet6 is the IPv6 address family constant + AfInet6 = 10 + + sysctlBase = "/proc/sys" + sysctlFileMode = 0640 +) diff --git a/pkg/nmlite/link/manager.go b/pkg/nmlite/link/manager.go new file mode 100644 index 00000000..a3333999 --- /dev/null +++ b/pkg/nmlite/link/manager.go @@ -0,0 +1,406 @@ +package link + +import ( + "context" + "fmt" + "net" + "time" + + "github.com/jetkvm/kvm/internal/sync" + + "github.com/jetkvm/kvm/internal/network/types" + "github.com/rs/zerolog" + "github.com/vishvananda/netlink" +) + +// StateChangeHandler is the function type for link state callbacks +type StateChangeHandler func(link *Link) + +// StateChangeCallback is the struct for link state callbacks +type StateChangeCallback struct { + Async bool + Func StateChangeHandler +} + +// NetlinkManager provides centralized netlink operations +type NetlinkManager struct { + logger *zerolog.Logger + mu sync.RWMutex + stateChangeCallbacks map[string][]StateChangeCallback +} + +func newNetlinkManager(logger *zerolog.Logger) *NetlinkManager { + if logger == nil { + logger = &zerolog.Logger{} // Default no-op logger + } + n := &NetlinkManager{ + logger: logger, + stateChangeCallbacks: make(map[string][]StateChangeCallback), + } + n.monitorStateChange() + return n +} + +// GetNetlinkManager returns the singleton NetlinkManager instance +func GetNetlinkManager() *NetlinkManager { + netlinkManagerOnce.Do(func() { + netlinkManagerInstance = newNetlinkManager(nil) + }) + return netlinkManagerInstance +} + +// InitializeNetlinkManager initializes the singleton NetlinkManager with a logger +func InitializeNetlinkManager(logger *zerolog.Logger) *NetlinkManager { + netlinkManagerOnce.Do(func() { + netlinkManagerInstance = newNetlinkManager(logger) + }) + return netlinkManagerInstance +} + +// AddStateChangeCallback adds a callback for link state changes +func (nm *NetlinkManager) AddStateChangeCallback(ifname string, callback StateChangeCallback) { + nm.mu.Lock() + defer nm.mu.Unlock() + + if _, ok := nm.stateChangeCallbacks[ifname]; !ok { + nm.stateChangeCallbacks[ifname] = make([]StateChangeCallback, 0) + } + + nm.stateChangeCallbacks[ifname] = append(nm.stateChangeCallbacks[ifname], callback) +} + +// Interface operations +func (nm *NetlinkManager) monitorStateChange() { + updateCh := make(chan netlink.LinkUpdate) + // we don't need to stop the subscription, as it will be closed when the program exits + stopCh := make(chan struct{}) //nolint:unused + netlink.LinkSubscribe(updateCh, stopCh) + + nm.logger.Info().Msg("state change monitoring started") + + go func() { + for update := range updateCh { + nm.runCallbacks(update) + } + }() +} + +func (nm *NetlinkManager) runCallbacks(update netlink.LinkUpdate) { + nm.mu.RLock() + defer nm.mu.RUnlock() + + ifname := update.Link.Attrs().Name + callbacks, ok := nm.stateChangeCallbacks[ifname] + + l := nm.logger.With().Str("interface", ifname).Logger() + if !ok { + l.Trace().Msg("no state change callbacks for interface") + return + } + + for _, callback := range callbacks { + l.Trace(). + Interface("callback", callback). + Bool("async", callback.Async). + Msg("calling callback") + + if callback.Async { + go callback.Func(&Link{Link: update.Link}) + } else { + callback.Func(&Link{Link: update.Link}) + } + } +} + +// GetLinkByName gets a network link by name +func (nm *NetlinkManager) GetLinkByName(name string) (*Link, error) { + nm.mu.RLock() + defer nm.mu.RUnlock() + link, err := netlink.LinkByName(name) + if err != nil { + return nil, err + } + return &Link{Link: link}, nil +} + +// LinkSetUp brings a network interface up +func (nm *NetlinkManager) LinkSetUp(link *Link) error { + nm.mu.RLock() + defer nm.mu.RUnlock() + return netlink.LinkSetUp(link) +} + +// LinkSetDown brings a network interface down +func (nm *NetlinkManager) LinkSetDown(link *Link) error { + nm.mu.RLock() + defer nm.mu.RUnlock() + return netlink.LinkSetDown(link) +} + +// EnsureInterfaceUp ensures the interface is up +func (nm *NetlinkManager) EnsureInterfaceUp(link *Link) error { + if link.Attrs().OperState == netlink.OperUp { + return nil + } + return nm.LinkSetUp(link) +} + +// EnsureInterfaceUpWithTimeout ensures the interface is up with timeout and retry logic +func (nm *NetlinkManager) EnsureInterfaceUpWithTimeout(ctx context.Context, iface *Link, timeout time.Duration) (*Link, error) { + ifname := iface.Attrs().Name + + l := nm.logger.With().Str("interface", ifname).Logger() + + linkUpTimeout := time.After(timeout) + + attempt := 0 + start := time.Now() + + for { + link, err := nm.GetLinkByName(ifname) + if err != nil { + return nil, err + } + + state := link.Attrs().OperState + if state == netlink.OperUp || state == netlink.OperUnknown { + return link, nil + } + + l.Info().Str("state", state.String()).Msg("bringing up interface") + + if err = nm.LinkSetUp(link); err != nil { + l.Error().Err(err).Msg("interface can't make it up") + } + + l = l.With().Int("attempt", attempt).Dur("duration", time.Since(start)).Logger() + + if attempt > 0 { + l.Info().Msg("interface up") + } + + select { + case <-time.After(500 * time.Millisecond): + attempt++ + continue + case <-ctx.Done(): + if err != nil { + return nil, err + } + return nil, ErrInterfaceUpCanceled + case <-linkUpTimeout: + attempt++ + l.Error().Msg("interface is still down after timeout") + if err != nil { + return nil, err + } + return nil, ErrInterfaceUpTimeout + } + } +} + +// Address operations + +// AddrList gets all addresses for a link +func (nm *NetlinkManager) AddrList(link *Link, family int) ([]netlink.Addr, error) { + nm.mu.RLock() + defer nm.mu.RUnlock() + return netlink.AddrList(link, family) +} + +// AddrAdd adds an address to a link +func (nm *NetlinkManager) AddrAdd(link *Link, addr *netlink.Addr) error { + nm.mu.RLock() + defer nm.mu.RUnlock() + return netlink.AddrAdd(link, addr) +} + +// AddrDel removes an address from a link +func (nm *NetlinkManager) AddrDel(link *Link, addr *netlink.Addr) error { + nm.mu.RLock() + defer nm.mu.RUnlock() + return netlink.AddrDel(link, addr) +} + +// RemoveAllAddresses removes all addresses of a specific family from a link +func (nm *NetlinkManager) RemoveAllAddresses(link *Link, family int) error { + addrs, err := nm.AddrList(link, family) + if err != nil { + return fmt.Errorf("failed to get addresses: %w", err) + } + + for _, addr := range addrs { + if err := nm.AddrDel(link, &addr); err != nil { + nm.logger.Warn().Err(err).Str("address", addr.IP.String()).Msg("failed to remove address") + } + } + + return nil +} + +// RemoveNonLinkLocalIPv6Addresses removes all non-link-local IPv6 addresses +func (nm *NetlinkManager) RemoveNonLinkLocalIPv6Addresses(link *Link) error { + addrs, err := nm.AddrList(link, AfInet6) + if err != nil { + return fmt.Errorf("failed to get IPv6 addresses: %w", err) + } + + for _, addr := range addrs { + if !addr.IP.IsLinkLocalUnicast() { + if err := nm.AddrDel(link, &addr); err != nil { + nm.logger.Warn().Err(err).Str("address", addr.IP.String()).Msg("failed to remove IPv6 address") + } + } + } + + return nil +} + +// RouteList gets all routes +func (nm *NetlinkManager) RouteList(link *Link, family int) ([]netlink.Route, error) { + nm.mu.RLock() + defer nm.mu.RUnlock() + return netlink.RouteList(link, family) +} + +// RouteAdd adds a route +func (nm *NetlinkManager) RouteAdd(route *netlink.Route) error { + nm.mu.RLock() + defer nm.mu.RUnlock() + return netlink.RouteAdd(route) +} + +// RouteDel removes a route +func (nm *NetlinkManager) RouteDel(route *netlink.Route) error { + nm.mu.RLock() + defer nm.mu.RUnlock() + return netlink.RouteDel(route) +} + +// RouteReplace replaces a route +func (nm *NetlinkManager) RouteReplace(route *netlink.Route) error { + nm.mu.RLock() + defer nm.mu.RUnlock() + return netlink.RouteReplace(route) +} + +// HasDefaultRoute checks if a default route exists for the given family +func (nm *NetlinkManager) HasDefaultRoute(family int) bool { + routes, err := netlink.RouteList(nil, family) + if err != nil { + return false + } + + for _, route := range routes { + if route.Dst == nil { + return true + } + if family == AfInet && route.Dst.IP.Equal(net.IPv4zero) && route.Dst.Mask.String() == "0.0.0.0/0" { + return true + } + if family == AfInet6 && route.Dst.IP.Equal(net.IPv6zero) && route.Dst.Mask.String() == "::/0" { + return true + } + } + + return false +} + +// AddDefaultRoute adds a default route +func (nm *NetlinkManager) AddDefaultRoute(link *Link, gateway net.IP, family int) error { + var dst *net.IPNet + switch family { + case AfInet: + dst = &ipv4DefaultRoute + case AfInet6: + dst = &ipv6DefaultRoute + default: + return fmt.Errorf("unsupported address family: %d", family) + } + + route := &netlink.Route{ + Dst: dst, + Gw: gateway, + LinkIndex: link.Attrs().Index, + } + + return nm.RouteReplace(route) +} + +// RemoveDefaultRoute removes the default route for the given family +func (nm *NetlinkManager) RemoveDefaultRoute(family int) error { + routes, err := nm.RouteList(nil, family) + if err != nil { + return fmt.Errorf("failed to get routes: %w", err) + } + + for _, route := range routes { + if route.Dst != nil { + if family == AfInet && route.Dst.IP.Equal(net.IPv4zero) && route.Dst.Mask.String() == "0.0.0.0/0" { + if err := nm.RouteDel(&route); err != nil { + nm.logger.Warn().Err(err).Msg("failed to remove IPv4 default route") + } + } + if family == AfInet6 && route.Dst.IP.Equal(net.IPv6zero) && route.Dst.Mask.String() == "::/0" { + if err := nm.RouteDel(&route); err != nil { + nm.logger.Warn().Err(err).Msg("failed to remove IPv6 default route") + } + } + } + } + + return nil +} + +// ReconcileLink reconciles the addresses and routes of a link +func (nm *NetlinkManager) ReconcileLink(link *Link, expected []*types.IPAddress) error { + expectedAddrs := make(map[string]bool) + existingAddrs := make(map[string]bool) + + for _, addr := range expected { + ipCidr := addr.Address.IP.String() + "/" + addr.Address.Mask.String() + expectedAddrs[ipCidr] = true + } + + addrs, err := nm.AddrList(link, AfUnspec) + if err != nil { + return fmt.Errorf("failed to get addresses: %w", err) + } + + for _, addr := range addrs { + ipCidr := addr.IP.String() + "/" + addr.IPNet.Mask.String() + existingAddrs[ipCidr] = true + } + + for _, addr := range expected { + family := AfUnspec + if addr.Address.IP.To4() != nil { + family = AfInet + } else if addr.Address.IP.To16() != nil { + family = AfInet6 + } + + ipCidr := addr.Address.IP.String() + "/" + addr.Address.Mask.String() + if ok := existingAddrs[ipCidr]; !ok { + ipNet := &net.IPNet{ + IP: addr.Address.IP, + Mask: addr.Address.Mask, + } + + if err := nm.AddrAdd(link, &netlink.Addr{IPNet: ipNet}); err != nil { + return fmt.Errorf("failed to add address %s: %w", ipCidr, err) + } + + nm.logger.Info().Str("address", ipCidr).Msg("added address") + } + + if addr.Gateway != nil { + nm.logger.Trace().Str("address", ipCidr).Str("gateway", addr.Gateway.String()).Msg("adding default route for address") + if err := nm.AddDefaultRoute(link, addr.Gateway, family); err != nil { + return fmt.Errorf("failed to add default route for address %s: %w", ipCidr, err) + } + } + } + + return nil +} diff --git a/pkg/nmlite/link/netlink.go b/pkg/nmlite/link/netlink.go index 8e342f6d..994935d7 100644 --- a/pkg/nmlite/link/netlink.go +++ b/pkg/nmlite/link/netlink.go @@ -2,35 +2,15 @@ package link import ( - "context" "errors" "fmt" "net" - "os" - "path" - "strconv" - "strings" - "time" "github.com/jetkvm/kvm/internal/sync" - "github.com/jetkvm/kvm/internal/network/types" - "github.com/rs/zerolog" "github.com/vishvananda/netlink" ) -const ( - // AfUnspec is the unspecified address family constant - AfUnspec = 0 - // AfInet is the IPv4 address family constant - AfInet = 2 - // AfInet6 is the IPv6 address family constant - AfInet6 = 10 - - sysctlBase = "/proc/sys" - sysctlFileMode = 0640 -) - var ( ipv4DefaultRoute = net.IPNet{ IP: net.IPv4zero, @@ -46,30 +26,30 @@ var ( netlinkManagerInstance *NetlinkManager netlinkManagerOnce sync.Once - // Error definitions - ErrInterfaceUpTimeout = errors.New("timeout after waiting for an interface to come up") + // ErrInterfaceUpTimeout is the error returned when the interface does not come up within the timeout + ErrInterfaceUpTimeout = errors.New("timeout after waiting for an interface to come up") + // ErrInterfaceUpCanceled is the error returned when the interface does not come up due to context cancellation ErrInterfaceUpCanceled = errors.New("context canceled while waiting for an interface to come up") ) -type LinkStateCallbackFunction func(link *Link) -type LinkStateCallback struct { - Async bool - Func LinkStateCallbackFunction -} - -// NetlinkManager provides centralized netlink operations -type NetlinkManager struct { - logger *zerolog.Logger - linkStateCh chan netlink.LinkUpdate - mu sync.RWMutex - linkStateCallbacks map[string][]LinkStateCallback -} - // Link is a wrapper around netlink.Link type Link struct { netlink.Link } +func (l *Link) Refresh() error { + linkName := l.Link.Attrs().Name + link, err := netlink.LinkByName(linkName) + if err != nil { + return err + } + if link == nil { + return fmt.Errorf("link not found: %s", linkName) + } + l.Link = link + return nil +} + // Attrs returns the attributes of the link func (l *Link) Attrs() *netlink.LinkAttrs { return l.Link.Attrs() @@ -100,496 +80,3 @@ func (l *Link) IsSame(other *Link) bool { } return true } - -func newNetlinkManager(logger *zerolog.Logger) *NetlinkManager { - if logger == nil { - logger = &zerolog.Logger{} // Default no-op logger - } - n := &NetlinkManager{ - logger: logger, - linkStateCallbacks: make(map[string][]LinkStateCallback), - } - n.monitorLinkState() - return n -} - -// GetNetlinkManager returns the singleton NetlinkManager instance -func GetNetlinkManager() *NetlinkManager { - netlinkManagerOnce.Do(func() { - netlinkManagerInstance = newNetlinkManager(nil) - }) - return netlinkManagerInstance -} - -// InitializeNetlinkManager initializes the singleton NetlinkManager with a logger -func InitializeNetlinkManager(logger *zerolog.Logger) *NetlinkManager { - netlinkManagerOnce.Do(func() { - netlinkManagerInstance = newNetlinkManager(logger) - }) - return netlinkManagerInstance -} - -func (nm *NetlinkManager) runCallbacks(update netlink.LinkUpdate) { - nm.mu.RLock() - defer nm.mu.RUnlock() - - ifname := update.Link.Attrs().Name - callbacks, ok := nm.linkStateCallbacks[ifname] - - l := nm.logger.With().Str("interface", ifname).Logger() - if !ok { - l.Trace().Msg("no callbacks for interface") - return - } - for _, callback := range callbacks { - l.Trace().Interface("callback", callback).Msg("calling callback") - - if callback.Async { - go callback.Func(&Link{Link: update.Link}) - } else { - callback.Func(&Link{Link: update.Link}) - } - } -} - -// AddLinkStateCallback adds a callback for link state changes -func (nm *NetlinkManager) AddLinkStateCallback(ifname string, callback LinkStateCallback) { - nm.mu.Lock() - defer nm.mu.Unlock() - nm.linkStateCallbacks[ifname] = append(nm.linkStateCallbacks[ifname], callback) -} - -// Interface operations -func (nm *NetlinkManager) monitorLinkState() { - updateCh := make(chan netlink.LinkUpdate) - // we don't need to stop the subscription, as it will be closed when the program exits - stopCh := make(chan struct{}) //nolint:unused - netlink.LinkSubscribe(updateCh, stopCh) - - nm.logger.Info().Msg("link state monitoring started") - - go func() { - for update := range updateCh { - nm.runCallbacks(update) - } - }() -} - -// GetLinkByName gets a network link by name -func (nm *NetlinkManager) GetLinkByName(name string) (*Link, error) { - nm.mu.RLock() - defer nm.mu.RUnlock() - link, err := netlink.LinkByName(name) - if err != nil { - return nil, err - } - return &Link{Link: link}, nil -} - -// LinkSetUp brings a network interface up -func (nm *NetlinkManager) LinkSetUp(link *Link) error { - nm.mu.RLock() - defer nm.mu.RUnlock() - return netlink.LinkSetUp(link) -} - -// LinkSetDown brings a network interface down -func (nm *NetlinkManager) LinkSetDown(link *Link) error { - nm.mu.RLock() - defer nm.mu.RUnlock() - return netlink.LinkSetDown(link) -} - -// EnsureInterfaceUp ensures the interface is up -func (nm *NetlinkManager) EnsureInterfaceUp(link *Link) error { - if link.Attrs().OperState == netlink.OperUp { - return nil - } - return nm.LinkSetUp(link) -} - -// EnsureInterfaceUpWithTimeout ensures the interface is up with timeout and retry logic -func (nm *NetlinkManager) EnsureInterfaceUpWithTimeout(ctx context.Context, iface *Link, timeout time.Duration) (*Link, error) { - ifname := iface.Attrs().Name - - l := nm.logger.With().Str("interface", ifname).Logger() - - linkUpTimeout := time.After(timeout) - - attempt := 0 - start := time.Now() - - for { - link, err := nm.GetLinkByName(ifname) - if err != nil { - return nil, err - } - - state := link.Attrs().OperState - if state == netlink.OperUp || state == netlink.OperUnknown { - return link, nil - } - - l.Info().Str("state", state.String()).Msg("bringing up interface") - - if err = nm.LinkSetUp(link); err != nil { - l.Error().Err(err).Msg("interface can't make it up") - } - - l = l.With().Int("attempt", attempt).Dur("duration", time.Since(start)).Logger() - - if attempt > 0 { - l.Info().Msg("interface up") - } - - select { - case <-time.After(500 * time.Millisecond): - attempt++ - continue - case <-ctx.Done(): - if err != nil { - return nil, err - } - return nil, ErrInterfaceUpCanceled - case <-linkUpTimeout: - attempt++ - l.Error().Msg("interface is still down after timeout") - if err != nil { - return nil, err - } - return nil, ErrInterfaceUpTimeout - } - } -} - -// Address operations - -// AddrList gets all addresses for a link -func (nm *NetlinkManager) AddrList(link *Link, family int) ([]netlink.Addr, error) { - nm.mu.RLock() - defer nm.mu.RUnlock() - return netlink.AddrList(link, family) -} - -// AddrAdd adds an address to a link -func (nm *NetlinkManager) AddrAdd(link *Link, addr *netlink.Addr) error { - nm.mu.RLock() - defer nm.mu.RUnlock() - return netlink.AddrAdd(link, addr) -} - -// AddrDel removes an address from a link -func (nm *NetlinkManager) AddrDel(link *Link, addr *netlink.Addr) error { - nm.mu.RLock() - defer nm.mu.RUnlock() - return netlink.AddrDel(link, addr) -} - -// RemoveAllAddresses removes all addresses of a specific family from a link -func (nm *NetlinkManager) RemoveAllAddresses(link *Link, family int) error { - addrs, err := nm.AddrList(link, family) - if err != nil { - return fmt.Errorf("failed to get addresses: %w", err) - } - - for _, addr := range addrs { - if err := nm.AddrDel(link, &addr); err != nil { - nm.logger.Warn().Err(err).Str("address", addr.IP.String()).Msg("failed to remove address") - } - } - - return nil -} - -// RemoveNonLinkLocalIPv6Addresses removes all non-link-local IPv6 addresses -func (nm *NetlinkManager) RemoveNonLinkLocalIPv6Addresses(link *Link) error { - addrs, err := nm.AddrList(link, AfInet6) - if err != nil { - return fmt.Errorf("failed to get IPv6 addresses: %w", err) - } - - for _, addr := range addrs { - if !addr.IP.IsLinkLocalUnicast() { - if err := nm.AddrDel(link, &addr); err != nil { - nm.logger.Warn().Err(err).Str("address", addr.IP.String()).Msg("failed to remove IPv6 address") - } - } - } - - return nil -} - -// RouteList gets all routes -func (nm *NetlinkManager) RouteList(link *Link, family int) ([]netlink.Route, error) { - nm.mu.RLock() - defer nm.mu.RUnlock() - return netlink.RouteList(link, family) -} - -// RouteAdd adds a route -func (nm *NetlinkManager) RouteAdd(route *netlink.Route) error { - nm.mu.RLock() - defer nm.mu.RUnlock() - return netlink.RouteAdd(route) -} - -// RouteDel removes a route -func (nm *NetlinkManager) RouteDel(route *netlink.Route) error { - nm.mu.RLock() - defer nm.mu.RUnlock() - return netlink.RouteDel(route) -} - -// RouteReplace replaces a route -func (nm *NetlinkManager) RouteReplace(route *netlink.Route) error { - nm.mu.RLock() - defer nm.mu.RUnlock() - return netlink.RouteReplace(route) -} - -// HasDefaultRoute checks if a default route exists for the given family -func (nm *NetlinkManager) HasDefaultRoute(family int) bool { - routes, err := netlink.RouteList(nil, family) - if err != nil { - return false - } - - for _, route := range routes { - if route.Dst == nil { - return true - } - if family == AfInet && route.Dst.IP.Equal(net.IPv4zero) && route.Dst.Mask.String() == "0.0.0.0/0" { - return true - } - if family == AfInet6 && route.Dst.IP.Equal(net.IPv6zero) && route.Dst.Mask.String() == "::/0" { - return true - } - } - - return false -} - -// AddDefaultRoute adds a default route -func (nm *NetlinkManager) AddDefaultRoute(link *Link, gateway net.IP, family int) error { - var dst *net.IPNet - if family == AfInet { - dst = &ipv4DefaultRoute - } else if family == AfInet6 { - dst = &ipv6DefaultRoute - } else { - return fmt.Errorf("unsupported address family: %d", family) - } - - route := &netlink.Route{ - Dst: dst, - Gw: gateway, - LinkIndex: link.Attrs().Index, - } - - return nm.RouteReplace(route) -} - -// RemoveDefaultRoute removes the default route for the given family -func (nm *NetlinkManager) RemoveDefaultRoute(family int) error { - routes, err := nm.RouteList(nil, family) - if err != nil { - return fmt.Errorf("failed to get routes: %w", err) - } - - for _, route := range routes { - if route.Dst != nil { - if family == AfInet && route.Dst.IP.Equal(net.IPv4zero) && route.Dst.Mask.String() == "0.0.0.0/0" { - if err := nm.RouteDel(&route); err != nil { - nm.logger.Warn().Err(err).Msg("failed to remove IPv4 default route") - } - } - if family == AfInet6 && route.Dst.IP.Equal(net.IPv6zero) && route.Dst.Mask.String() == "::/0" { - if err := nm.RouteDel(&route); err != nil { - nm.logger.Warn().Err(err).Msg("failed to remove IPv6 default route") - } - } - } - } - - return nil -} - -func (nm *NetlinkManager) ReconcileLinkAddrs(link *Link, expected []*types.IPAddress) error { - expectedAddrs := make(map[string]bool) - existingAddrs := make(map[string]bool) - - for _, addr := range expected { - ipCidr := addr.Address.IP.String() + "/" + addr.Address.Mask.String() - expectedAddrs[ipCidr] = true - } - - addrs, err := nm.AddrList(link, AfUnspec) - if err != nil { - return fmt.Errorf("failed to get addresses: %w", err) - } - - for _, addr := range addrs { - ipCidr := addr.IP.String() + "/" + addr.IPNet.Mask.String() - existingAddrs[ipCidr] = true - } - - for _, addr := range expected { - family := AfUnspec - if addr.Address.IP.To4() != nil { - family = AfInet - } else if addr.Address.IP.To16() != nil { - family = AfInet6 - } - - ipCidr := addr.Address.IP.String() + "/" + addr.Address.Mask.String() - if ok := existingAddrs[ipCidr]; !ok { - ipNet := &net.IPNet{ - IP: addr.Address.IP, - Mask: addr.Address.Mask, - } - - if err := nm.AddrAdd(link, &netlink.Addr{IPNet: ipNet}); err != nil { - return fmt.Errorf("failed to add address %s: %w", ipCidr, err) - } - - nm.logger.Info().Str("address", ipCidr).Msg("added address") - } - - if addr.Gateway != nil { - nm.logger.Trace().Str("address", ipCidr).Str("gateway", addr.Gateway.String()).Msg("adding default route for address") - if err := nm.AddDefaultRoute(link, addr.Gateway, family); err != nil { - return fmt.Errorf("failed to add default route for address %s: %w", ipCidr, err) - } - } - } - - return nil -} - -// Sysctl operations - -// SetSysctlValues sets sysctl values for the interface -func (nm *NetlinkManager) SetSysctlValues(ifaceName string, values map[string]int) error { - for name, value := range values { - name = fmt.Sprintf(name, ifaceName) - name = strings.ReplaceAll(name, ".", "/") - - if err := os.WriteFile(path.Join(sysctlBase, name), []byte(strconv.Itoa(value)), sysctlFileMode); err != nil { - return fmt.Errorf("failed to set sysctl %s=%d: %w", name, value, err) - } - } - return nil -} - -// EnableIPv6 enables IPv6 on the interface -func (nm *NetlinkManager) EnableIPv6(ifaceName string) error { - return nm.SetSysctlValues(ifaceName, map[string]int{ - "net.ipv6.conf.%s.disable_ipv6": 0, - "net.ipv6.conf.%s.accept_ra": 2, - }) -} - -// DisableIPv6 disables IPv6 on the interface -func (nm *NetlinkManager) DisableIPv6(ifaceName string) error { - return nm.SetSysctlValues(ifaceName, map[string]int{ - "net.ipv6.conf.%s.disable_ipv6": 1, - }) -} - -// EnableIPv6SLAAC enables IPv6 SLAAC on the interface -func (nm *NetlinkManager) EnableIPv6SLAAC(ifaceName string) error { - return nm.SetSysctlValues(ifaceName, map[string]int{ - "net.ipv6.conf.%s.disable_ipv6": 0, - "net.ipv6.conf.%s.accept_ra": 2, - }) -} - -// EnableIPv6LinkLocal enables IPv6 link-local only on the interface -func (nm *NetlinkManager) EnableIPv6LinkLocal(ifaceName string) error { - return nm.SetSysctlValues(ifaceName, map[string]int{ - "net.ipv6.conf.%s.disable_ipv6": 0, - "net.ipv6.conf.%s.accept_ra": 0, - }) -} - -// Utility functions - -// ParseIPv4Netmask parses an IPv4 netmask string and returns the IPNet -func (nm *NetlinkManager) ParseIPv4Netmask(address, netmask string) (*net.IPNet, error) { - if strings.Contains(address, "/") { - _, ipNet, err := net.ParseCIDR(address) - if err != nil { - return nil, fmt.Errorf("invalid IPv4 address: %s", address) - } - return ipNet, nil - } - - ip := net.ParseIP(address) - if ip == nil { - return nil, fmt.Errorf("invalid IPv4 address: %s", address) - } - if ip.To4() == nil { - return nil, fmt.Errorf("not an IPv4 address: %s", address) - } - - mask := net.ParseIP(netmask) - if mask == nil { - return nil, fmt.Errorf("invalid IPv4 netmask: %s", netmask) - } - if mask.To4() == nil { - return nil, fmt.Errorf("not an IPv4 netmask: %s", netmask) - } - - return &net.IPNet{ - IP: ip, - Mask: net.IPv4Mask(mask[12], mask[13], mask[14], mask[15]), - }, nil -} - -// ParseIPv6Prefix parses an IPv6 address and prefix length -func (nm *NetlinkManager) ParseIPv6Prefix(address string, prefixLength int) (*net.IPNet, error) { - if strings.Contains(address, "/") { - _, ipNet, err := net.ParseCIDR(address) - if err != nil { - return nil, fmt.Errorf("invalid IPv6 address: %s", address) - } - return ipNet, nil - } - - ip := net.ParseIP(address) - if ip == nil { - return nil, fmt.Errorf("invalid IPv6 address: %s", address) - } - if ip.To16() == nil || ip.To4() != nil { - return nil, fmt.Errorf("not an IPv6 address: %s", address) - } - - if prefixLength < 0 || prefixLength > 128 { - return nil, fmt.Errorf("invalid IPv6 prefix length: %d (must be 0-128)", prefixLength) - } - - return &net.IPNet{ - IP: ip, - Mask: net.CIDRMask(prefixLength, 128), - }, nil -} - -// ValidateIPAddress validates an IP address -func (nm *NetlinkManager) ValidateIPAddress(address string, isIPv6 bool) error { - ip := net.ParseIP(address) - if ip == nil { - return fmt.Errorf("invalid IP address: %s", address) - } - - if isIPv6 { - if ip.To16() == nil || ip.To4() != nil { - return fmt.Errorf("not an IPv6 address: %s", address) - } - } else { - if ip.To4() == nil { - return fmt.Errorf("not an IPv4 address: %s", address) - } - } - - return nil -} diff --git a/pkg/nmlite/link/sysctl.go b/pkg/nmlite/link/sysctl.go new file mode 100644 index 00000000..33a0a62a --- /dev/null +++ b/pkg/nmlite/link/sysctl.go @@ -0,0 +1,52 @@ +package link + +import ( + "fmt" + "os" + "path" + "strconv" + "strings" +) + +func (nm *NetlinkManager) setSysctlValues(ifaceName string, values map[string]int) error { + for name, value := range values { + name = fmt.Sprintf(name, ifaceName) + name = strings.ReplaceAll(name, ".", "/") + + if err := os.WriteFile(path.Join(sysctlBase, name), []byte(strconv.Itoa(value)), sysctlFileMode); err != nil { + return fmt.Errorf("failed to set sysctl %s=%d: %w", name, value, err) + } + } + return nil +} + +// EnableIPv6 enables IPv6 on the interface +func (nm *NetlinkManager) EnableIPv6(ifaceName string) error { + return nm.setSysctlValues(ifaceName, map[string]int{ + "net.ipv6.conf.%s.disable_ipv6": 0, + "net.ipv6.conf.%s.accept_ra": 2, + }) +} + +// DisableIPv6 disables IPv6 on the interface +func (nm *NetlinkManager) DisableIPv6(ifaceName string) error { + return nm.setSysctlValues(ifaceName, map[string]int{ + "net.ipv6.conf.%s.disable_ipv6": 1, + }) +} + +// EnableIPv6SLAAC enables IPv6 SLAAC on the interface +func (nm *NetlinkManager) EnableIPv6SLAAC(ifaceName string) error { + return nm.setSysctlValues(ifaceName, map[string]int{ + "net.ipv6.conf.%s.disable_ipv6": 0, + "net.ipv6.conf.%s.accept_ra": 2, + }) +} + +// EnableIPv6LinkLocal enables IPv6 link-local only on the interface +func (nm *NetlinkManager) EnableIPv6LinkLocal(ifaceName string) error { + return nm.setSysctlValues(ifaceName, map[string]int{ + "net.ipv6.conf.%s.disable_ipv6": 0, + "net.ipv6.conf.%s.accept_ra": 0, + }) +} diff --git a/pkg/nmlite/link/utils.go b/pkg/nmlite/link/utils.go new file mode 100644 index 00000000..ba911b86 --- /dev/null +++ b/pkg/nmlite/link/utils.go @@ -0,0 +1,87 @@ +package link + +import ( + "fmt" + "net" + "strings" +) + +// ParseIPv4Netmask parses an IPv4 netmask string and returns the IPNet +func ParseIPv4Netmask(address, netmask string) (*net.IPNet, error) { + if strings.Contains(address, "/") { + _, ipNet, err := net.ParseCIDR(address) + if err != nil { + return nil, fmt.Errorf("invalid IPv4 address: %s", address) + } + return ipNet, nil + } + + ip := net.ParseIP(address) + if ip == nil { + return nil, fmt.Errorf("invalid IPv4 address: %s", address) + } + if ip.To4() == nil { + return nil, fmt.Errorf("not an IPv4 address: %s", address) + } + + mask := net.ParseIP(netmask) + if mask == nil { + return nil, fmt.Errorf("invalid IPv4 netmask: %s", netmask) + } + if mask.To4() == nil { + return nil, fmt.Errorf("not an IPv4 netmask: %s", netmask) + } + + return &net.IPNet{ + IP: ip, + Mask: net.IPv4Mask(mask[12], mask[13], mask[14], mask[15]), + }, nil +} + +// ParseIPv6Prefix parses an IPv6 address and prefix length +func ParseIPv6Prefix(address string, prefixLength int) (*net.IPNet, error) { + if strings.Contains(address, "/") { + _, ipNet, err := net.ParseCIDR(address) + if err != nil { + return nil, fmt.Errorf("invalid IPv6 address: %s", address) + } + return ipNet, nil + } + + ip := net.ParseIP(address) + if ip == nil { + return nil, fmt.Errorf("invalid IPv6 address: %s", address) + } + if ip.To16() == nil || ip.To4() != nil { + return nil, fmt.Errorf("not an IPv6 address: %s", address) + } + + if prefixLength < 0 || prefixLength > 128 { + return nil, fmt.Errorf("invalid IPv6 prefix length: %d (must be 0-128)", prefixLength) + } + + return &net.IPNet{ + IP: ip, + Mask: net.CIDRMask(prefixLength, 128), + }, nil +} + +// ValidateIPAddress validates an IP address +func ValidateIPAddress(address string, isIPv6 bool) error { + ip := net.ParseIP(address) + if ip == nil { + return fmt.Errorf("invalid IP address: %s", address) + } + + if isIPv6 { + if ip.To16() == nil || ip.To4() != nil { + return fmt.Errorf("not an IPv6 address: %s", address) + } + } else { + if ip.To4() == nil { + return fmt.Errorf("not an IPv4 address: %s", address) + } + } + + return nil +} diff --git a/pkg/nmlite/static.go b/pkg/nmlite/static.go index 1bf24c47..292c8b59 100644 --- a/pkg/nmlite/static.go +++ b/pkg/nmlite/static.go @@ -176,8 +176,7 @@ func (scm *StaticConfigManager) parseIPv4Config(config *types.IPv4StaticConfig) } // Parse IP address and netmask - netlinkMgr := getNetlinkManager() - ipNet, err := netlinkMgr.ParseIPv4Netmask(config.Address.String, config.Netmask.String) + ipNet, err := link.ParseIPv4Netmask(config.Address.String, config.Netmask.String) if err != nil { return nil, err } @@ -192,7 +191,7 @@ func (scm *StaticConfigManager) parseIPv4Config(config *types.IPv4StaticConfig) // Parse DNS servers var dns []net.IP for _, dnsStr := range config.DNS { - if err := netlinkMgr.ValidateIPAddress(dnsStr, false); err != nil { + if err := link.ValidateIPAddress(dnsStr, false); err != nil { return nil, fmt.Errorf("invalid DNS server: %w", err) } dns = append(dns, net.ParseIP(dnsStr)) @@ -212,8 +211,7 @@ func (scm *StaticConfigManager) parseIPv6Config(config *types.IPv6StaticConfig) } // Parse IP address and prefix - netlinkMgr := getNetlinkManager() - ipNet, err := netlinkMgr.ParseIPv6Prefix(config.Prefix.String, 64) // Default to /64 if not specified + ipNet, err := link.ParseIPv6Prefix(config.Prefix.String, 64) // Default to /64 if not specified if err != nil { return nil, err }