mirror of https://github.com/jetkvm/kvm.git
Compare commits
9 Commits
aef26459d3
...
f128343187
| Author | SHA1 | Date |
|---|---|---|
|
|
f128343187 | |
|
|
8310077af6 | |
|
|
638949c289 | |
|
|
8449911343 | |
|
|
db64c649d4 | |
|
|
abb8c4f0b5 | |
|
|
f452e6b4c4 | |
|
|
d6ebbf4d09 | |
|
|
17a15619ff |
14
config.go
14
config.go
|
|
@ -246,17 +246,25 @@ func LoadConfig() {
|
|||
}
|
||||
|
||||
func SaveConfig() error {
|
||||
return saveConfig(configPath)
|
||||
}
|
||||
|
||||
func SaveBackupConfig() error {
|
||||
return saveConfig(configPath + ".bak")
|
||||
}
|
||||
|
||||
func saveConfig(path string) error {
|
||||
configLock.Lock()
|
||||
defer configLock.Unlock()
|
||||
|
||||
logger.Trace().Str("path", configPath).Msg("Saving config")
|
||||
logger.Trace().Str("path", path).Msg("Saving config")
|
||||
|
||||
// fixup old keyboard layout value
|
||||
if config.KeyboardLayout == "en_US" {
|
||||
config.KeyboardLayout = "en-US"
|
||||
}
|
||||
|
||||
file, err := os.Create(configPath)
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create config file: %w", err)
|
||||
}
|
||||
|
|
@ -272,7 +280,7 @@ func SaveConfig() error {
|
|||
return fmt.Errorf("failed to wite config: %w", err)
|
||||
}
|
||||
|
||||
logger.Info().Str("path", configPath).Msg("config saved")
|
||||
logger.Info().Str("path", path).Msg("config saved")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
11
display.go
11
display.go
|
|
@ -184,7 +184,6 @@ func waitCtrlAndRequestDisplayUpdate(shouldWakeDisplay bool, reason string) {
|
|||
waitDisplayUpdate.Lock()
|
||||
defer waitDisplayUpdate.Unlock()
|
||||
|
||||
// nativeInstance.WaitCtrlClientConnected()
|
||||
requestDisplayUpdate(shouldWakeDisplay, reason)
|
||||
}
|
||||
|
||||
|
|
@ -326,12 +325,9 @@ func startBacklightTickers() {
|
|||
dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
||||
|
||||
go func() {
|
||||
for { //nolint:staticcheck
|
||||
select {
|
||||
case <-dimTicker.C:
|
||||
for range dimTicker.C {
|
||||
tick_displayDim()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
|
|
@ -340,12 +336,9 @@ func startBacklightTickers() {
|
|||
offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
||||
|
||||
go func() {
|
||||
for { //nolint:staticcheck
|
||||
select {
|
||||
case <-offTicker.C:
|
||||
for range offTicker.C {
|
||||
tick_displayOff()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
go.mod
1
go.mod
|
|
@ -61,6 +61,7 @@ require (
|
|||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mdlayher/ndp v1.1.0 // indirect
|
||||
github.com/mdlayher/packet v1.1.2 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -99,6 +99,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
|||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mdlayher/ndp v1.1.0 h1:QylGKGVtH60sKZUE88+IW5ila1Z/M9/OXhWdsVKuscs=
|
||||
github.com/mdlayher/ndp v1.1.0/go.mod h1:FmgESgemgjl38vuOIyAHWUUL6vQKA/pQNkvXdWsdQFM=
|
||||
github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=
|
||||
github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=
|
||||
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
package netif
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
func ensureInterfaceIsUp(iface *netlink.Link) error {
|
||||
if (*iface).Attrs().OperState == netlink.OperUp {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := netlink.LinkSetUp(*iface); err != nil {
|
||||
return fmt.Errorf("failed to set interface up: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
4
main.go
4
main.go
|
|
@ -33,6 +33,7 @@ func Main() {
|
|||
go runWatchdog()
|
||||
go confirmCurrentSystem()
|
||||
|
||||
initDisplay()
|
||||
initNative(systemVersionLocal, appVersionLocal)
|
||||
|
||||
http.DefaultClient.Timeout = 1 * time.Minute
|
||||
|
|
@ -74,9 +75,6 @@ func Main() {
|
|||
}
|
||||
initJiggler()
|
||||
|
||||
// initialize display
|
||||
initDisplay()
|
||||
|
||||
go func() {
|
||||
time.Sleep(15 * time.Minute)
|
||||
for {
|
||||
|
|
|
|||
36
network.go
36
network.go
|
|
@ -2,7 +2,9 @@ package kvm
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/confparser"
|
||||
"github.com/jetkvm/kvm/internal/mdns"
|
||||
"github.com/jetkvm/kvm/internal/network/types"
|
||||
"github.com/jetkvm/kvm/pkg/nmlite"
|
||||
|
|
@ -72,12 +74,32 @@ func networkStateChanged(iface string, state *types.InterfaceState) {
|
|||
}
|
||||
}
|
||||
|
||||
func validateNetworkConfig() {
|
||||
err := confparser.SetDefaultsAndValidate(config.NetworkConfig)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
networkLogger.Error().Err(err).Msg("failed to validate config, reverting to default config")
|
||||
SaveBackupConfig()
|
||||
|
||||
// do not use a pointer to the default config
|
||||
// it has been already changed during LoadConfig
|
||||
config.NetworkConfig = &(types.NetworkConfig{})
|
||||
SaveConfig()
|
||||
}
|
||||
|
||||
func initNetwork() error {
|
||||
ensureConfigLoaded()
|
||||
|
||||
// validate the config, if it's invalid, revert to the default config and save the backup
|
||||
validateNetworkConfig()
|
||||
|
||||
networkManager = nmlite.NewNetworkManager(context.Background(), networkLogger)
|
||||
networkManager.SetOnInterfaceStateChange(networkStateChanged)
|
||||
networkManager.AddInterface(NetIfName, config.NetworkConfig)
|
||||
if err := networkManager.AddInterface(NetIfName, config.NetworkConfig); err != nil {
|
||||
return fmt.Errorf("failed to add interface: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -94,13 +116,18 @@ func rpcGetNetworkSettings() *RpcNetworkSettings {
|
|||
func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, error) {
|
||||
netConfig := settings.ToNetworkConfig()
|
||||
|
||||
networkLogger.Debug().Interface("newConfig", netConfig).Interface("config", settings).Msg("setting new config")
|
||||
l := networkLogger.With().
|
||||
Str("interface", NetIfName).
|
||||
Interface("newConfig", netConfig).
|
||||
Logger()
|
||||
|
||||
l.Debug().Msg("setting new config")
|
||||
|
||||
s := networkManager.SetInterfaceConfig(NetIfName, netConfig)
|
||||
if s != nil {
|
||||
return nil, s
|
||||
}
|
||||
networkLogger.Debug().Interface("newConfig", netConfig).Interface("config", settings).Msg("new config")
|
||||
l.Debug().Msg("new config applied")
|
||||
|
||||
newConfig, err := networkManager.GetInterfaceConfig(NetIfName)
|
||||
if err != nil {
|
||||
|
|
@ -108,8 +135,7 @@ func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, er
|
|||
}
|
||||
config.NetworkConfig = newConfig
|
||||
|
||||
networkLogger.Debug().Interface("newConfig", newConfig).Interface("config", settings).Msg("saving config")
|
||||
|
||||
l.Debug().Msg("saving new config")
|
||||
if err := SaveConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"time"
|
||||
|
||||
|
|
@ -13,6 +14,7 @@ import (
|
|||
"github.com/jetkvm/kvm/internal/logging"
|
||||
"github.com/jetkvm/kvm/internal/network/types"
|
||||
"github.com/jetkvm/kvm/pkg/nmlite/link"
|
||||
"github.com/mdlayher/ndp"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
|
@ -104,6 +106,9 @@ func NewInterfaceManager(ctx context.Context, ifaceName string, config *types.Ne
|
|||
|
||||
// Start starts managing the interface
|
||||
func (im *InterfaceManager) Start() error {
|
||||
im.stateMu.Lock()
|
||||
defer im.stateMu.Unlock()
|
||||
|
||||
im.logger.Info().Msg("starting interface manager")
|
||||
|
||||
// Start monitoring interface state
|
||||
|
|
@ -111,18 +116,38 @@ func (im *InterfaceManager) Start() error {
|
|||
go im.monitorInterfaceState()
|
||||
|
||||
nl := getNetlinkManager()
|
||||
nl.AddLinkStateCallback(im.ifaceName, link.LinkStateCallback{
|
||||
|
||||
// Set the link state
|
||||
linkState, err := nl.GetLinkByName(im.ifaceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get interface: %w", err)
|
||||
}
|
||||
im.linkState = linkState
|
||||
|
||||
// Bring the interface up
|
||||
_, linkUpErr := nl.EnsureInterfaceUpWithTimeout(
|
||||
im.ctx,
|
||||
im.linkState,
|
||||
30*time.Second,
|
||||
)
|
||||
|
||||
// Set callback after the interface is up
|
||||
nl.AddStateChangeCallback(im.ifaceName, link.StateChangeCallback{
|
||||
Async: true,
|
||||
Func: func(link *link.Link) {
|
||||
im.handleLinkStateChange(link)
|
||||
},
|
||||
})
|
||||
|
||||
if linkUpErr != nil {
|
||||
im.logger.Error().Err(linkUpErr).Msg("failed to bring interface up, continuing anyway")
|
||||
} else {
|
||||
// Apply initial configuration
|
||||
if err := im.applyConfiguration(); err != nil {
|
||||
im.logger.Error().Err(err).Msg("failed to apply initial configuration")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
im.logger.Info().Msg("interface manager started")
|
||||
return nil
|
||||
|
|
@ -160,6 +185,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 +193,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 +201,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 +209,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 +217,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 +225,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 +233,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 +246,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 +263,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 +278,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()
|
||||
}
|
||||
|
|
@ -436,10 +474,20 @@ func (im *InterfaceManager) applyIPv6SLAAC() error {
|
|||
}
|
||||
|
||||
netlinkMgr := getNetlinkManager()
|
||||
|
||||
// Ensure interface is up
|
||||
if err := netlinkMgr.EnsureInterfaceUp(l); err != nil {
|
||||
return fmt.Errorf("failed to bring interface up: %w", err)
|
||||
}
|
||||
|
||||
if err := netlinkMgr.RemoveNonLinkLocalIPv6Addresses(l); err != nil {
|
||||
return fmt.Errorf("failed to remove non-link-local IPv6 addresses: %w", err)
|
||||
}
|
||||
|
||||
if err := im.SendRouterSolicitation(); err != nil {
|
||||
im.logger.Error().Err(err).Msg("failed to send router solicitation, continuing anyway")
|
||||
}
|
||||
|
||||
// Enable SLAAC
|
||||
return im.staticConfig.EnableIPv6SLAAC()
|
||||
}
|
||||
|
|
@ -532,6 +580,53 @@ func (im *InterfaceManager) handleLinkStateChange(link *link.Link) {
|
|||
}
|
||||
}
|
||||
|
||||
// SendRouterSolicitation sends a router solicitation
|
||||
func (im *InterfaceManager) SendRouterSolicitation() error {
|
||||
im.logger.Info().Msg("sending router solicitation")
|
||||
m := &ndp.RouterSolicitation{}
|
||||
|
||||
l, err := im.link()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get interface: %w", err)
|
||||
}
|
||||
|
||||
if l.Attrs().OperState != netlink.OperUp {
|
||||
return fmt.Errorf("interface %s is not up", im.ifaceName)
|
||||
}
|
||||
|
||||
iface := l.Interface()
|
||||
if iface == nil {
|
||||
return fmt.Errorf("failed to get net.Interface for %s", im.ifaceName)
|
||||
}
|
||||
|
||||
hwAddr := l.HardwareAddr()
|
||||
if hwAddr == nil {
|
||||
return fmt.Errorf("failed to get hardware address for %s", im.ifaceName)
|
||||
}
|
||||
|
||||
c, _, err := ndp.Listen(iface, ndp.LinkLocal)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create NDP listener on %s: %w", im.ifaceName, err)
|
||||
}
|
||||
|
||||
m.Options = append(m.Options, &ndp.LinkLayerAddress{
|
||||
Addr: hwAddr,
|
||||
Direction: ndp.Source,
|
||||
})
|
||||
|
||||
targetAddr := netip.MustParseAddr("ff02::2")
|
||||
|
||||
if err := c.WriteTo(m, nil, targetAddr); err != nil {
|
||||
c.Close()
|
||||
return fmt.Errorf("failed to write to %s: %w", targetAddr.String(), err)
|
||||
}
|
||||
|
||||
im.logger.Info().Msg("router solicitation sent")
|
||||
c.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (im *InterfaceManager) handleLinkUp() {
|
||||
im.logger.Info().Msg("link up")
|
||||
|
||||
|
|
@ -540,6 +635,11 @@ func (im *InterfaceManager) handleLinkUp() {
|
|||
if im.config.IPv4Mode.String == "dhcp" {
|
||||
im.dhcpClient.Renew()
|
||||
}
|
||||
|
||||
if im.config.IPv6Mode.String == "slaac" {
|
||||
im.staticConfig.EnableIPv6SLAAC()
|
||||
im.SendRouterSolicitation()
|
||||
}
|
||||
}
|
||||
|
||||
func (im *InterfaceManager) handleLinkDown() {
|
||||
|
|
@ -641,6 +741,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 +817,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
|
||||
|
|
|
|||
|
|
@ -153,14 +153,14 @@ func (c *Client) ensureInterfaceUp(ifname string) (*link.Link, error) {
|
|||
return nlm.EnsureInterfaceUpWithTimeout(c.ctx, iface, c.cfg.LinkUpTimeout)
|
||||
}
|
||||
|
||||
func (c *Client) sendInitialRequests() chan interface{} {
|
||||
func (c *Client) sendInitialRequests() chan any {
|
||||
return c.sendRequests(c.cfg.IPv4, c.cfg.IPv6)
|
||||
}
|
||||
|
||||
func (c *Client) sendRequestsFamily(
|
||||
family int,
|
||||
wg *sync.WaitGroup,
|
||||
r *chan interface{},
|
||||
r *chan any,
|
||||
l *zerolog.Logger,
|
||||
iface *link.Link,
|
||||
) {
|
||||
|
|
@ -185,12 +185,12 @@ func (c *Client) sendRequestsFamily(
|
|||
}(iface)
|
||||
}
|
||||
|
||||
func (c *Client) sendRequests(ipv4, ipv6 bool) chan interface{} {
|
||||
func (c *Client) sendRequests(ipv4, ipv6 bool) chan any {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// Yeah, this is a hack, until we can cancel all leases in progress.
|
||||
r := make(chan interface{}, 3*len(c.ifaces))
|
||||
r := make(chan any, 3*len(c.ifaces))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, iface := range c.ifaces {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package jetdhcpc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
|
||||
"github.com/vishvananda/netlink"
|
||||
|
|
@ -46,7 +48,11 @@ func (c *Client) requestLease4(iface netlink.Link) (*Lease, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
l.Info().Msgf("DHCPv4 lease acquired: %s", lease.ACK.Summary())
|
||||
if lease == nil || lease.ACK == nil {
|
||||
return nil, fmt.Errorf("failed to acquire DHCPv4 lease")
|
||||
}
|
||||
|
||||
summaryStructured(lease.ACK, &l).Info().Msgf("DHCPv4 lease acquired: %s", lease.ACK.String())
|
||||
|
||||
return fromNclient4Lease(lease, ifname), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,11 +21,35 @@ func (s dhcpLogger) Printf(format string, v ...interface{}) {
|
|||
|
||||
// PrintMessage prints a DHCP message in the short format via predefined Printfer
|
||||
func (s dhcpLogger) PrintMessage(prefix string, message *dhcpv4.DHCPv4) {
|
||||
s.l.Info().Str("prefix", prefix).Str("message", message.String()).Msg("DHCP message")
|
||||
s.l.Info().Msgf("%s: %s", prefix, message.String())
|
||||
}
|
||||
|
||||
func summaryStructured(d *dhcpv4.DHCPv4, l *zerolog.Logger) *zerolog.Logger {
|
||||
logger := l.With().
|
||||
Str("opCode", d.OpCode.String()).
|
||||
Str("hwType", d.HWType.String()).
|
||||
Int("hopCount", int(d.HopCount)).
|
||||
Str("transactionID", d.TransactionID.String()).
|
||||
Int("numSeconds", int(d.NumSeconds)).
|
||||
Str("flagsString", d.FlagsToString()).
|
||||
Int("flags", int(d.Flags)).
|
||||
Str("clientIP", d.ClientIPAddr.String()).
|
||||
Str("yourIP", d.YourIPAddr.String()).
|
||||
Str("serverIP", d.ServerIPAddr.String()).
|
||||
Str("gatewayIP", d.GatewayIPAddr.String()).
|
||||
Str("clientMAC", d.ClientHWAddr.String()).
|
||||
Str("serverHostname", d.ServerHostName).
|
||||
Str("bootFileName", d.BootFileName).
|
||||
Str("options", d.Options.Summary(nil)).
|
||||
Logger()
|
||||
return &logger
|
||||
}
|
||||
|
||||
func (c *Client) getDHCP4Logger(ifname string) nclient4.ClientOpt {
|
||||
logger := c.l.With().Str("interface", ifname).Logger()
|
||||
logger := c.l.With().
|
||||
Str("interface", ifname).
|
||||
Str("source", "dhcp4").
|
||||
Logger()
|
||||
|
||||
return nclient4.WithLogger(dhcpLogger{
|
||||
l: &logger,
|
||||
|
|
|
|||
|
|
@ -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,429 @@
|
|||
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
|
||||
|
||||
l = l.With().
|
||||
Int("attempt", attempt).
|
||||
Dur("duration", time.Since(start)).
|
||||
Str("state", state.String()).
|
||||
Logger()
|
||||
if state == netlink.OperUp || state == netlink.OperUnknown {
|
||||
if attempt > 0 {
|
||||
l.Info().Int("attempt", attempt-1).Msg("interface is up")
|
||||
}
|
||||
return link, nil
|
||||
}
|
||||
|
||||
l.Info().Msg("bringing up interface")
|
||||
|
||||
// bring up the interface
|
||||
if err = nm.LinkSetUp(link); err != nil {
|
||||
l.Error().Err(err).Msg("interface can't make it up")
|
||||
}
|
||||
|
||||
// refresh the link attributes
|
||||
if err = link.Refresh(); err != nil {
|
||||
l.Error().Err(err).Msg("failed to refresh link attributes")
|
||||
}
|
||||
|
||||
// check the state again
|
||||
state = link.Attrs().OperState
|
||||
l = l.With().Str("new_state", state.String()).Logger()
|
||||
if state == netlink.OperUp {
|
||||
l.Info().Msg("interface is up")
|
||||
return link, nil
|
||||
}
|
||||
l.Warn().Msg("interface is still down, retrying")
|
||||
|
||||
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()
|
||||
ipNet := &net.IPNet{
|
||||
IP: addr.Address.IP,
|
||||
Mask: addr.Address.Mask,
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -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,37 +26,96 @@ var (
|
|||
netlinkManagerInstance *NetlinkManager
|
||||
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")
|
||||
// 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
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// All lock actions should be done in external functions
|
||||
// and the internal functions should not be called directly
|
||||
|
||||
func (l *Link) refresh() error {
|
||||
linkName := l.ifName()
|
||||
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
|
||||
}
|
||||
|
||||
func (l *Link) attrs() *netlink.LinkAttrs {
|
||||
return l.Link.Attrs()
|
||||
}
|
||||
|
||||
func (l *Link) ifName() string {
|
||||
attrs := l.attrs()
|
||||
if attrs.Name == "" {
|
||||
return ""
|
||||
}
|
||||
return attrs.Name
|
||||
}
|
||||
|
||||
// Refresh refreshes the link
|
||||
func (l *Link) Refresh() error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
return l.refresh()
|
||||
}
|
||||
|
||||
// Attrs returns the attributes of the link
|
||||
func (l *Link) Attrs() *netlink.LinkAttrs {
|
||||
return l.Link.Attrs()
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
return l.attrs()
|
||||
}
|
||||
|
||||
// Interface returns the interface of the link
|
||||
func (l *Link) Interface() *net.Interface {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
ifname := l.ifName()
|
||||
if ifname == "" {
|
||||
return nil
|
||||
}
|
||||
iface, err := net.InterfaceByName(ifname)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return iface
|
||||
}
|
||||
|
||||
// HardwareAddr returns the hardware address of the link
|
||||
func (l *Link) HardwareAddr() net.HardwareAddr {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
attrs := l.attrs()
|
||||
if attrs.HardwareAddr == nil {
|
||||
return nil
|
||||
}
|
||||
return attrs.HardwareAddr
|
||||
}
|
||||
|
||||
// AddrList returns the addresses of the link
|
||||
func (l *Link) AddrList(family int) ([]netlink.Addr, error) {
|
||||
return netlink.AddrList(l, family)
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
return netlink.AddrList(l.Link, family)
|
||||
}
|
||||
|
||||
func (l *Link) IsSame(other *Link) bool {
|
||||
|
|
@ -100,496 +139,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -133,7 +133,7 @@ func (rcm *ResolvConfManager) generateResolvConf(iface string, nameservers []net
|
|||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, map[string]interface{}{
|
||||
if err := tmpl.Execute(&buf, map[string]any{
|
||||
"iface": iface,
|
||||
"nameservers": nameservers,
|
||||
"searchList": searchList,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -222,20 +222,16 @@ export default function SettingsNetworkRoute() {
|
|||
description="Configure the network settings for the device"
|
||||
action={
|
||||
<>
|
||||
|
||||
{(formState.isDirty || formState.isSubmitting) && (
|
||||
// <div className="animate-fadeInStill opacity-1 animation-duration-300">
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
disabled={formState.isSubmitting}
|
||||
disabled={!(formState.isDirty || formState.isSubmitting)}
|
||||
loading={formState.isSubmitting}
|
||||
type="submit"
|
||||
text={formState.isSubmitting ? "Saving..." : "Save Settings"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
|
@ -256,6 +252,7 @@ export default function SettingsNetworkRoute() {
|
|||
<SettingsItem title="Hostname" description="Set the device hostname">
|
||||
<InputField
|
||||
size="SM"
|
||||
placeholder="jetkvm"
|
||||
{...register("hostname")}
|
||||
error={formState.errors.hostname?.message}
|
||||
/>
|
||||
|
|
@ -420,21 +417,18 @@ export default function SettingsNetworkRoute() {
|
|||
)}
|
||||
</AutoHeight>
|
||||
</div>
|
||||
{(formState.isDirty || formState.isSubmitting) && (
|
||||
<>
|
||||
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||
<div className="animate-fadeInStill opacity-0 animation-duration-300">
|
||||
<div className="animate-fadeInStill animation-duration-300">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
disabled={formState.isSubmitting}
|
||||
disabled={!(formState.isDirty || formState.isSubmitting)}
|
||||
loading={formState.isSubmitting}
|
||||
type="submit"
|
||||
text={formState.isSubmitting ? "Saving..." : "Save Settings"}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
|
|
|||
Loading…
Reference in New Issue