mirror of https://github.com/jetkvm/kvm.git
fix: link addr not updated
This commit is contained in:
parent
d6ebbf4d09
commit
f452e6b4c4
|
|
@ -18,13 +18,10 @@ const (
|
||||||
hostsPath = "/etc/hosts"
|
hostsPath = "/etc/hosts"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
hostnameLock sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
// HostnameManager manages system hostname and /etc/hosts
|
// HostnameManager manages system hostname and /etc/hosts
|
||||||
type HostnameManager struct {
|
type HostnameManager struct {
|
||||||
logger *zerolog.Logger
|
logger *zerolog.Logger
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHostnameManager creates a new hostname manager
|
// 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
|
// SetHostname sets the system hostname and updates /etc/hosts
|
||||||
func (hm *HostnameManager) SetHostname(hostname, fqdn string) error {
|
func (hm *HostnameManager) SetHostname(hostname, fqdn string) error {
|
||||||
hostnameLock.Lock()
|
hm.mu.Lock()
|
||||||
defer hostnameLock.Unlock()
|
defer hm.mu.Unlock()
|
||||||
|
|
||||||
hostname = ToValidHostname(strings.TrimSpace(hostname))
|
hostname = ToValidHostname(strings.TrimSpace(hostname))
|
||||||
fqdn = ToValidHostname(strings.TrimSpace(fqdn))
|
fqdn = ToValidHostname(strings.TrimSpace(fqdn))
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ func (im *InterfaceManager) Start() error {
|
||||||
go im.monitorInterfaceState()
|
go im.monitorInterfaceState()
|
||||||
|
|
||||||
nl := getNetlinkManager()
|
nl := getNetlinkManager()
|
||||||
nl.AddLinkStateCallback(im.ifaceName, link.LinkStateCallback{
|
nl.AddStateChangeCallback(im.ifaceName, link.StateChangeCallback{
|
||||||
Async: true,
|
Async: true,
|
||||||
Func: func(link *link.Link) {
|
Func: func(link *link.Link) {
|
||||||
im.handleLinkStateChange(link)
|
im.handleLinkStateChange(link)
|
||||||
|
|
@ -160,6 +160,7 @@ func (im *InterfaceManager) IsUp() bool {
|
||||||
return im.state.Up
|
return im.state.Up
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsOnline returns true if the interface is online
|
||||||
func (im *InterfaceManager) IsOnline() bool {
|
func (im *InterfaceManager) IsOnline() bool {
|
||||||
im.stateMu.RLock()
|
im.stateMu.RLock()
|
||||||
defer im.stateMu.RUnlock()
|
defer im.stateMu.RUnlock()
|
||||||
|
|
@ -167,6 +168,7 @@ func (im *InterfaceManager) IsOnline() bool {
|
||||||
return im.state.Online
|
return im.state.Online
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IPv4Ready returns true if the interface has an IPv4 address
|
||||||
func (im *InterfaceManager) IPv4Ready() bool {
|
func (im *InterfaceManager) IPv4Ready() bool {
|
||||||
im.stateMu.RLock()
|
im.stateMu.RLock()
|
||||||
defer im.stateMu.RUnlock()
|
defer im.stateMu.RUnlock()
|
||||||
|
|
@ -174,6 +176,7 @@ func (im *InterfaceManager) IPv4Ready() bool {
|
||||||
return im.state.IPv4Ready
|
return im.state.IPv4Ready
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IPv6Ready returns true if the interface has an IPv6 address
|
||||||
func (im *InterfaceManager) IPv6Ready() bool {
|
func (im *InterfaceManager) IPv6Ready() bool {
|
||||||
im.stateMu.RLock()
|
im.stateMu.RLock()
|
||||||
defer im.stateMu.RUnlock()
|
defer im.stateMu.RUnlock()
|
||||||
|
|
@ -181,6 +184,7 @@ func (im *InterfaceManager) IPv6Ready() bool {
|
||||||
return im.state.IPv6Ready
|
return im.state.IPv6Ready
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIPv4Addresses returns the IPv4 addresses of the interface
|
||||||
func (im *InterfaceManager) GetIPv4Addresses() []string {
|
func (im *InterfaceManager) GetIPv4Addresses() []string {
|
||||||
im.stateMu.RLock()
|
im.stateMu.RLock()
|
||||||
defer im.stateMu.RUnlock()
|
defer im.stateMu.RUnlock()
|
||||||
|
|
@ -188,6 +192,7 @@ func (im *InterfaceManager) GetIPv4Addresses() []string {
|
||||||
return im.state.IPv4Addresses
|
return im.state.IPv4Addresses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIPv4Address returns the IPv4 address of the interface
|
||||||
func (im *InterfaceManager) GetIPv4Address() string {
|
func (im *InterfaceManager) GetIPv4Address() string {
|
||||||
im.stateMu.RLock()
|
im.stateMu.RLock()
|
||||||
defer im.stateMu.RUnlock()
|
defer im.stateMu.RUnlock()
|
||||||
|
|
@ -195,6 +200,7 @@ func (im *InterfaceManager) GetIPv4Address() string {
|
||||||
return im.state.IPv4Address
|
return im.state.IPv4Address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIPv6Address returns the IPv6 address of the interface
|
||||||
func (im *InterfaceManager) GetIPv6Address() string {
|
func (im *InterfaceManager) GetIPv6Address() string {
|
||||||
im.stateMu.RLock()
|
im.stateMu.RLock()
|
||||||
defer im.stateMu.RUnlock()
|
defer im.stateMu.RUnlock()
|
||||||
|
|
@ -202,6 +208,7 @@ func (im *InterfaceManager) GetIPv6Address() string {
|
||||||
return im.state.IPv6Address
|
return im.state.IPv6Address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIPv6Addresses returns the IPv6 addresses of the interface
|
||||||
func (im *InterfaceManager) GetIPv6Addresses() []string {
|
func (im *InterfaceManager) GetIPv6Addresses() []string {
|
||||||
im.stateMu.RLock()
|
im.stateMu.RLock()
|
||||||
defer im.stateMu.RUnlock()
|
defer im.stateMu.RUnlock()
|
||||||
|
|
@ -214,6 +221,7 @@ func (im *InterfaceManager) GetIPv6Addresses() []string {
|
||||||
return []string{}
|
return []string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMACAddress returns the MAC address of the interface
|
||||||
func (im *InterfaceManager) GetMACAddress() string {
|
func (im *InterfaceManager) GetMACAddress() string {
|
||||||
return im.state.MACAddress
|
return im.state.MACAddress
|
||||||
}
|
}
|
||||||
|
|
@ -230,7 +238,11 @@ func (im *InterfaceManager) GetState() *types.InterfaceState {
|
||||||
return &state
|
return &state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NTPServers returns the NTP servers of the interface
|
||||||
func (im *InterfaceManager) NTPServers() []net.IP {
|
func (im *InterfaceManager) NTPServers() []net.IP {
|
||||||
|
im.stateMu.RLock()
|
||||||
|
defer im.stateMu.RUnlock()
|
||||||
|
|
||||||
return im.state.NTPServers
|
return im.state.NTPServers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,6 +253,7 @@ func (im *InterfaceManager) GetConfig() *types.NetworkConfig {
|
||||||
return &config
|
return &config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ApplyConfiguration applies the current configuration to the interface
|
||||||
func (im *InterfaceManager) ApplyConfiguration() error {
|
func (im *InterfaceManager) ApplyConfiguration() error {
|
||||||
return im.applyConfiguration()
|
return im.applyConfiguration()
|
||||||
}
|
}
|
||||||
|
|
@ -641,6 +654,10 @@ func (im *InterfaceManager) updateInterfaceState() error {
|
||||||
|
|
||||||
// updateIPAddresses updates the IP addresses in the state
|
// updateIPAddresses updates the IP addresses in the state
|
||||||
func (im *InterfaceManager) updateIPAddresses(nl *link.Link) error {
|
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)
|
addrs, err := nl.AddrList(link.AfUnspec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get addresses: %w", err)
|
return fmt.Errorf("failed to get addresses: %w", err)
|
||||||
|
|
@ -713,7 +730,7 @@ func (im *InterfaceManager) ReconcileLinkAddrs(addrs []*types.IPAddress) error {
|
||||||
if link == nil {
|
if link == nil {
|
||||||
return fmt.Errorf("failed to get interface: %w", err)
|
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
|
// applyDHCPLease applies DHCP lease configuration using ReconcileLinkAddrs
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -2,35 +2,15 @@
|
||||||
package link
|
package link
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/sync"
|
"github.com/jetkvm/kvm/internal/sync"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/network/types"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/vishvananda/netlink"
|
"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 (
|
var (
|
||||||
ipv4DefaultRoute = net.IPNet{
|
ipv4DefaultRoute = net.IPNet{
|
||||||
IP: net.IPv4zero,
|
IP: net.IPv4zero,
|
||||||
|
|
@ -46,30 +26,30 @@ var (
|
||||||
netlinkManagerInstance *NetlinkManager
|
netlinkManagerInstance *NetlinkManager
|
||||||
netlinkManagerOnce sync.Once
|
netlinkManagerOnce sync.Once
|
||||||
|
|
||||||
// Error definitions
|
// 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")
|
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")
|
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
|
// Link is a wrapper around netlink.Link
|
||||||
type Link struct {
|
type Link struct {
|
||||||
netlink.Link
|
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
|
// Attrs returns the attributes of the link
|
||||||
func (l *Link) Attrs() *netlink.LinkAttrs {
|
func (l *Link) Attrs() *netlink.LinkAttrs {
|
||||||
return l.Link.Attrs()
|
return l.Link.Attrs()
|
||||||
|
|
@ -100,496 +80,3 @@ func (l *Link) IsSame(other *Link) bool {
|
||||||
}
|
}
|
||||||
return true
|
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -176,8 +176,7 @@ func (scm *StaticConfigManager) parseIPv4Config(config *types.IPv4StaticConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse IP address and netmask
|
// Parse IP address and netmask
|
||||||
netlinkMgr := getNetlinkManager()
|
ipNet, err := link.ParseIPv4Netmask(config.Address.String, config.Netmask.String)
|
||||||
ipNet, err := netlinkMgr.ParseIPv4Netmask(config.Address.String, config.Netmask.String)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -192,7 +191,7 @@ func (scm *StaticConfigManager) parseIPv4Config(config *types.IPv4StaticConfig)
|
||||||
// Parse DNS servers
|
// Parse DNS servers
|
||||||
var dns []net.IP
|
var dns []net.IP
|
||||||
for _, dnsStr := range config.DNS {
|
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)
|
return nil, fmt.Errorf("invalid DNS server: %w", err)
|
||||||
}
|
}
|
||||||
dns = append(dns, net.ParseIP(dnsStr))
|
dns = append(dns, net.ParseIP(dnsStr))
|
||||||
|
|
@ -212,8 +211,7 @@ func (scm *StaticConfigManager) parseIPv6Config(config *types.IPv6StaticConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse IP address and prefix
|
// Parse IP address and prefix
|
||||||
netlinkMgr := getNetlinkManager()
|
ipNet, err := link.ParseIPv6Prefix(config.Prefix.String, 64) // Default to /64 if not specified
|
||||||
ipNet, err := netlinkMgr.ParseIPv6Prefix(config.Prefix.String, 64) // Default to /64 if not specified
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue