Compare commits

...

6 Commits

Author SHA1 Message Date
Siyuan 656df6c910 fix deadlocks 2025-10-07 18:59:43 +00:00
Siyuan 45b55fe89f fix dhcp6 logger 2025-10-07 18:31:44 +00:00
Siyuan 78f0479b6b fix netmask calculation 2025-10-07 18:27:47 +00:00
Siyuan 3c83bcfe69 add missing dhcp client methods 2025-10-07 17:50:29 +00:00
Siyuan 50469c1fb6 renew dhcp lease on link up 2025-10-07 17:45:44 +00:00
Siyuan 456ee66fc2 monitor link state using netlink 2025-10-07 17:18:20 +00:00
25 changed files with 731 additions and 493 deletions

View File

@ -1,127 +0,0 @@
package network
// import (
// "fmt"
// "time"
// "github.com/jetkvm/kvm/internal/confparser"
// "github.com/jetkvm/kvm/internal/network/types"
// "github.com/jetkvm/kvm/internal/udhcpc"
// )
// type RpcIPv6Address struct {
// Address string `json:"address"`
// ValidLifetime *time.Time `json:"valid_lifetime,omitempty"`
// PreferredLifetime *time.Time `json:"preferred_lifetime,omitempty"`
// Scope int `json:"scope"`
// }
// type RpcNetworkState struct {
// InterfaceName string `json:"interface_name"`
// MacAddress string `json:"mac_address"`
// IPv4 string `json:"ipv4,omitempty"`
// IPv6 string `json:"ipv6,omitempty"`
// IPv6LinkLocal string `json:"ipv6_link_local,omitempty"`
// IPv4Addresses []string `json:"ipv4_addresses,omitempty"`
// IPv6Addresses []RpcIPv6Address `json:"ipv6_addresses,omitempty"`
// DHCPLease *udhcpc.Lease `json:"dhcp_lease,omitempty"`
// }
// type RpcNetworkSettings struct {
// NetworkConfig types.NetworkConfig
// }
// // func (s *NetworkInterfaceState) MacAddress() string {
// // if s.macAddr == nil {
// // return ""
// // }
// // return s.macAddr.String()
// // }
// // func (s *NetworkInterfaceState) IPv4Address() string {
// // if s.ipv4Addr == nil {
// // return ""
// // }
// // return s.ipv4Addr.String()
// // }
// // func (s *NetworkInterfaceState) IPv6Address() string {
// // if s.ipv6Addr == nil {
// // return ""
// // }
// // return s.ipv6Addr.String()
// // }
// // func (s *NetworkInterfaceState) IPv6LinkLocalAddress() string {
// // if s.ipv6LinkLocal == nil {
// // return ""
// // }
// // return s.ipv6LinkLocal.String()
// // }
// func (s *NetworkInterfaceState) RpcGetNetworkState() RpcNetworkState {
// ipv6Addresses := make([]RpcIPv6Address, 0)
// if s.ipv6Addresses != nil && s.config.IPv6Mode.String != "disabled" {
// for _, addr := range s.ipv6Addresses {
// ipv6Addresses = append(ipv6Addresses, RpcIPv6Address{
// Address: addr.Prefix.String(),
// ValidLifetime: addr.ValidLifetime,
// PreferredLifetime: addr.PreferredLifetime,
// Scope: addr.Scope,
// })
// }
// }
// return RpcNetworkState{
// InterfaceName: s.interfaceName,
// MacAddress: s.MacAddress(),
// IPv4: s.IPv4Address(),
// IPv6: s.IPv6Address(),
// IPv6LinkLocal: s.IPv6LinkLocalAddress(),
// IPv4Addresses: s.ipv4Addresses,
// IPv6Addresses: ipv6Addresses,
// DHCPLease: s.dhcpClient.GetLease(),
// }
// }
// func (s *NetworkInterfaceState) RpcGetNetworkSettings() RpcNetworkSettings {
// if s.config == nil {
// return RpcNetworkSettings{}
// }
// return RpcNetworkSettings{
// NetworkConfig: *s.config,
// }
// }
// func (s *NetworkInterfaceState) RpcSetNetworkSettings(settings RpcNetworkSettings) error {
// currentSettings := s.config
// err := confparser.SetDefaultsAndValidate(&settings.NetworkConfig)
// if err != nil {
// return err
// }
// if IsSame(currentSettings, settings.NetworkConfig) {
// // no changes, do nothing
// return nil
// }
// s.config = &settings.NetworkConfig
// s.onConfigChange(s.config)
// return nil
// }
// func (s *NetworkInterfaceState) RpcRenewDHCPLease() error {
// if s.dhcpClient == nil {
// return fmt.Errorf("dhcp client not initialized")
// }
// return s.dhcpClient.Renew()
// }

View File

@ -0,0 +1,15 @@
package types
// DHCPClient is the interface for a DHCP client.
type DHCPClient interface {
Domain() string
Lease4() *DHCPLease
Lease6() *DHCPLease
Renew() error
Release() error
SetIPv4(enabled bool)
SetIPv6(enabled bool)
SetOnLeaseChange(callback func(lease *DHCPLease))
Start() error
Stop() error
}

View File

@ -1,26 +0,0 @@
package network
import (
"encoding/json"
"time"
)
func lifetimeToTime(lifetime int) *time.Time {
if lifetime == 0 {
return nil
}
t := time.Now().Add(time.Duration(lifetime) * time.Second)
return &t
}
func IsSame(a, b any) bool {
aJSON, err := json.Marshal(a)
if err != nil {
return false
}
bJSON, err := json.Marshal(b)
if err != nil {
return false
}
return string(aJSON) == string(bJSON)
}

View File

@ -3,6 +3,7 @@ package timesync
import ( import (
"context" "context"
"math/rand/v2" "math/rand/v2"
"net"
"strconv" "strconv"
"time" "time"
@ -37,7 +38,47 @@ var DefaultNTPServerHostnames = []string{
"pool.ntp.org", "pool.ntp.org",
} }
func (t *TimeSync) filterNTPServers(ntpServers []string) ([]string, error) {
if len(ntpServers) == 0 {
return nil, nil
}
hasIPv4, err := t.preCheckIPv4()
if err != nil {
t.l.Error().Err(err).Msg("failed to check IPv4")
return nil, err
}
hasIPv6, err := t.preCheckIPv6()
if err != nil {
t.l.Error().Err(err).Msg("failed to check IPv6")
return nil, err
}
filteredServers := []string{}
for _, server := range ntpServers {
ip := net.ParseIP(server)
t.l.Trace().Str("server", server).Interface("ip", ip).Msg("checking NTP server")
if ip == nil {
continue
}
if hasIPv4 && ip.To4() != nil {
filteredServers = append(filteredServers, server)
}
if hasIPv6 && ip.To16() != nil {
filteredServers = append(filteredServers, server)
}
}
return filteredServers, nil
}
func (t *TimeSync) queryNetworkTime(ntpServers []string) (now *time.Time, offset *time.Duration) { func (t *TimeSync) queryNetworkTime(ntpServers []string) (now *time.Time, offset *time.Duration) {
ntpServers, err := t.filterNTPServers(ntpServers)
if err != nil {
t.l.Error().Err(err).Msg("failed to filter NTP servers")
return nil, nil
}
chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4)) chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4))
t.l.Info().Strs("servers", ntpServers).Int("chunkSize", chunkSize).Msg("querying NTP servers") t.l.Info().Strs("servers", ntpServers).Int("chunkSize", chunkSize).Msg("querying NTP servers")

View File

@ -24,6 +24,8 @@ var (
timeSyncRetryInterval = 0 * time.Second timeSyncRetryInterval = 0 * time.Second
) )
type PreCheckFunc func() (bool, error)
type TimeSync struct { type TimeSync struct {
syncLock *sync.Mutex syncLock *sync.Mutex
l *zerolog.Logger l *zerolog.Logger
@ -37,11 +39,15 @@ type TimeSync struct {
syncSuccess bool syncSuccess bool
preCheckFunc func() (bool, error) preCheckFunc PreCheckFunc
preCheckIPv4 PreCheckFunc
preCheckIPv6 PreCheckFunc
} }
type TimeSyncOptions struct { type TimeSyncOptions struct {
PreCheckFunc func() (bool, error) PreCheckFunc PreCheckFunc
PreCheckIPv4 PreCheckFunc
PreCheckIPv6 PreCheckFunc
Logger *zerolog.Logger Logger *zerolog.Logger
NetworkConfig *types.NetworkConfig NetworkConfig *types.NetworkConfig
} }
@ -69,6 +75,8 @@ func NewTimeSync(opts *TimeSyncOptions) *TimeSync {
rtcDevicePath: rtcDevice, rtcDevicePath: rtcDevice,
rtcLock: &sync.Mutex{}, rtcLock: &sync.Mutex{},
preCheckFunc: opts.PreCheckFunc, preCheckFunc: opts.PreCheckFunc,
preCheckIPv4: opts.PreCheckIPv4,
preCheckIPv6: opts.PreCheckIPv6,
networkConfig: opts.NetworkConfig, networkConfig: opts.NetworkConfig,
} }
@ -112,7 +120,13 @@ func (t *TimeSync) getSyncMode() SyncMode {
} }
} }
t.l.Debug().Strs("Ordering", syncMode.Ordering).Bool("Ntp", syncMode.Ntp).Bool("Http", syncMode.Http).Bool("NtpUseFallback", syncMode.NtpUseFallback).Bool("HttpUseFallback", syncMode.HttpUseFallback).Msg("sync mode") t.l.Debug().
Strs("Ordering", syncMode.Ordering).
Bool("Ntp", syncMode.Ntp).
Bool("Http", syncMode.Http).
Bool("NtpUseFallback", syncMode.NtpUseFallback).
Bool("HttpUseFallback", syncMode.HttpUseFallback).
Msg("sync mode")
return syncMode return syncMode
} }

View File

@ -1,186 +0,0 @@
package udhcpc
import (
"bufio"
"encoding/json"
"fmt"
"net"
"os"
"reflect"
"strconv"
"strings"
"time"
)
type Lease struct {
// from https://udhcp.busybox.net/README.udhcpc
IPAddress net.IP `env:"ip" json:"ip"` // The obtained IP
Netmask net.IP `env:"subnet" json:"netmask"` // The assigned subnet mask
Broadcast net.IP `env:"broadcast" json:"broadcast"` // The broadcast address for this network
TTL int `env:"ipttl" json:"ttl,omitempty"` // The TTL to use for this network
MTU int `env:"mtu" json:"mtu,omitempty"` // The MTU to use for this network
HostName string `env:"hostname" json:"hostname,omitempty"` // The assigned hostname
Domain string `env:"domain" json:"domain,omitempty"` // The domain name of the network
BootPNextServer net.IP `env:"siaddr" json:"bootp_next_server,omitempty"` // The bootp next server option
BootPServerName string `env:"sname" json:"bootp_server_name,omitempty"` // The bootp server name option
BootPFile string `env:"boot_file" json:"bootp_file,omitempty"` // The bootp boot file option
Timezone string `env:"timezone" json:"timezone,omitempty"` // Offset in seconds from UTC
Routers []net.IP `env:"router" json:"routers,omitempty"` // A list of routers
DNS []net.IP `env:"dns" json:"dns_servers,omitempty"` // A list of DNS servers
NTPServers []net.IP `env:"ntpsrv" json:"ntp_servers,omitempty"` // A list of NTP servers
LPRServers []net.IP `env:"lprsvr" json:"lpr_servers,omitempty"` // A list of LPR servers
TimeServers []net.IP `env:"timesvr" json:"_time_servers,omitempty"` // A list of time servers (obsolete)
IEN116NameServers []net.IP `env:"namesvr" json:"_name_servers,omitempty"` // A list of IEN 116 name servers (obsolete)
LogServers []net.IP `env:"logsvr" json:"_log_servers,omitempty"` // A list of MIT-LCS UDP log servers (obsolete)
CookieServers []net.IP `env:"cookiesvr" json:"_cookie_servers,omitempty"` // A list of RFC 865 cookie servers (obsolete)
WINSServers []net.IP `env:"wins" json:"_wins_servers,omitempty"` // A list of WINS servers
SwapServer net.IP `env:"swapsvr" json:"_swap_server,omitempty"` // The IP address of the client's swap server
BootSize int `env:"bootsize" json:"bootsize,omitempty"` // The length in 512 octect blocks of the bootfile
RootPath string `env:"rootpath" json:"root_path,omitempty"` // The path name of the client's root disk
LeaseTime time.Duration `env:"lease" json:"lease,omitempty"` // The lease time, in seconds
DHCPType string `env:"dhcptype" json:"dhcp_type,omitempty"` // DHCP message type (safely ignored)
ServerID string `env:"serverid" json:"server_id,omitempty"` // The IP of the server
Message string `env:"message" json:"reason,omitempty"` // Reason for a DHCPNAK
TFTPServerName string `env:"tftp" json:"tftp,omitempty"` // The TFTP server name
BootFileName string `env:"bootfile" json:"bootfile,omitempty"` // The boot file name
Uptime time.Duration `env:"uptime" json:"uptime,omitempty"` // The uptime of the device when the lease was obtained, in seconds
LeaseExpiry *time.Time `json:"lease_expiry,omitempty"` // The expiry time of the lease
isEmpty map[string]bool
}
func (l *Lease) setIsEmpty(m map[string]bool) {
l.isEmpty = m
}
func (l *Lease) IsEmpty(key string) bool {
return l.isEmpty[key]
}
func (l *Lease) ToJSON() string {
json, err := json.Marshal(l)
if err != nil {
return ""
}
return string(json)
}
func (l *Lease) SetLeaseExpiry() (time.Time, error) {
if l.Uptime == 0 || l.LeaseTime == 0 {
return time.Time{}, fmt.Errorf("uptime or lease time isn't set")
}
// get the uptime of the device
file, err := os.Open("/proc/uptime")
if err != nil {
return time.Time{}, fmt.Errorf("failed to open uptime file: %w", err)
}
defer file.Close()
var uptime time.Duration
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := scanner.Text()
parts := strings.Split(text, " ")
uptime, err = time.ParseDuration(parts[0] + "s")
if err != nil {
return time.Time{}, fmt.Errorf("failed to parse uptime: %w", err)
}
}
relativeLeaseRemaining := (l.Uptime + l.LeaseTime) - uptime
leaseExpiry := time.Now().Add(relativeLeaseRemaining)
l.LeaseExpiry = &leaseExpiry
return leaseExpiry, nil
}
func UnmarshalDHCPCLease(lease *Lease, str string) error {
// parse the lease file as a map
data := make(map[string]string)
for line := range strings.SplitSeq(str, "\n") {
line = strings.TrimSpace(line)
// skip empty lines and comments
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
data[key] = value
}
// now iterate over the lease struct and set the values
leaseType := reflect.TypeOf(lease).Elem()
leaseValue := reflect.ValueOf(lease).Elem()
valuesParsed := make(map[string]bool)
for i := 0; i < leaseType.NumField(); i++ {
field := leaseValue.Field(i)
// get the env tag
key := leaseType.Field(i).Tag.Get("env")
if key == "" {
continue
}
valuesParsed[key] = false
// get the value from the data map
value, ok := data[key]
if !ok || value == "" {
continue
}
switch field.Interface().(type) {
case string:
field.SetString(value)
case int:
val, err := strconv.Atoi(value)
if err != nil {
continue
}
field.SetInt(int64(val))
case time.Duration:
val, err := time.ParseDuration(value + "s")
if err != nil {
continue
}
field.Set(reflect.ValueOf(val))
case net.IP:
ip := net.ParseIP(value)
if ip == nil {
continue
}
field.Set(reflect.ValueOf(ip))
case []net.IP:
val := make([]net.IP, 0)
for ipStr := range strings.FieldsSeq(value) {
ip := net.ParseIP(ipStr)
if ip == nil {
continue
}
val = append(val, ip)
}
field.Set(reflect.ValueOf(val))
default:
return fmt.Errorf("unsupported field `%s` type: %s", key, field.Type().String())
}
valuesParsed[key] = true
}
lease.setIsEmpty(valuesParsed)
return nil
}

View File

@ -6,7 +6,8 @@ import (
"fmt" "fmt"
"github.com/jetkvm/kvm/internal/network/types" "github.com/jetkvm/kvm/internal/network/types"
"github.com/jetkvm/kvm/pkg/nmlite/dhclient" "github.com/jetkvm/kvm/pkg/nmlite/jetdhcpc"
"github.com/jetkvm/kvm/pkg/nmlite/udhcpc"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/vishvananda/netlink" "github.com/vishvananda/netlink"
) )
@ -16,7 +17,7 @@ type DHCPClient struct {
ctx context.Context ctx context.Context
ifaceName string ifaceName string
logger *zerolog.Logger logger *zerolog.Logger
client *dhclient.Client client types.DHCPClient
link netlink.Link link netlink.Link
// Configuration // Configuration
@ -40,9 +41,6 @@ func NewDHCPClient(ctx context.Context, ifaceName string, logger *zerolog.Logger
return nil, fmt.Errorf("logger cannot be nil") return nil, fmt.Errorf("logger cannot be nil")
} }
// Create state manager
// stateManager := NewDHCPStateManager("", logger)
return &DHCPClient{ return &DHCPClient{
ctx: ctx, ctx: ctx,
ifaceName: ifaceName, ifaceName: ifaceName,
@ -71,23 +69,22 @@ func (dc *DHCPClient) SetOnLeaseChange(callback func(lease *types.DHCPLease)) {
dc.onLeaseChange = callback dc.onLeaseChange = callback
} }
// Start starts the DHCP client func (dc *DHCPClient) initClient() (types.DHCPClient, error) {
func (dc *DHCPClient) Start() error { if false {
if dc.client != nil { return dc.initJetDHCPC()
dc.logger.Warn().Msg("DHCP client already started") } else {
return nil return dc.initUDHCPC()
}
} }
dc.logger.Info().Msg("starting DHCP client") func (dc *DHCPClient) initJetDHCPC() (types.DHCPClient, error) {
return jetdhcpc.NewClient(dc.ctx, []string{dc.ifaceName}, &jetdhcpc.Config{
// Create the underlying DHCP client
client, err := dhclient.NewClient(dc.ctx, []string{dc.ifaceName}, &dhclient.Config{
IPv4: dc.ipv4Enabled, IPv4: dc.ipv4Enabled,
IPv6: dc.ipv6Enabled, IPv6: dc.ipv6Enabled,
OnLease4Change: func(lease *dhclient.Lease) { OnLease4Change: func(lease *types.DHCPLease) {
dc.handleLeaseChange(lease, false) dc.handleLeaseChange(lease, false)
}, },
OnLease6Change: func(lease *dhclient.Lease) { OnLease6Change: func(lease *types.DHCPLease) {
dc.handleLeaseChange(lease, true) dc.handleLeaseChange(lease, true)
}, },
UpdateResolvConf: func(nameservers []string) error { UpdateResolvConf: func(nameservers []string) error {
@ -98,6 +95,31 @@ func (dc *DHCPClient) Start() error {
return nil return nil
}, },
}, dc.logger) }, dc.logger)
}
func (dc *DHCPClient) initUDHCPC() (types.DHCPClient, error) {
c := udhcpc.NewDHCPClient(&udhcpc.DHCPClientOptions{
InterfaceName: dc.ifaceName,
PidFile: "",
Logger: dc.logger,
OnLeaseChange: func(lease *types.DHCPLease) {
dc.handleLeaseChange(lease, false)
},
})
return c, nil
}
// Start starts the DHCP client
func (dc *DHCPClient) Start() error {
if dc.client != nil {
dc.logger.Warn().Msg("DHCP client already started")
return nil
}
dc.logger.Info().Msg("starting DHCP client")
// Create the underlying DHCP client
client, err := dc.initClient()
if err != nil { if err != nil {
return fmt.Errorf("failed to create DHCP client: %w", err) return fmt.Errorf("failed to create DHCP client: %w", err)
@ -115,6 +137,27 @@ func (dc *DHCPClient) Start() error {
return nil return nil
} }
func (dc *DHCPClient) Domain() string {
if dc.client == nil {
return ""
}
return dc.client.Domain()
}
func (dc *DHCPClient) Lease4() *types.DHCPLease {
if dc.client == nil {
return nil
}
return dc.client.Lease4()
}
func (dc *DHCPClient) Lease6() *types.DHCPLease {
if dc.client == nil {
return nil
}
return dc.client.Lease6()
}
// Stop stops the DHCP client // Stop stops the DHCP client
func (dc *DHCPClient) Stop() error { func (dc *DHCPClient) Stop() error {
if dc.client == nil { if dc.client == nil {
@ -150,62 +193,19 @@ func (dc *DHCPClient) Release() error {
return nil return nil
} }
// GetLease4 returns the current IPv4 lease
func (dc *DHCPClient) GetLease4() *types.DHCPLease {
if dc.client == nil {
return nil
}
lease := dc.client.Lease4()
if lease == nil {
return nil
}
return dc.convertLease(lease, false)
}
// GetLease6 returns the current IPv6 lease
func (dc *DHCPClient) GetLease6() *types.DHCPLease {
if dc.client == nil {
return nil
}
lease := dc.client.Lease6()
if lease == nil {
return nil
}
return dc.convertLease(lease, true)
}
// handleLeaseChange handles lease changes from the underlying DHCP client // handleLeaseChange handles lease changes from the underlying DHCP client
func (dc *DHCPClient) handleLeaseChange(lease *dhclient.Lease, isIPv6 bool) { func (dc *DHCPClient) handleLeaseChange(lease *types.DHCPLease, isIPv6 bool) {
if lease == nil { if lease == nil {
return return
} }
convertedLease := dc.convertLease(lease, isIPv6)
if convertedLease == nil {
dc.logger.Error().Msg("failed to convert lease")
return
}
dc.logger.Info(). dc.logger.Info().
Bool("ipv6", isIPv6). Bool("ipv6", isIPv6).
Str("ip", convertedLease.IPAddress.String()). Str("ip", lease.IPAddress.String()).
Msg("DHCP lease changed") Msg("DHCP lease changed")
// Notify callback // Notify callback
if dc.onLeaseChange != nil { if dc.onLeaseChange != nil {
dc.onLeaseChange(convertedLease) dc.onLeaseChange(lease)
} }
} }
// convertLease converts a dhclient.Lease to types.DHCPLease
func (dc *DHCPClient) convertLease(lease *dhclient.Lease, isIPv6 bool) *types.DHCPLease {
if lease == nil {
return nil
}
return lease.ToDHCPLease()
}

View File

@ -22,6 +22,7 @@ type InterfaceManager struct {
config *types.NetworkConfig config *types.NetworkConfig
logger *zerolog.Logger logger *zerolog.Logger
state *types.InterfaceState state *types.InterfaceState
linkState *link.Link
stateMu sync.RWMutex stateMu sync.RWMutex
// Network components // Network components
@ -107,6 +108,14 @@ func (im *InterfaceManager) Start() error {
im.wg.Add(1) im.wg.Add(1)
go im.monitorInterfaceState() go im.monitorInterfaceState()
nl := getNetlinkManager()
nl.AddLinkStateCallback(im.ifaceName, link.LinkStateCallback{
Async: true,
Func: func(link *link.Link) {
im.handleLinkStateChange(link)
},
})
// Apply initial configuration // Apply initial configuration
if err := im.applyConfiguration(); err != nil { if err := im.applyConfiguration(); err != nil {
im.logger.Error().Err(err).Msg("failed to apply initial configuration") im.logger.Error().Err(err).Msg("failed to apply initial configuration")
@ -143,22 +152,63 @@ func (im *InterfaceManager) link() (*link.Link, error) {
// IsUp returns true if the interface is up // IsUp returns true if the interface is up
func (im *InterfaceManager) IsUp() bool { func (im *InterfaceManager) IsUp() bool {
im.stateMu.RLock()
defer im.stateMu.RUnlock()
return im.state.Up return im.state.Up
} }
func (im *InterfaceManager) IsOnline() bool { func (im *InterfaceManager) IsOnline() bool {
return im.IsUp() im.stateMu.RLock()
defer im.stateMu.RUnlock()
return im.state.Online
}
func (im *InterfaceManager) IPv4Ready() bool {
im.stateMu.RLock()
defer im.stateMu.RUnlock()
return im.state.IPv4Ready
}
func (im *InterfaceManager) IPv6Ready() bool {
im.stateMu.RLock()
defer im.stateMu.RUnlock()
return im.state.IPv6Ready
} }
func (im *InterfaceManager) GetIPv4Addresses() []string { func (im *InterfaceManager) GetIPv4Addresses() []string {
im.stateMu.RLock()
defer im.stateMu.RUnlock()
return im.state.IPv4Addresses return im.state.IPv4Addresses
} }
func (im *InterfaceManager) GetIPv4Address() string {
im.stateMu.RLock()
defer im.stateMu.RUnlock()
return im.state.IPv4Address
}
func (im *InterfaceManager) GetIPv6Address() string {
im.stateMu.RLock()
defer im.stateMu.RUnlock()
return im.state.IPv6Address
}
func (im *InterfaceManager) GetIPv6Addresses() []string { func (im *InterfaceManager) GetIPv6Addresses() []string {
im.stateMu.RLock()
defer im.stateMu.RUnlock()
addresses := []string{} addresses := []string{}
for _, addr := range im.state.IPv6Addresses { for _, addr := range im.state.IPv6Addresses {
addresses = append(addresses, addr.Address.String()) addresses = append(addresses, addr.Address.String())
} }
return []string{} return []string{}
} }
@ -189,6 +239,10 @@ func (im *InterfaceManager) GetConfig() *types.NetworkConfig {
return &config return &config
} }
func (im *InterfaceManager) ApplyConfiguration() error {
return im.applyConfiguration()
}
// SetConfig updates the interface configuration // SetConfig updates the interface configuration
func (im *InterfaceManager) SetConfig(config *types.NetworkConfig) error { func (im *InterfaceManager) SetConfig(config *types.NetworkConfig) error {
if config == nil { if config == nil {
@ -446,7 +500,7 @@ func (im *InterfaceManager) getDomain() string {
// Try to get domain from DHCP lease // Try to get domain from DHCP lease
if im.dhcpClient != nil { if im.dhcpClient != nil {
if lease := im.dhcpClient.GetLease4(); lease != nil && lease.Domain != "" { if lease := im.dhcpClient.Lease4(); lease != nil && lease.Domain != "" {
return lease.Domain return lease.Domain
} }
} }
@ -454,6 +508,55 @@ func (im *InterfaceManager) getDomain() string {
return "local" return "local"
} }
func (im *InterfaceManager) handleLinkStateChange(link *link.Link) {
{
im.stateMu.Lock()
defer im.stateMu.Unlock()
if link.IsSame(im.linkState) {
return
}
im.linkState = link
}
im.logger.Info().Interface("link", link).Msg("link state changed")
operState := link.Attrs().OperState
if operState == netlink.OperUp {
im.handleLinkUp()
} else {
im.handleLinkDown()
}
}
func (im *InterfaceManager) handleLinkUp() {
im.logger.Info().Msg("link up")
im.applyConfiguration()
if im.config.IPv4Mode.String == "dhcp" {
im.dhcpClient.Renew()
}
}
func (im *InterfaceManager) handleLinkDown() {
im.logger.Info().Msg("link down")
if im.config.IPv4Mode.String == "dhcp" {
im.dhcpClient.Stop()
}
netlinkMgr := getNetlinkManager()
if err := netlinkMgr.RemoveAllAddresses(im.linkState, link.AfInet); err != nil {
im.logger.Error().Err(err).Msg("failed to remove all IPv4 addresses")
}
if err := netlinkMgr.RemoveNonLinkLocalIPv6Addresses(im.linkState); err != nil {
im.logger.Error().Err(err).Msg("failed to remove non-link-local IPv6 addresses")
}
}
// monitorInterfaceState monitors the interface state and updates accordingly // monitorInterfaceState monitors the interface state and updates accordingly
func (im *InterfaceManager) monitorInterfaceState() { func (im *InterfaceManager) monitorInterfaceState() {
defer im.wg.Done() defer im.wg.Done()
@ -477,6 +580,7 @@ func (im *InterfaceManager) monitorInterfaceState() {
} }
} }
} }
} }
// updateInterfaceState updates the current interface state // updateInterfaceState updates the current interface state
@ -498,11 +602,11 @@ func (im *InterfaceManager) updateInterfaceState() error {
hasAddrs = true hasAddrs = true
} }
im.stateMu.Lock()
defer im.stateMu.Unlock()
// Check if state changed // Check if state changed
stateChanged := false stateChanged := false
// We should release the lock before calling the callbacks
// to avoid deadlocks
im.stateMu.Lock()
if im.state.Up != isUp { if im.state.Up != isUp {
im.state.Up = isUp im.state.Up = isUp
stateChanged = true stateChanged = true
@ -522,7 +626,8 @@ func (im *InterfaceManager) updateInterfaceState() error {
im.logger.Error().Err(err).Msg("failed to update IP addresses") im.logger.Error().Err(err).Msg("failed to update IP addresses")
} }
// im.state.LastUpdated = time.Now() // TODO: remove this im.state.LastUpdated = time.Now()
im.stateMu.Unlock()
// Notify callback if state changed // Notify callback if state changed
if stateChanged && im.onStateChange != nil { if stateChanged && im.onStateChange != nil {
@ -541,10 +646,13 @@ func (im *InterfaceManager) updateIPAddresses(nl *link.Link) error {
return fmt.Errorf("failed to get addresses: %w", err) return fmt.Errorf("failed to get addresses: %w", err)
} }
var ipv4Addresses []string var (
var ipv6Addresses []types.IPv6Address ipv4Addresses []string
var ipv4Addr, ipv6Addr string ipv6Addresses []types.IPv6Address
var ipv6LinkLocal string ipv4Addr, ipv6Addr string
ipv6LinkLocal string
ipv4Ready, ipv6Ready = false, false
)
for _, addr := range addrs { for _, addr := range addrs {
im.logger.Debug().Str("address", addr.IP.String()).Msg("checking address") im.logger.Debug().Str("address", addr.IP.String()).Msg("checking address")
@ -553,6 +661,7 @@ func (im *InterfaceManager) updateIPAddresses(nl *link.Link) error {
ipv4Addresses = append(ipv4Addresses, addr.IPNet.String()) ipv4Addresses = append(ipv4Addresses, addr.IPNet.String())
if ipv4Addr == "" { if ipv4Addr == "" {
ipv4Addr = addr.IP.String() ipv4Addr = addr.IP.String()
ipv4Ready = true
} }
} else if addr.IP.To16() != nil { } else if addr.IP.To16() != nil {
// IPv6 address // IPv6 address
@ -566,6 +675,7 @@ func (im *InterfaceManager) updateIPAddresses(nl *link.Link) error {
}) })
if ipv6Addr == "" { if ipv6Addr == "" {
ipv6Addr = addr.IP.String() ipv6Addr = addr.IP.String()
ipv6Ready = true
} }
} }
} }
@ -574,6 +684,10 @@ func (im *InterfaceManager) updateIPAddresses(nl *link.Link) error {
im.state.IPv4Addresses = ipv4Addresses im.state.IPv4Addresses = ipv4Addresses
im.state.IPv6Addresses = ipv6Addresses im.state.IPv6Addresses = ipv6Addresses
im.state.IPv6LinkLocal = ipv6LinkLocal im.state.IPv6LinkLocal = ipv6LinkLocal
im.state.IPv4Address = ipv4Addr
im.state.IPv6Address = ipv6Addr
im.state.IPv4Ready = ipv4Ready
im.state.IPv6Ready = ipv6Ready
return nil return nil
} }
@ -581,9 +695,8 @@ func (im *InterfaceManager) updateIPAddresses(nl *link.Link) error {
// updateStateFromDHCPLease updates the state from a DHCP lease // updateStateFromDHCPLease updates the state from a DHCP lease
func (im *InterfaceManager) updateStateFromDHCPLease(lease *types.DHCPLease) { func (im *InterfaceManager) updateStateFromDHCPLease(lease *types.DHCPLease) {
im.stateMu.Lock() im.stateMu.Lock()
defer im.stateMu.Unlock()
im.state.DHCPLease4 = lease im.state.DHCPLease4 = lease
im.stateMu.Unlock()
// Update resolv.conf with DNS information // Update resolv.conf with DNS information
if im.resolvConf != nil { if im.resolvConf != nil {
@ -614,10 +727,12 @@ func (im *InterfaceManager) applyDHCPLease(lease *types.DHCPLease) error {
// convertDHCPLeaseToIPv4Config converts a DHCP lease to IPv4Config // convertDHCPLeaseToIPv4Config converts a DHCP lease to IPv4Config
func (im *InterfaceManager) convertDHCPLeaseToIPv4Config(lease *types.DHCPLease) *types.IPAddress { func (im *InterfaceManager) convertDHCPLeaseToIPv4Config(lease *types.DHCPLease) *types.IPAddress {
mask := lease.Netmask
// Create IPNet from IP and netmask // Create IPNet from IP and netmask
ipNet := &net.IPNet{ ipNet := &net.IPNet{
IP: lease.IPAddress, IP: lease.IPAddress,
Mask: net.IPMask(lease.Netmask), Mask: net.IPv4Mask(mask[12], mask[13], mask[14], mask[15]),
} }
// Create IPv4Address // Create IPv4Address
@ -628,7 +743,10 @@ func (im *InterfaceManager) convertDHCPLeaseToIPv4Config(lease *types.DHCPLease)
Permanent: false, Permanent: false,
} }
im.logger.Trace().Interface("ipv4Addr", ipv4Addr).Msg("converted DHCP lease to IPv4Config") im.logger.Trace().
Interface("ipv4Addr", ipv4Addr).
Interface("lease", lease).
Msg("converted DHCP lease to IPv4Config")
// Create IPv4Config // Create IPv4Config
return &ipv4Addr return &ipv4Addr

View File

@ -1,4 +1,4 @@
package dhclient package jetdhcpc
import ( import (
"context" "context"
@ -12,6 +12,7 @@ import (
"github.com/go-co-op/gocron/v2" "github.com/go-co-op/gocron/v2"
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/dhcpv6"
"github.com/jetkvm/kvm/internal/network/types"
"github.com/jetkvm/kvm/pkg/nmlite/link" "github.com/jetkvm/kvm/pkg/nmlite/link"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
@ -27,7 +28,7 @@ var (
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 LeaseChangeHandler func(lease *Lease) type LeaseChangeHandler func(lease *types.DHCPLease)
// Config is a DHCP client configuration. // Config is a DHCP client configuration.
type Config struct { type Config struct {
@ -81,6 +82,7 @@ type Config struct {
} }
type Client struct { type Client struct {
types.DHCPClient
ifaces []string ifaces []string
cfg Config cfg Config
l *zerolog.Logger l *zerolog.Logger
@ -153,6 +155,34 @@ func (c *Client) sendInitialRequests() chan interface{} {
return c.sendRequests(c.cfg.IPv4, c.cfg.IPv6) return c.sendRequests(c.cfg.IPv4, c.cfg.IPv6)
} }
func (c *Client) sendRequestsFamily(
family int,
wg *sync.WaitGroup,
r *chan interface{},
l *zerolog.Logger,
iface *link.Link,
) {
wg.Add(1)
go func(iface *link.Link) {
defer wg.Done()
var (
lease *Lease
err error
)
switch family {
case link.AfInet:
lease, err = c.requestLease4(iface)
case link.AfInet6:
lease, err = c.requestLease6(iface)
}
if err != nil {
l.Error().Err(err).Msg("Could not get lease")
return
}
(*r) <- lease
}(iface)
}
func (c *Client) sendRequests(ipv4, ipv6 bool) chan interface{} { func (c *Client) sendRequests(ipv4, ipv6 bool) chan interface{} {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
@ -175,30 +205,11 @@ func (c *Client) sendRequests(ipv4, ipv6 bool) chan interface{} {
} }
if ipv4 { if ipv4 {
wg.Add(1) c.sendRequestsFamily(link.AfInet, &wg, &r, &l, iface)
go func(iface *link.Link) {
defer wg.Done()
lease, err := c.requestLease4(iface)
if err != nil {
l.Error().Err(err).Msg("Could not get IPv4 lease")
return
}
r <- lease
}(iface)
} }
if ipv6 { if ipv6 {
return // TODO: implement DHCP6 c.sendRequestsFamily(link.AfInet6, &wg, &r, &l, iface)
wg.Add(1)
go func(iface *link.Link) {
defer wg.Done()
lease, err := c.requestLease6(iface)
if err != nil {
l.Error().Err(err).Msg("Could not get IPv6 lease")
return
}
r <- lease
}(iface)
} }
}(iface) }(iface)
} }
@ -210,18 +221,26 @@ func (c *Client) sendRequests(ipv4, ipv6 bool) chan interface{} {
return r return r
} }
func (c *Client) Lease4() *Lease { func (c *Client) Lease4() *types.DHCPLease {
c.lease4Mu.Lock() c.lease4Mu.Lock()
defer c.lease4Mu.Unlock() defer c.lease4Mu.Unlock()
return c.currentLease4 if c.currentLease4 == nil {
return nil
} }
func (c *Client) Lease6() *Lease { return c.currentLease4.ToDHCPLease()
}
func (c *Client) Lease6() *types.DHCPLease {
c.lease6Mu.Lock() c.lease6Mu.Lock()
defer c.lease6Mu.Unlock() defer c.lease6Mu.Unlock()
return c.currentLease6 if c.currentLease6 == nil {
return nil
}
return c.currentLease6.ToDHCPLease()
} }
func (c *Client) Domain() string { func (c *Client) Domain() string {
@ -288,11 +307,11 @@ func (c *Client) handleLeaseChange(lease *Lease) {
// TODO: handle lease expiration // TODO: handle lease expiration
if c.cfg.OnLease4Change != nil && ipv4 { if c.cfg.OnLease4Change != nil && ipv4 {
c.cfg.OnLease4Change(lease) c.cfg.OnLease4Change(lease.ToDHCPLease())
} }
if c.cfg.OnLease6Change != nil && !ipv4 { if c.cfg.OnLease6Change != nil && !ipv4 {
c.cfg.OnLease6Change(lease) c.cfg.OnLease6Change(lease.ToDHCPLease())
} }
} }
@ -304,12 +323,14 @@ func (c *Client) renew() {
} }
} }
func (c *Client) Renew() { func (c *Client) Renew() error {
go c.renew() go c.renew()
return nil
} }
func (c *Client) Release() { func (c *Client) Release() error {
// TODO: implement // TODO: implement
return nil
} }
func (c *Client) SetIPv4(ipv4 bool) { func (c *Client) SetIPv4(ipv4 bool) {

View File

@ -1,4 +1,4 @@
package dhclient package jetdhcpc
import ( import (
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"

View File

@ -1,18 +1,18 @@
package dhclient package jetdhcpc
import ( import (
"log"
"net" "net"
"time" "time"
"github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/dhcpv6"
"github.com/insomniacslk/dhcp/dhcpv6/nclient6" "github.com/insomniacslk/dhcp/dhcpv6/nclient6"
"github.com/rs/zerolog"
"github.com/vishvananda/netlink" "github.com/vishvananda/netlink"
) )
// isIPv6LinkReady returns true if the interface has a link-local address // isIPv6LinkReady returns true if the interface has a link-local address
// which is not tentative. // which is not tentative.
func isIPv6LinkReady(l netlink.Link) (bool, error) { func isIPv6LinkReady(l netlink.Link, logger *zerolog.Logger) (bool, error) {
addrs, err := netlink.AddrList(l, 10) // AF_INET6 addrs, err := netlink.AddrList(l, 10) // AF_INET6
if err != nil { if err != nil {
return false, err return false, err
@ -20,7 +20,7 @@ func isIPv6LinkReady(l netlink.Link) (bool, error) {
for _, addr := range addrs { for _, addr := range addrs {
if addr.IP.IsLinkLocalUnicast() && (addr.Flags&0x40 == 0) { // IFA_F_TENTATIVE if addr.IP.IsLinkLocalUnicast() && (addr.Flags&0x40 == 0) { // IFA_F_TENTATIVE
if addr.Flags&0x80 != 0 { // IFA_F_DADFAILED if addr.Flags&0x80 != 0 { // IFA_F_DADFAILED
log.Printf("DADFAILED for %v, continuing anyhow", addr.IP) logger.Warn().Str("address", addr.IP.String()).Msg("DADFAILED for address, continuing anyhow")
} }
return true, nil return true, nil
} }
@ -30,7 +30,7 @@ func isIPv6LinkReady(l netlink.Link) (bool, error) {
// isIPv6RouteReady returns true if serverAddr is reachable. // isIPv6RouteReady returns true if serverAddr is reachable.
func isIPv6RouteReady(serverAddr net.IP) waitForCondition { func isIPv6RouteReady(serverAddr net.IP) waitForCondition {
return func(l netlink.Link) (bool, error) { return func(l netlink.Link, logger *zerolog.Logger) (bool, error) {
if serverAddr.IsMulticast() { if serverAddr.IsMulticast() {
return true, nil return true, nil
} }

View File

@ -1,4 +1,4 @@
package dhclient package jetdhcpc
import ( import (
"bufio" "bufio"

View File

@ -1,4 +1,4 @@
package dhclient package jetdhcpc
import ( import (
"bytes" "bytes"

View File

@ -1,4 +1,4 @@
package dhclient package jetdhcpc
import ( import (
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"

View File

@ -1,5 +1,4 @@
// Package nmlite provides DHCP state persistence for the network manager. package jetdhcpc
package dhclient
import ( import (
"encoding/json" "encoding/json"

View File

@ -1,13 +1,14 @@
package dhclient package jetdhcpc
import ( import (
"context" "context"
"time" "time"
"github.com/rs/zerolog"
"github.com/vishvananda/netlink" "github.com/vishvananda/netlink"
) )
type waitForCondition func(l netlink.Link) (ready bool, err error) type waitForCondition func(l netlink.Link, logger *zerolog.Logger) (ready bool, err error)
func (c *Client) waitFor( func (c *Client) waitFor(
link netlink.Link, link netlink.Link,
@ -15,18 +16,19 @@ func (c *Client) waitFor(
condition waitForCondition, condition waitForCondition,
timeoutError error, timeoutError error,
) error { ) error {
return waitFor(c.ctx, link, timeout, condition, timeoutError) return waitFor(c.ctx, link, c.l, timeout, condition, timeoutError)
} }
func waitFor( func waitFor(
ctx context.Context, ctx context.Context,
link netlink.Link, link netlink.Link,
logger *zerolog.Logger,
timeout <-chan time.Time, timeout <-chan time.Time,
condition waitForCondition, condition waitForCondition,
timeoutError error, timeoutError error,
) error { ) error {
for { for {
if ready, err := condition(link); err != nil { if ready, err := condition(link, logger); err != nil {
return err return err
} else if ready { } else if ready {
break break

View File

@ -50,10 +50,18 @@ var (
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 // NetlinkManager provides centralized netlink operations
type NetlinkManager struct { type NetlinkManager struct {
logger *zerolog.Logger logger *zerolog.Logger
linkStateCh chan netlink.LinkUpdate
mu sync.RWMutex mu sync.RWMutex
linkStateCallbacks map[string][]LinkStateCallback
} }
// Link is a wrapper around netlink.Link // Link is a wrapper around netlink.Link
@ -70,12 +78,44 @@ func (l *Link) AddrList(family int) ([]netlink.Addr, error) {
return netlink.AddrList(l, family) return netlink.AddrList(l, family)
} }
func (l *Link) IsSame(other *Link) bool {
if l == nil || other == nil {
return false
}
a := l.Attrs()
b := other.Attrs()
if a.OperState != b.OperState {
return false
}
if a.Index != b.Index {
return false
}
if a.MTU != b.MTU {
return false
}
if a.HardwareAddr.String() != b.HardwareAddr.String() {
return false
}
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 // GetNetlinkManager returns the singleton NetlinkManager instance
func GetNetlinkManager() *NetlinkManager { func GetNetlinkManager() *NetlinkManager {
netlinkManagerOnce.Do(func() { netlinkManagerOnce.Do(func() {
netlinkManagerInstance = &NetlinkManager{ netlinkManagerInstance = newNetlinkManager(nil)
logger: &zerolog.Logger{}, // Default no-op logger
}
}) })
return netlinkManagerInstance return netlinkManagerInstance
} }
@ -83,18 +123,56 @@ func GetNetlinkManager() *NetlinkManager {
// InitializeNetlinkManager initializes the singleton NetlinkManager with a logger // InitializeNetlinkManager initializes the singleton NetlinkManager with a logger
func InitializeNetlinkManager(logger *zerolog.Logger) *NetlinkManager { func InitializeNetlinkManager(logger *zerolog.Logger) *NetlinkManager {
netlinkManagerOnce.Do(func() { netlinkManagerOnce.Do(func() {
if logger == nil { netlinkManagerInstance = newNetlinkManager(logger)
// Create a no-op logger if none provided
logger = &zerolog.Logger{}
}
netlinkManagerInstance = &NetlinkManager{
logger: logger,
}
}) })
return netlinkManagerInstance 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 // 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 // GetLinkByName gets a network link by name
func (nm *NetlinkManager) GetLinkByName(name string) (*Link, error) { func (nm *NetlinkManager) GetLinkByName(name string) (*Link, error) {

View File

@ -0,0 +1,35 @@
package nmlite
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
)
func TestToResolvConf(t *testing.T) {
rc, err := ResolvConfManager{}.generateResolvConf(
"eth0",
[]net.IP{
net.ParseIP("198.51.100.53"),
net.ParseIP("203.0.113.53"),
},
[]string{"example.com"},
"example.com",
)
if err != nil {
t.Fatal(err)
}
want := `# the resolv.conf file is managed by the jetkvm network manager
# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
search example.com # eth0
domain example.com # eth0
nameserver 198.51.100.53 # eth0
nameserver 203.0.113.53 # eth0
`
assert.Equal(t, want, rc.String())
}

View File

@ -12,7 +12,12 @@ func (nm *NetworkManager) IsOnline() bool {
} }
func (nm *NetworkManager) IsUp() bool { func (nm *NetworkManager) IsUp() bool {
return nm.IsOnline() for _, iface := range nm.interfaces {
if iface.IsUp() {
return true
}
}
return false
} }
func (nm *NetworkManager) GetHostname() string { func (nm *NetworkManager) GetHostname() string {
@ -46,6 +51,20 @@ func (nm *NetworkManager) GetIPv4Addresses() []string {
return []string{} return []string{}
} }
func (nm *NetworkManager) GetIPv4Address() string {
for _, iface := range nm.interfaces {
return iface.GetIPv4Address()
}
return ""
}
func (nm *NetworkManager) GetIPv6Address() string {
for _, iface := range nm.interfaces {
return iface.GetIPv6Address()
}
return ""
}
func (nm *NetworkManager) GetIPv6Addresses() []string { func (nm *NetworkManager) GetIPv6Addresses() []string {
for _, iface := range nm.interfaces { for _, iface := range nm.interfaces {
return iface.GetIPv6Addresses() return iface.GetIPv6Addresses()
@ -60,20 +79,26 @@ func (nm *NetworkManager) GetMACAddress() string {
return "" return ""
} }
func (nm *NetworkManager) IPv4String() string { func (nm *NetworkManager) IPv4Ready() bool {
l := nm.GetIPv4Addresses() for _, iface := range nm.interfaces {
if len(l) == 0 { return iface.IPv4Ready()
return ""
} }
return l[0] return false
}
func (nm *NetworkManager) IPv6Ready() bool {
for _, iface := range nm.interfaces {
return iface.IPv6Ready()
}
return false
}
func (nm *NetworkManager) IPv4String() string {
return nm.GetIPv4Address()
} }
func (nm *NetworkManager) IPv6String() string { func (nm *NetworkManager) IPv6String() string {
l := nm.GetIPv6Addresses() return nm.GetIPv6Address()
if len(l) == 0 {
return ""
}
return l[0]
} }
func (nm *NetworkManager) MACString() string { func (nm *NetworkManager) MACString() string {

169
pkg/nmlite/udhcpc/parser.go Normal file
View File

@ -0,0 +1,169 @@
package udhcpc
import (
"bufio"
"encoding/json"
"fmt"
"net"
"os"
"reflect"
"strconv"
"strings"
"time"
"github.com/jetkvm/kvm/internal/network/types"
)
type Lease struct {
types.DHCPLease
// from https://udhcp.busybox.net/README.udhcpc
isEmpty map[string]bool
}
func (l *Lease) setIsEmpty(m map[string]bool) {
l.isEmpty = m
}
// IsEmpty returns true if the lease is empty for the given key.
func (l *Lease) IsEmpty(key string) bool {
return l.isEmpty[key]
}
// ToJSON returns the lease as a JSON string.
func (l *Lease) ToJSON() string {
json, err := json.Marshal(l)
if err != nil {
return ""
}
return string(json)
}
// ToDHCPLease converts a lease to a DHCP lease.
func (l *Lease) ToDHCPLease() *types.DHCPLease {
return &l.DHCPLease
}
// SetLeaseExpiry sets the lease expiry time.
func (l *Lease) SetLeaseExpiry() (time.Time, error) {
if l.Uptime == 0 || l.LeaseTime == 0 {
return time.Time{}, fmt.Errorf("uptime or lease time isn't set")
}
// get the uptime of the device
file, err := os.Open("/proc/uptime")
if err != nil {
return time.Time{}, fmt.Errorf("failed to open uptime file: %w", err)
}
defer file.Close()
var uptime time.Duration
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := scanner.Text()
parts := strings.Split(text, " ")
uptime, err = time.ParseDuration(parts[0] + "s")
if err != nil {
return time.Time{}, fmt.Errorf("failed to parse uptime: %w", err)
}
}
relativeLeaseRemaining := (l.Uptime + l.LeaseTime) - uptime
leaseExpiry := time.Now().Add(relativeLeaseRemaining)
l.LeaseExpiry = &leaseExpiry
return leaseExpiry, nil
}
// UnmarshalDHCPCLease unmarshals a lease from a string.
func UnmarshalDHCPCLease(obj *Lease, str string) error {
lease := &obj.DHCPLease
// parse the lease file as a map
data := make(map[string]string)
for line := range strings.SplitSeq(str, "\n") {
line = strings.TrimSpace(line)
// skip empty lines and comments
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
data[key] = value
}
// now iterate over the lease struct and set the values
leaseType := reflect.TypeOf(lease).Elem()
leaseValue := reflect.ValueOf(lease).Elem()
valuesParsed := make(map[string]bool)
for i := 0; i < leaseType.NumField(); i++ {
field := leaseValue.Field(i)
// get the env tag
key := leaseType.Field(i).Tag.Get("env")
if key == "" {
continue
}
valuesParsed[key] = false
// get the value from the data map
value, ok := data[key]
if !ok || value == "" {
continue
}
switch field.Interface().(type) {
case string:
field.SetString(value)
case int:
val, err := strconv.Atoi(value)
if err != nil {
continue
}
field.SetInt(int64(val))
case time.Duration:
val, err := time.ParseDuration(value + "s")
if err != nil {
continue
}
field.Set(reflect.ValueOf(val))
case net.IP:
ip := net.ParseIP(value)
if ip == nil {
continue
}
field.Set(reflect.ValueOf(ip))
case []net.IP:
val := make([]net.IP, 0)
for ipStr := range strings.FieldsSeq(value) {
ip := net.ParseIP(ipStr)
if ip == nil {
continue
}
val = append(val, ip)
}
field.Set(reflect.ValueOf(val))
default:
return fmt.Errorf("unsupported field `%s` type: %s", key, field.Type().String())
}
valuesParsed[key] = true
}
obj.setIsEmpty(valuesParsed)
return nil
}

View File

@ -6,9 +6,11 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"sync"
"time" "time"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"github.com/jetkvm/kvm/internal/network/types"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
@ -18,20 +20,22 @@ const (
) )
type DHCPClient struct { type DHCPClient struct {
types.DHCPClient
InterfaceName string InterfaceName string
leaseFile string leaseFile string
pidFile string pidFile string
lease *Lease lease *Lease
logger *zerolog.Logger logger *zerolog.Logger
process *os.Process process *os.Process
onLeaseChange func(lease *Lease) runOnce sync.Once
onLeaseChange func(lease *types.DHCPLease)
} }
type DHCPClientOptions struct { type DHCPClientOptions struct {
InterfaceName string InterfaceName string
PidFile string PidFile string
Logger *zerolog.Logger Logger *zerolog.Logger
OnLeaseChange func(lease *Lease) OnLeaseChange func(lease *types.DHCPLease)
} }
var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel) var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel)
@ -67,8 +71,8 @@ func (c *DHCPClient) getWatchPaths() []string {
} }
// Run starts the DHCP client and watches the lease file for changes. // Run starts the DHCP client and watches the lease file for changes.
// this isn't a blocking call, and the lease file is reloaded when a change is detected. // this is a blocking call.
func (c *DHCPClient) Run() error { func (c *DHCPClient) run() error {
err := c.loadLeaseFile() err := c.loadLeaseFile()
if err != nil && !errors.Is(err, os.ErrNotExist) { if err != nil && !errors.Is(err, os.ErrNotExist) {
return err return err
@ -125,7 +129,7 @@ func (c *DHCPClient) Run() error {
// c.logger.Error().Msg("udhcpc process not found") // c.logger.Error().Msg("udhcpc process not found")
// } // }
// block the goroutine until the lease file is updated // block the goroutine
<-make(chan struct{}) <-make(chan struct{})
return nil return nil
@ -182,7 +186,7 @@ func (c *DHCPClient) loadLeaseFile() error {
Msg("current dhcp lease expiry time calculated") Msg("current dhcp lease expiry time calculated")
} }
c.onLeaseChange(lease) c.onLeaseChange(lease.ToDHCPLease())
c.logger.Info(). c.logger.Info().
Str("ip", lease.IPAddress.String()). Str("ip", lease.IPAddress.String()).
@ -196,3 +200,47 @@ func (c *DHCPClient) loadLeaseFile() error {
func (c *DHCPClient) GetLease() *Lease { func (c *DHCPClient) GetLease() *Lease {
return c.lease return c.lease
} }
func (c *DHCPClient) Domain() string {
return c.lease.Domain
}
func (c *DHCPClient) Lease4() *types.DHCPLease {
if c.lease == nil {
return nil
}
return c.lease.ToDHCPLease()
}
func (c *DHCPClient) Lease6() *types.DHCPLease {
// TODO: implement
return nil
}
func (c *DHCPClient) SetIPv4(enabled bool) {
// TODO: implement
}
func (c *DHCPClient) SetIPv6(enabled bool) {
// TODO: implement
}
func (c *DHCPClient) SetOnLeaseChange(callback func(lease *types.DHCPLease)) {
c.onLeaseChange = callback
}
func (c *DHCPClient) Start() error {
c.runOnce.Do(func() {
go func() {
err := c.run()
if err != nil {
c.logger.Error().Err(err).Msg("failed to run udhcpc")
}
}()
})
return nil
}
func (c *DHCPClient) Stop() error {
return c.KillProcess() // udhcpc already has KillProcess()
}

View File

@ -43,6 +43,18 @@ func initTimeSync() {
timeSync = timesync.NewTimeSync(&timesync.TimeSyncOptions{ timeSync = timesync.NewTimeSync(&timesync.TimeSyncOptions{
Logger: timesyncLogger, Logger: timesyncLogger,
NetworkConfig: config.NetworkConfig, NetworkConfig: config.NetworkConfig,
PreCheckIPv4: func() (bool, error) {
if !networkManager.IPv4Ready() {
return false, nil
}
return true, nil
},
PreCheckIPv6: func() (bool, error) {
if !networkManager.IPv6Ready() {
return false, nil
}
return true, nil
},
PreCheckFunc: func() (bool, error) { PreCheckFunc: func() (bool, error) {
if !networkManager.IsOnline() { if !networkManager.IsOnline() {
return false, nil return false, nil