mirror of https://github.com/jetkvm/kvm.git
monitor link state using netlink
This commit is contained in:
parent
df0f5efff3
commit
456ee66fc2
|
|
@ -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()
|
||||
// }
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/network/types"
|
||||
"github.com/jetkvm/kvm/pkg/nmlite/dhclient"
|
||||
"github.com/jetkvm/kvm/pkg/nmlite/jetdhcpc"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
|
@ -16,7 +16,7 @@ type DHCPClient struct {
|
|||
ctx context.Context
|
||||
ifaceName string
|
||||
logger *zerolog.Logger
|
||||
client *dhclient.Client
|
||||
client types.DHCPClient
|
||||
link netlink.Link
|
||||
|
||||
// Configuration
|
||||
|
|
@ -40,9 +40,6 @@ func NewDHCPClient(ctx context.Context, ifaceName string, logger *zerolog.Logger
|
|||
return nil, fmt.Errorf("logger cannot be nil")
|
||||
}
|
||||
|
||||
// Create state manager
|
||||
// stateManager := NewDHCPStateManager("", logger)
|
||||
|
||||
return &DHCPClient{
|
||||
ctx: ctx,
|
||||
ifaceName: ifaceName,
|
||||
|
|
@ -81,13 +78,13 @@ func (dc *DHCPClient) Start() error {
|
|||
dc.logger.Info().Msg("starting DHCP client")
|
||||
|
||||
// Create the underlying DHCP client
|
||||
client, err := dhclient.NewClient(dc.ctx, []string{dc.ifaceName}, &dhclient.Config{
|
||||
client, err := jetdhcpc.NewClient(dc.ctx, []string{dc.ifaceName}, &jetdhcpc.Config{
|
||||
IPv4: dc.ipv4Enabled,
|
||||
IPv6: dc.ipv6Enabled,
|
||||
OnLease4Change: func(lease *dhclient.Lease) {
|
||||
OnLease4Change: func(lease *types.DHCPLease) {
|
||||
dc.handleLeaseChange(lease, false)
|
||||
},
|
||||
OnLease6Change: func(lease *dhclient.Lease) {
|
||||
OnLease6Change: func(lease *types.DHCPLease) {
|
||||
dc.handleLeaseChange(lease, true)
|
||||
},
|
||||
UpdateResolvConf: func(nameservers []string) error {
|
||||
|
|
@ -115,6 +112,27 @@ func (dc *DHCPClient) Start() error {
|
|||
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
|
||||
func (dc *DHCPClient) Stop() error {
|
||||
if dc.client == nil {
|
||||
|
|
@ -150,62 +168,19 @@ func (dc *DHCPClient) Release() error {
|
|||
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
|
||||
func (dc *DHCPClient) handleLeaseChange(lease *dhclient.Lease, isIPv6 bool) {
|
||||
func (dc *DHCPClient) handleLeaseChange(lease *types.DHCPLease, isIPv6 bool) {
|
||||
if lease == nil {
|
||||
return
|
||||
}
|
||||
|
||||
convertedLease := dc.convertLease(lease, isIPv6)
|
||||
if convertedLease == nil {
|
||||
dc.logger.Error().Msg("failed to convert lease")
|
||||
return
|
||||
}
|
||||
|
||||
dc.logger.Info().
|
||||
Bool("ipv6", isIPv6).
|
||||
Str("ip", convertedLease.IPAddress.String()).
|
||||
Str("ip", lease.IPAddress.String()).
|
||||
Msg("DHCP lease changed")
|
||||
|
||||
// Notify callback
|
||||
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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ type InterfaceManager struct {
|
|||
config *types.NetworkConfig
|
||||
logger *zerolog.Logger
|
||||
state *types.InterfaceState
|
||||
linkState *link.Link
|
||||
stateMu sync.RWMutex
|
||||
|
||||
// Network components
|
||||
|
|
@ -107,6 +108,14 @@ func (im *InterfaceManager) Start() error {
|
|||
im.wg.Add(1)
|
||||
go im.monitorInterfaceState()
|
||||
|
||||
nl := getNetlinkManager()
|
||||
nl.AddLinkStateCallback(im.ifaceName, link.LinkStateCallback{
|
||||
Async: true,
|
||||
Func: func(link *link.Link) {
|
||||
im.handleLinkStateChange(link)
|
||||
},
|
||||
})
|
||||
|
||||
// Apply initial configuration
|
||||
if err := im.applyConfiguration(); err != nil {
|
||||
im.logger.Error().Err(err).Msg("failed to apply initial configuration")
|
||||
|
|
@ -189,6 +198,10 @@ func (im *InterfaceManager) GetConfig() *types.NetworkConfig {
|
|||
return &config
|
||||
}
|
||||
|
||||
func (im *InterfaceManager) ApplyConfiguration() error {
|
||||
return im.applyConfiguration()
|
||||
}
|
||||
|
||||
// SetConfig updates the interface configuration
|
||||
func (im *InterfaceManager) SetConfig(config *types.NetworkConfig) error {
|
||||
if config == nil {
|
||||
|
|
@ -446,7 +459,7 @@ func (im *InterfaceManager) getDomain() string {
|
|||
|
||||
// Try to get domain from DHCP lease
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -454,6 +467,51 @@ func (im *InterfaceManager) getDomain() string {
|
|||
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()
|
||||
}
|
||||
|
||||
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
|
||||
func (im *InterfaceManager) monitorInterfaceState() {
|
||||
defer im.wg.Done()
|
||||
|
|
@ -477,6 +535,7 @@ func (im *InterfaceManager) monitorInterfaceState() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// updateInterfaceState updates the current interface state
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package dhclient
|
||||
package jetdhcpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/go-co-op/gocron/v2"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
"github.com/jetkvm/kvm/internal/network/types"
|
||||
"github.com/jetkvm/kvm/pkg/nmlite/link"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
|
@ -27,7 +28,7 @@ var (
|
|||
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.
|
||||
type Config struct {
|
||||
|
|
@ -81,6 +82,7 @@ type Config struct {
|
|||
}
|
||||
|
||||
type Client struct {
|
||||
types.DHCPClient
|
||||
ifaces []string
|
||||
cfg Config
|
||||
l *zerolog.Logger
|
||||
|
|
@ -153,6 +155,34 @@ func (c *Client) sendInitialRequests() chan interface{} {
|
|||
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{} {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
|
@ -175,30 +205,11 @@ func (c *Client) sendRequests(ipv4, ipv6 bool) chan interface{} {
|
|||
}
|
||||
|
||||
if ipv4 {
|
||||
wg.Add(1)
|
||||
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)
|
||||
c.sendRequestsFamily(link.AfInet, &wg, &r, &l, iface)
|
||||
}
|
||||
|
||||
if ipv6 {
|
||||
return // TODO: implement DHCP6
|
||||
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)
|
||||
c.sendRequestsFamily(link.AfInet6, &wg, &r, &l, iface)
|
||||
}
|
||||
}(iface)
|
||||
}
|
||||
|
|
@ -210,18 +221,26 @@ func (c *Client) sendRequests(ipv4, ipv6 bool) chan interface{} {
|
|||
return r
|
||||
}
|
||||
|
||||
func (c *Client) Lease4() *Lease {
|
||||
func (c *Client) Lease4() *types.DHCPLease {
|
||||
c.lease4Mu.Lock()
|
||||
defer c.lease4Mu.Unlock()
|
||||
|
||||
return c.currentLease4
|
||||
if c.currentLease4 == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.currentLease4.ToDHCPLease()
|
||||
}
|
||||
|
||||
func (c *Client) Lease6() *Lease {
|
||||
func (c *Client) Lease6() *types.DHCPLease {
|
||||
c.lease6Mu.Lock()
|
||||
defer c.lease6Mu.Unlock()
|
||||
|
||||
return c.currentLease6
|
||||
if c.currentLease6 == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.currentLease6.ToDHCPLease()
|
||||
}
|
||||
|
||||
func (c *Client) Domain() string {
|
||||
|
|
@ -288,11 +307,11 @@ func (c *Client) handleLeaseChange(lease *Lease) {
|
|||
|
||||
// TODO: handle lease expiration
|
||||
if c.cfg.OnLease4Change != nil && ipv4 {
|
||||
c.cfg.OnLease4Change(lease)
|
||||
c.cfg.OnLease4Change(lease.ToDHCPLease())
|
||||
}
|
||||
|
||||
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()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Release() {
|
||||
func (c *Client) Release() error {
|
||||
// TODO: implement
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) SetIPv4(ipv4 bool) {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dhclient
|
||||
package jetdhcpc
|
||||
|
||||
import (
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dhclient
|
||||
package jetdhcpc
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dhclient
|
||||
package jetdhcpc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dhclient
|
||||
package jetdhcpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dhclient
|
||||
package jetdhcpc
|
||||
|
||||
import (
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
// Package nmlite provides DHCP state persistence for the network manager.
|
||||
package dhclient
|
||||
package jetdhcpc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dhclient
|
||||
package jetdhcpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -50,10 +50,18 @@ var (
|
|||
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
|
||||
mu sync.RWMutex
|
||||
logger *zerolog.Logger
|
||||
linkStateCh chan netlink.LinkUpdate
|
||||
mu sync.RWMutex
|
||||
linkStateCallbacks map[string][]LinkStateCallback
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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
|
||||
func GetNetlinkManager() *NetlinkManager {
|
||||
netlinkManagerOnce.Do(func() {
|
||||
netlinkManagerInstance = &NetlinkManager{
|
||||
logger: &zerolog.Logger{}, // Default no-op logger
|
||||
}
|
||||
netlinkManagerInstance = newNetlinkManager(nil)
|
||||
})
|
||||
return netlinkManagerInstance
|
||||
}
|
||||
|
|
@ -83,18 +123,56 @@ func GetNetlinkManager() *NetlinkManager {
|
|||
// InitializeNetlinkManager initializes the singleton NetlinkManager with a logger
|
||||
func InitializeNetlinkManager(logger *zerolog.Logger) *NetlinkManager {
|
||||
netlinkManagerOnce.Do(func() {
|
||||
if logger == nil {
|
||||
// Create a no-op logger if none provided
|
||||
logger = &zerolog.Logger{}
|
||||
}
|
||||
netlinkManagerInstance = &NetlinkManager{
|
||||
logger: logger,
|
||||
}
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
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)
|
||||
}
|
||||
|
||||
// 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(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
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/jetkvm/kvm/internal/network/types"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
|
|
@ -18,6 +19,7 @@ const (
|
|||
)
|
||||
|
||||
type DHCPClient struct {
|
||||
types.DHCPClient
|
||||
InterfaceName string
|
||||
leaseFile string
|
||||
pidFile string
|
||||
|
|
@ -196,3 +198,35 @@ func (c *DHCPClient) loadLeaseFile() error {
|
|||
func (c *DHCPClient) GetLease() *Lease {
|
||||
return c.lease
|
||||
}
|
||||
|
||||
func (c *DHCPClient) Domain() string {
|
||||
return c.lease.Domain
|
||||
}
|
||||
|
||||
func (c *DHCPClient) Lease4() *Lease {
|
||||
return c.lease
|
||||
}
|
||||
|
||||
func (c *DHCPClient) Lease6() *Lease {
|
||||
return c.lease
|
||||
}
|
||||
|
||||
func (c *DHCPClient) SetIPv4(enabled bool) {
|
||||
// TODO: implement
|
||||
}
|
||||
|
||||
func (c *DHCPClient) SetIPv6(enabled bool) {
|
||||
// TODO: implement
|
||||
}
|
||||
|
||||
func (c *DHCPClient) SetOnLeaseChange(callback func(lease *Lease)) {
|
||||
c.onLeaseChange = callback
|
||||
}
|
||||
|
||||
func (c *DHCPClient) Start() error {
|
||||
return c.Run() // udhcpc already has Run()
|
||||
}
|
||||
|
||||
func (c *DHCPClient) Stop() error {
|
||||
return c.KillProcess() // udhcpc already has KillProcess()
|
||||
}
|
||||
Loading…
Reference in New Issue