diff --git a/internal/network/types/type.go b/internal/network/types/type.go index e664be6f..2bee48ac 100644 --- a/internal/network/types/type.go +++ b/internal/network/types/type.go @@ -4,9 +4,11 @@ import ( "net" "net/http" "net/url" + "slices" "time" "github.com/guregu/null/v6" + "github.com/vishvananda/netlink" ) // IPAddress represents a network interface address @@ -19,14 +21,50 @@ type IPAddress struct { Permanent bool } +func (a *IPAddress) String() string { + return a.Address.String() +} + +func (a *IPAddress) Compare(n netlink.Addr) bool { + if !a.Address.IP.Equal(n.IP) { + return false + } + if slices.Compare(a.Address.Mask, n.IPNet.Mask) != 0 { + return false + } + return true +} + +func (a *IPAddress) NetlinkAddr() netlink.Addr { + return netlink.Addr{ + IPNet: &a.Address, + } +} + +func (a *IPAddress) DefaultRoute(linkIndex int) netlink.Route { + return netlink.Route{ + Dst: nil, + Gw: a.Gateway, + LinkIndex: linkIndex, + } +} + +// ParsedIPConfig represents the parsed IP configuration +type ParsedIPConfig struct { + Addresses []IPAddress + Nameservers []net.IP + SearchList []string + Domain string + MTU int + Interface string +} + // IPv6Address represents an IPv6 address with lifetime information type IPv6Address struct { Address net.IP `json:"address"` Prefix net.IPNet `json:"prefix"` ValidLifetime *time.Time `json:"valid_lifetime"` PreferredLifetime *time.Time `json:"preferred_lifetime"` - valid_lft int `json:"valid_lft"` - prefered_lft int `json:"prefered_lft"` Scope int `json:"scope"` } @@ -120,6 +158,7 @@ type InterfaceState struct { IPv4Address string `json:"ipv4_address,omitempty"` IPv6Address string `json:"ipv6_address,omitempty"` IPv6LinkLocal string `json:"ipv6_link_local,omitempty"` + IPv6Gateway string `json:"ipv6_gateway,omitempty"` IPv4Addresses []string `json:"ipv4_addresses,omitempty"` IPv6Addresses []IPv6Address `json:"ipv6_addresses,omitempty"` NTPServers []net.IP `json:"ntp_servers,omitempty"` diff --git a/pkg/nmlite/interface.go b/pkg/nmlite/interface.go index 93d7f840..28dc11d6 100644 --- a/pkg/nmlite/interface.go +++ b/pkg/nmlite/interface.go @@ -403,13 +403,23 @@ func (im *InterfaceManager) applyIPv4Static() error { return fmt.Errorf("IPv4 static configuration is nil") } + im.logger.Info().Msg("stopping DHCP") + // Disable DHCP if im.dhcpClient != nil { im.dhcpClient.SetIPv4(false) } - // Apply static configuration - return im.staticConfig.ApplyIPv4Static(im.config.IPv4Static) + im.logger.Info().Interface("config", im.config.IPv4Static).Msg("applying IPv4 static configuration") + + config, err := im.staticConfig.ToIPv4Static(im.config.IPv4Static) + if err != nil { + return fmt.Errorf("failed to convert IPv4 static configuration: %w", err) + } + + im.logger.Info().Interface("config", config).Msg("converted IPv4 static configuration") + + return im.ReconcileLinkAddrs(config.Addresses, link.AfInet) } // applyIPv4DHCP applies DHCP IPv4 configuration @@ -440,13 +450,20 @@ func (im *InterfaceManager) applyIPv6Static() error { return fmt.Errorf("IPv6 static configuration is nil") } + im.logger.Info().Msg("stopping DHCPv6") // Disable DHCPv6 if im.dhcpClient != nil { im.dhcpClient.SetIPv6(false) } // Apply static configuration - return im.staticConfig.ApplyIPv6Static(im.config.IPv6Static) + config, err := im.staticConfig.ToIPv6Static(im.config.IPv6Static) + if err != nil { + return fmt.Errorf("failed to convert IPv6 static configuration: %w", err) + } + im.logger.Info().Interface("config", config).Msg("converted IPv6 static configuration") + + return im.ReconcileLinkAddrs(config.Addresses, link.AfInet6) } // applyIPv6DHCP applies DHCPv6 configuration @@ -696,7 +713,7 @@ func (im *InterfaceManager) updateStateFromDHCPLease(lease *types.DHCPLease) { } // ReconcileLinkAddrs reconciles the link addresses -func (im *InterfaceManager) ReconcileLinkAddrs(addrs []*types.IPAddress) error { +func (im *InterfaceManager) ReconcileLinkAddrs(addrs []types.IPAddress, family int) error { nl := getNetlinkManager() link, err := im.link() if err != nil { @@ -705,7 +722,7 @@ func (im *InterfaceManager) ReconcileLinkAddrs(addrs []*types.IPAddress) error { if link == nil { return fmt.Errorf("failed to get interface: %w", err) } - return nl.ReconcileLink(link, addrs) + return nl.ReconcileLink(link, addrs, family) } // applyDHCPLease applies DHCP lease configuration using ReconcileLinkAddrs @@ -714,7 +731,7 @@ func (im *InterfaceManager) applyDHCPLease(lease *types.DHCPLease) error { ipv4Config := im.convertDHCPLeaseToIPv4Config(lease) // Apply the configuration using ReconcileLinkAddrs - return im.ReconcileLinkAddrs([]*types.IPAddress{ipv4Config}) + return im.ReconcileLinkAddrs([]types.IPAddress{*ipv4Config}, link.AfInet) } // convertDHCPLeaseToIPv4Config converts a DHCP lease to IPv4Config diff --git a/pkg/nmlite/interface_state.go b/pkg/nmlite/interface_state.go index 96fbdb61..f29c8f22 100644 --- a/pkg/nmlite/interface_state.go +++ b/pkg/nmlite/interface_state.go @@ -65,6 +65,8 @@ func (im *InterfaceManager) updateInterfaceState() error { // updateIPAddresses updates the IP addresses in the state func (im *InterfaceManager) updateInterfaceStateAddresses(nl *link.Link) (bool, error) { + mgr := getNetlinkManager() + addrs, err := nl.AddrList(link.AfUnspec) if err != nil { return false, fmt.Errorf("failed to get addresses: %w", err) @@ -75,14 +77,17 @@ func (im *InterfaceManager) updateInterfaceStateAddresses(nl *link.Link) (bool, ipv6Addresses []types.IPv6Address ipv4Addr, ipv6Addr string ipv6LinkLocal string + ipv6Gateway string ipv4Ready, ipv6Ready = false, false stateChanged = false ) + routes, _ := mgr.ListDefaultRoutes(link.AfInet6) + if len(routes) > 0 { + ipv6Gateway = routes[0].Gw.String() + } + for _, addr := range addrs { - im.logger.Debug(). - IPAddr("address", addr.IP). - Msg("checking address") if addr.IP.To4() != nil { // IPv4 address ipv4Addresses = append(ipv4Addresses, addr.IPNet.String()) @@ -138,6 +143,11 @@ func (im *InterfaceManager) updateInterfaceStateAddresses(nl *link.Link) (bool, stateChanged = true } + if im.state.IPv6Gateway != ipv6Gateway { + im.state.IPv6Gateway = ipv6Gateway + stateChanged = true + } + if im.state.IPv4Ready != ipv4Ready { im.state.IPv4Ready = ipv4Ready stateChanged = true diff --git a/pkg/nmlite/jetdhcpc/client.go b/pkg/nmlite/jetdhcpc/client.go index 7bb68795..68706638 100644 --- a/pkg/nmlite/jetdhcpc/client.go +++ b/pkg/nmlite/jetdhcpc/client.go @@ -279,7 +279,7 @@ func (c *Client) handleLeaseChange(lease *Lease) { } // clear all current jobs with the same tags - c.scheduler.RemoveByTags(version) + // c.scheduler.RemoveByTags(version) // add scheduler job to renew the lease if lease.RenewalTime > 0 { @@ -357,6 +357,9 @@ func (c *Client) SetIPv4(ipv4 bool) { } func (c *Client) SetIPv6(ipv6 bool) { + c.cfgMu.Lock() + defer c.cfgMu.Unlock() + c.cfg.IPv6 = ipv6 } diff --git a/pkg/nmlite/link/manager.go b/pkg/nmlite/link/manager.go index 96813b4f..fec9b075 100644 --- a/pkg/nmlite/link/manager.go +++ b/pkg/nmlite/link/manager.go @@ -302,26 +302,28 @@ func (nm *NetlinkManager) RouteReplace(route *netlink.Route) error { return netlink.RouteReplace(route) } +// ListDefaultRoutes lists the default routes for the given family +func (nm *NetlinkManager) ListDefaultRoutes(family int) ([]netlink.Route, error) { + routes, err := netlink.RouteListFiltered( + family, + &netlink.Route{Dst: nil, Table: 254}, + netlink.RT_FILTER_DST|netlink.RT_FILTER_TABLE, + ) + if err != nil { + nm.logger.Error().Err(err).Int("family", family).Msg("failed to list default routes") + return nil, err + } + + return routes, nil +} + // HasDefaultRoute checks if a default route exists for the given family func (nm *NetlinkManager) HasDefaultRoute(family int) bool { - routes, err := netlink.RouteList(nil, family) + routes, err := nm.ListDefaultRoutes(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 + return len(routes) > 0 } // AddDefaultRoute adds a default route @@ -370,59 +372,152 @@ func (nm *NetlinkManager) RemoveDefaultRoute(family int) error { 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) +func (nm *NetlinkManager) reconcileDefaultRoute(link *Link, expected map[string]net.IP, family int) error { + linkIndex := link.Attrs().Index - for _, addr := range expected { - ipCidr := addr.Address.IP.String() + "/" + addr.Address.Mask.String() - expectedAddrs[ipCidr] = true + added := 0 + toRemove := make([]*netlink.Route, 0) + + defaultRoutes, err := nm.ListDefaultRoutes(family) + if err != nil { + return fmt.Errorf("failed to get default routes: %w", err) } - addrs, err := nm.AddrList(link, AfUnspec) + // check existing default routes + for _, defaultRoute := range defaultRoutes { + // only check the default routes for the current link + // TODO: we should also check others later + if defaultRoute.LinkIndex != linkIndex { + continue + } + + key := defaultRoute.Gw.String() + if _, ok := expected[key]; !ok { + toRemove = append(toRemove, &defaultRoute) + continue + } + + nm.logger.Warn().Str("gateway", key).Msg("keeping default route") + delete(expected, key) + } + + // remove remaining default routes + for _, defaultRoute := range toRemove { + nm.logger.Warn().Str("gateway", defaultRoute.Gw.String()).Msg("removing default route") + if err := nm.RouteDel(defaultRoute); err != nil { + nm.logger.Warn().Err(err).Msg("failed to remove default route") + } + } + + // add remaining expected default routes + for _, gateway := range expected { + nm.logger.Warn().Str("gateway", gateway.String()).Msg("adding default route") + + route := &netlink.Route{ + Dst: &ipv4DefaultRoute, + Gw: gateway, + LinkIndex: linkIndex, + } + if family == AfInet6 { + route.Dst = &ipv6DefaultRoute + } + if err := nm.RouteAdd(route); err != nil { + nm.logger.Warn().Err(err).Interface("route", route).Msg("failed to add default route") + } + added++ + } + + nm.logger.Info(). + Int("added", added). + Int("removed", len(toRemove)). + Msg("default routes reconciled") + + return nil +} + +// ReconcileLink reconciles the addresses and routes of a link +func (nm *NetlinkManager) ReconcileLink(link *Link, expected []types.IPAddress, family int) error { + toAdd := make([]*types.IPAddress, 0) + toRemove := make([]*netlink.Addr, 0) + toUpdate := make([]*types.IPAddress, 0) + expectedAddrs := make(map[string]*types.IPAddress) + + expectedGateways := make(map[string]net.IP) + + // add all expected addresses to the map + for _, addr := range expected { + expectedAddrs[addr.String()] = &addr + if addr.Gateway != nil { + expectedGateways[addr.String()] = addr.Gateway + } + } + + addrs, err := nm.AddrList(link, family) if err != nil { return fmt.Errorf("failed to get addresses: %w", err) } + // check existing addresses for _, addr := range addrs { - ipCidr := addr.IP.String() + "/" + addr.IPNet.Mask.String() - existingAddrs[ipCidr] = true + // skip the link-local address + if addr.IP.IsLinkLocalUnicast() { + continue + } + + expectedAddr, ok := expectedAddrs[addr.IPNet.String()] + if !ok { + toRemove = append(toRemove, &addr) + continue + } + + // if it's not fully equal, we need to update it + if !expectedAddr.Compare(addr) { + toUpdate = append(toUpdate, expectedAddr) + continue + } + + // remove it from expected addresses + delete(expectedAddrs, addr.IPNet.String()) } - for _, addr := range expected { - family := AfUnspec - if addr.Address.IP.To4() != nil { - family = AfInet - } else if addr.Address.IP.To16() != nil { - family = AfInet6 + // add remaining expected addresses + for _, addr := range expectedAddrs { + toAdd = append(toAdd, addr) + } + + for _, addr := range toUpdate { + netlinkAddr := addr.NetlinkAddr() + if err := nm.AddrDel(link, &netlinkAddr); err != nil { + nm.logger.Warn().Err(err).Str("address", addr.Address.String()).Msg("failed to update address") } + // we'll add it again later + toAdd = append(toAdd, addr) + } - ipCidr := addr.Address.IP.String() + "/" + addr.Address.Mask.String() - ipNet := &net.IPNet{ - IP: addr.Address.IP, - Mask: addr.Address.Mask, + for _, addr := range toAdd { + netlinkAddr := addr.NetlinkAddr() + if err := nm.AddrAdd(link, &netlinkAddr); err != nil { + nm.logger.Warn().Err(err).Str("address", addr.Address.String()).Msg("failed to add address") } + } - l := nm.logger.With().Str("address", ipNet.String()).Logger() - if ok := existingAddrs[ipCidr]; !ok { - l.Trace().Msg("adding address") - - if err := nm.AddrAdd(link, &netlink.Addr{IPNet: ipNet}); err != nil { - return fmt.Errorf("failed to add address %s: %w", ipCidr, err) - } - - l.Info().Msg("address added") + for _, netlinkAddr := range toRemove { + if err := nm.AddrDel(link, netlinkAddr); err != nil { + nm.logger.Warn().Err(err).Str("address", netlinkAddr.IP.String()).Msg("failed to remove address") } + } - if addr.Gateway != nil { - gl := l.With().Str("gateway", addr.Gateway.String()).Logger() - gl.Trace().Msg("adding default route") - if err := nm.AddDefaultRoute(link, addr.Gateway, family); err != nil { - return fmt.Errorf("failed to add default route for address %s: %w", ipCidr, err) - } - gl.Info().Msg("default route added") - } + actualToAdd := len(toAdd) - len(toUpdate) + if len(toAdd) > 0 || len(toUpdate) > 0 || len(toRemove) > 0 { + nm.logger.Info(). + Int("added", actualToAdd). + Int("updated", len(toUpdate)). + Int("removed", len(toRemove)). + Msg("addresses reconciled") + } + + if err := nm.reconcileDefaultRoute(link, expectedGateways, family); err != nil { + nm.logger.Warn().Err(err).Msg("failed to reconcile default route") } return nil diff --git a/pkg/nmlite/link/types.go b/pkg/nmlite/link/types.go index abcf69e3..06f941a0 100644 --- a/pkg/nmlite/link/types.go +++ b/pkg/nmlite/link/types.go @@ -11,13 +11,3 @@ type IPv4Address struct { Secondary bool Permanent bool } - -// IPv4Config represents the configuration for an IPv4 interface -type IPv4Config struct { - Addresses []IPv4Address - Nameservers []net.IP - SearchList []string - Domain string - MTU int - Interface string -} diff --git a/pkg/nmlite/static.go b/pkg/nmlite/static.go index 292c8b59..e23424b1 100644 --- a/pkg/nmlite/static.go +++ b/pkg/nmlite/static.go @@ -7,7 +7,6 @@ import ( "github.com/jetkvm/kvm/internal/network/types" "github.com/jetkvm/kvm/pkg/nmlite/link" "github.com/rs/zerolog" - "github.com/vishvananda/netlink" ) // StaticConfigManager manages static network configuration @@ -32,81 +31,90 @@ func NewStaticConfigManager(ifaceName string, logger *zerolog.Logger) (*StaticCo }, nil } -// ApplyIPv4Static applies static IPv4 configuration -func (scm *StaticConfigManager) ApplyIPv4Static(config *types.IPv4StaticConfig) error { - scm.logger.Info().Msg("applying static IPv4 configuration") +// ToIPv4Static applies static IPv4 configuration +func (scm *StaticConfigManager) ToIPv4Static(config *types.IPv4StaticConfig) (*types.ParsedIPConfig, error) { + if config == nil { + return nil, fmt.Errorf("config is nil") + } - // Parse and validate configuration - ipv4Config, err := scm.parseIPv4Config(config) + // Parse IP address and netmask + ipNet, err := link.ParseIPv4Netmask(config.Address.String, config.Netmask.String) if err != nil { - return fmt.Errorf("failed to parse IPv4 config: %w", err) + return nil, err + } + scm.logger.Info().Str("ipNet", ipNet.String()).Interface("ipc", config).Msg("parsed IPv4 address and netmask") + + // Parse gateway + gateway := net.ParseIP(config.Gateway.String) + if gateway == nil { + return nil, fmt.Errorf("invalid gateway: %s", config.Gateway.String) } - // Get interface - netlinkMgr := getNetlinkManager() - link, err := netlinkMgr.GetLinkByName(scm.ifaceName) - if err != nil { - return fmt.Errorf("failed to get interface: %w", err) + // Parse DNS servers + var dns []net.IP + for _, dnsStr := range config.DNS { + if err := link.ValidateIPAddress(dnsStr, false); err != nil { + return nil, fmt.Errorf("invalid DNS server: %w", err) + } + dns = append(dns, net.ParseIP(dnsStr)) } - // Ensure interface is up - if err := netlinkMgr.EnsureInterfaceUp(link); err != nil { - return fmt.Errorf("failed to bring interface up: %w", err) + address := types.IPAddress{ + Family: link.AfInet, + Address: *ipNet, + Gateway: gateway, + Secondary: false, + Permanent: true, } - // Apply IP address - if err := scm.applyIPv4Address(link, ipv4Config); err != nil { - return fmt.Errorf("failed to apply IPv4 address: %w", err) - } - - // Apply default route - if err := scm.applyIPv4Route(link, ipv4Config); err != nil { - return fmt.Errorf("failed to apply IPv4 route: %w", err) - } - - scm.logger.Info().Msg("static IPv4 configuration applied successfully") - return nil + return &types.ParsedIPConfig{ + Addresses: []types.IPAddress{address}, + Nameservers: dns, + Interface: scm.ifaceName, + }, nil } -// ApplyIPv6Static applies static IPv6 configuration -func (scm *StaticConfigManager) ApplyIPv6Static(config *types.IPv6StaticConfig) error { - scm.logger.Info().Msg("applying static IPv6 configuration") +// ToIPv6Static applies static IPv6 configuration +func (scm *StaticConfigManager) ToIPv6Static(config *types.IPv6StaticConfig) (*types.ParsedIPConfig, error) { + if config == nil { + return nil, fmt.Errorf("config is nil") + } - // Parse and validate configuration - ipv6Config, err := scm.parseIPv6Config(config) + // Parse IP address and prefix + ipNet, err := link.ParseIPv6Prefix(config.Prefix.String, 64) // Default to /64 if not specified if err != nil { - return fmt.Errorf("failed to parse IPv6 config: %w", err) + return nil, err } - // Get interface - netlinkMgr := getNetlinkManager() - link, err := netlinkMgr.GetLinkByName(scm.ifaceName) - if err != nil { - return fmt.Errorf("failed to get interface: %w", err) + // Parse gateway + gateway := net.ParseIP(config.Gateway.String) + if gateway == nil { + return nil, fmt.Errorf("invalid gateway: %s", config.Gateway.String) } - // Enable IPv6 - if err := scm.enableIPv6(); err != nil { - return fmt.Errorf("failed to enable IPv6: %w", err) + // Parse DNS servers + var dns []net.IP + for _, dnsStr := range config.DNS { + dnsIP := net.ParseIP(dnsStr) + if dnsIP == nil { + return nil, fmt.Errorf("invalid DNS server: %s", dnsStr) + } + dns = append(dns, dnsIP) } - // Ensure interface is up - if err := netlinkMgr.EnsureInterfaceUp(link); err != nil { - return fmt.Errorf("failed to bring interface up: %w", err) + address := types.IPAddress{ + Family: link.AfInet6, + Address: *ipNet, + Gateway: gateway, + Secondary: false, + Permanent: true, } - // Apply IP address - if err := scm.applyIPv6Address(link, ipv6Config); err != nil { - return fmt.Errorf("failed to apply IPv6 address: %w", err) - } - - // Apply default route - if err := scm.applyIPv6Route(link, ipv6Config); err != nil { - return fmt.Errorf("failed to apply IPv6 route: %w", err) - } - - scm.logger.Info().Msg("static IPv6 configuration applied successfully") - return nil + return &types.ParsedIPConfig{ + Addresses: []types.IPAddress{address}, + Nameservers: dns, + Interface: scm.ifaceName, + }, nil } // DisableIPv4 disables IPv4 on the interface @@ -169,156 +177,6 @@ func (scm *StaticConfigManager) EnableIPv6LinkLocal() error { return netlinkMgr.EnsureInterfaceUp(link) } -// parseIPv4Config parses and validates IPv4 static configuration -func (scm *StaticConfigManager) parseIPv4Config(config *types.IPv4StaticConfig) (*parsedIPv4Config, error) { - if config == nil { - return nil, fmt.Errorf("config is nil") - } - - // Parse IP address and netmask - ipNet, err := link.ParseIPv4Netmask(config.Address.String, config.Netmask.String) - if err != nil { - return nil, err - } - scm.logger.Info().Str("ipNet", ipNet.String()).Interface("ipc", config).Msg("parsed IPv4 address and netmask") - - // Parse gateway - gateway := net.ParseIP(config.Gateway.String) - if gateway == nil { - return nil, fmt.Errorf("invalid gateway: %s", config.Gateway.String) - } - - // Parse DNS servers - var dns []net.IP - for _, dnsStr := range config.DNS { - if err := link.ValidateIPAddress(dnsStr, false); err != nil { - return nil, fmt.Errorf("invalid DNS server: %w", err) - } - dns = append(dns, net.ParseIP(dnsStr)) - } - - return &parsedIPv4Config{ - network: *ipNet, - gateway: gateway, - dns: dns, - }, nil -} - -// parseIPv6Config parses and validates IPv6 static configuration -func (scm *StaticConfigManager) parseIPv6Config(config *types.IPv6StaticConfig) (*parsedIPv6Config, error) { - if config == nil { - return nil, fmt.Errorf("config is nil") - } - - // Parse IP address and prefix - ipNet, err := link.ParseIPv6Prefix(config.Prefix.String, 64) // Default to /64 if not specified - if err != nil { - return nil, err - } - - // Parse gateway - gateway := net.ParseIP(config.Gateway.String) - if gateway == nil { - return nil, fmt.Errorf("invalid gateway: %s", config.Gateway.String) - } - - // Parse DNS servers - var dns []net.IP - for _, dnsStr := range config.DNS { - dnsIP := net.ParseIP(dnsStr) - if dnsIP == nil { - return nil, fmt.Errorf("invalid DNS server: %s", dnsStr) - } - dns = append(dns, dnsIP) - } - - return &parsedIPv6Config{ - prefix: *ipNet, - gateway: gateway, - dns: dns, - }, nil -} - -// applyIPv4Address applies IPv4 address to interface -func (scm *StaticConfigManager) applyIPv4Address(iface *link.Link, config *parsedIPv4Config) error { - netlinkMgr := getNetlinkManager() - - // Remove existing IPv4 addresses - if err := netlinkMgr.RemoveAllAddresses(iface, link.AfInet); err != nil { - return fmt.Errorf("failed to remove existing IPv4 addresses: %w", err) - } - - // Add new address - addr := &netlink.Addr{ - IPNet: &config.network, - } - if err := netlinkMgr.AddrAdd(iface, addr); err != nil { - return fmt.Errorf("failed to add IPv4 address: %w", err) - } - - scm.logger.Info().Str("address", config.network.String()).Msg("IPv4 address applied") - return nil -} - -// applyIPv6Address applies IPv6 address to interface -func (scm *StaticConfigManager) applyIPv6Address(iface *link.Link, config *parsedIPv6Config) error { - netlinkMgr := getNetlinkManager() - - // Remove existing global IPv6 addresses - if err := netlinkMgr.RemoveNonLinkLocalIPv6Addresses(iface); err != nil { - return fmt.Errorf("failed to remove existing IPv6 addresses: %w", err) - } - - // Add new address - addr := &netlink.Addr{ - IPNet: &config.prefix, - } - if err := netlinkMgr.AddrAdd(iface, addr); err != nil { - return fmt.Errorf("failed to add IPv6 address: %w", err) - } - - scm.logger.Info().Str("address", config.prefix.String()).Msg("IPv6 address applied") - return nil -} - -// applyIPv4Route applies IPv4 default route -func (scm *StaticConfigManager) applyIPv4Route(iface *link.Link, config *parsedIPv4Config) error { - netlinkMgr := getNetlinkManager() - - // Check if default route already exists - if netlinkMgr.HasDefaultRoute(link.AfInet) { - scm.logger.Info().Msg("IPv4 default route already exists") - return nil - } - - // Add default route - if err := netlinkMgr.AddDefaultRoute(iface, config.gateway, link.AfInet); err != nil { - return fmt.Errorf("failed to add IPv4 default route: %w", err) - } - - scm.logger.Info().Str("gateway", config.gateway.String()).Msg("IPv4 default route applied") - return nil -} - -// applyIPv6Route applies IPv6 default route -func (scm *StaticConfigManager) applyIPv6Route(iface *link.Link, config *parsedIPv6Config) error { - netlinkMgr := getNetlinkManager() - - // Check if default route already exists - if netlinkMgr.HasDefaultRoute(link.AfInet6) { - scm.logger.Info().Msg("IPv6 default route already exists") - return nil - } - - // Add default route - if err := netlinkMgr.AddDefaultRoute(iface, config.gateway, link.AfInet6); err != nil { - return fmt.Errorf("failed to add IPv6 default route: %w", err) - } - - scm.logger.Info().Str("gateway", config.gateway.String()).Msg("IPv6 default route applied") - return nil -} - // removeIPv4DefaultRoute removes IPv4 default route func (scm *StaticConfigManager) removeIPv4DefaultRoute() error { netlinkMgr := getNetlinkManager() @@ -330,17 +188,3 @@ func (scm *StaticConfigManager) enableIPv6() error { netlinkMgr := getNetlinkManager() return netlinkMgr.EnableIPv6(scm.ifaceName) } - -// parsedIPv4Config represents parsed IPv4 configuration -type parsedIPv4Config struct { - network net.IPNet - gateway net.IP - dns []net.IP -} - -// parsedIPv6Config represents parsed IPv6 configuration -type parsedIPv6Config struct { - prefix net.IPNet - gateway net.IP - dns []net.IP -} diff --git a/ui/src/components/Ipv6NetworkCard.tsx b/ui/src/components/Ipv6NetworkCard.tsx index ca6c3079..5ed27ca1 100644 --- a/ui/src/components/Ipv6NetworkCard.tsx +++ b/ui/src/components/Ipv6NetworkCard.tsx @@ -25,6 +25,14 @@ export default function Ipv6NetworkCard({ {networkState?.ipv6_link_local} +
+ + Gateway + + + {networkState?.ipv6_gateway} + +
diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index e9ef15d5..ca8ff46f 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -709,6 +709,7 @@ export interface NetworkState { ipv6?: string; ipv6_addresses?: IPv6Address[]; ipv6_link_local?: string; + ipv6_gateway?: string; dhcp_lease?: DhcpLease; setNetworkState: (state: NetworkState) => void;