diff --git a/internal/network/rpc.go b/internal/network/rpc.go deleted file mode 100644 index d18b9ae9..00000000 --- a/internal/network/rpc.go +++ /dev/null @@ -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() -// } diff --git a/internal/network/utils.go b/internal/network/utils.go deleted file mode 100644 index 797fd72f..00000000 --- a/internal/network/utils.go +++ /dev/null @@ -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) -} diff --git a/internal/udhcpc/parser.go b/internal/udhcpc/parser.go deleted file mode 100644 index d75857c9..00000000 --- a/internal/udhcpc/parser.go +++ /dev/null @@ -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 -} diff --git a/pkg/nmlite/dhcp.go b/pkg/nmlite/dhcp.go index fa1312e6..f64d4b80 100644 --- a/pkg/nmlite/dhcp.go +++ b/pkg/nmlite/dhcp.go @@ -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() -} diff --git a/pkg/nmlite/interface.go b/pkg/nmlite/interface.go index 19912652..8928ce14 100644 --- a/pkg/nmlite/interface.go +++ b/pkg/nmlite/interface.go @@ -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 diff --git a/pkg/nmlite/dhclient/client.go b/pkg/nmlite/jetdhcpc/client.go similarity index 86% rename from pkg/nmlite/dhclient/client.go rename to pkg/nmlite/jetdhcpc/client.go index af6dc9ea..0f076ebb 100644 --- a/pkg/nmlite/dhclient/client.go +++ b/pkg/nmlite/jetdhcpc/client.go @@ -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) { diff --git a/pkg/nmlite/dhclient/dhcp4.go b/pkg/nmlite/jetdhcpc/dhcp4.go similarity index 98% rename from pkg/nmlite/dhclient/dhcp4.go rename to pkg/nmlite/jetdhcpc/dhcp4.go index 9518bc87..e77db862 100644 --- a/pkg/nmlite/dhclient/dhcp4.go +++ b/pkg/nmlite/jetdhcpc/dhcp4.go @@ -1,4 +1,4 @@ -package dhclient +package jetdhcpc import ( "github.com/insomniacslk/dhcp/dhcpv4" diff --git a/pkg/nmlite/dhclient/dhcp6.go b/pkg/nmlite/jetdhcpc/dhcp6.go similarity index 99% rename from pkg/nmlite/dhclient/dhcp6.go rename to pkg/nmlite/jetdhcpc/dhcp6.go index 6c393ecf..f71f744c 100644 --- a/pkg/nmlite/dhclient/dhcp6.go +++ b/pkg/nmlite/jetdhcpc/dhcp6.go @@ -1,4 +1,4 @@ -package dhclient +package jetdhcpc import ( "log" diff --git a/pkg/nmlite/dhclient/lease.go b/pkg/nmlite/jetdhcpc/lease.go similarity index 99% rename from pkg/nmlite/dhclient/lease.go rename to pkg/nmlite/jetdhcpc/lease.go index b63600bf..dbd4fef9 100644 --- a/pkg/nmlite/dhclient/lease.go +++ b/pkg/nmlite/jetdhcpc/lease.go @@ -1,4 +1,4 @@ -package dhclient +package jetdhcpc import ( "bufio" diff --git a/pkg/nmlite/dhclient/legacy.go b/pkg/nmlite/jetdhcpc/legacy.go similarity index 99% rename from pkg/nmlite/dhclient/legacy.go rename to pkg/nmlite/jetdhcpc/legacy.go index 2a47a70e..b8ee4c0b 100644 --- a/pkg/nmlite/dhclient/legacy.go +++ b/pkg/nmlite/jetdhcpc/legacy.go @@ -1,4 +1,4 @@ -package dhclient +package jetdhcpc import ( "bytes" diff --git a/pkg/nmlite/dhclient/logging.go b/pkg/nmlite/jetdhcpc/logging.go similarity index 98% rename from pkg/nmlite/dhclient/logging.go rename to pkg/nmlite/jetdhcpc/logging.go index 3d6cb8ce..3ee696e7 100644 --- a/pkg/nmlite/dhclient/logging.go +++ b/pkg/nmlite/jetdhcpc/logging.go @@ -1,4 +1,4 @@ -package dhclient +package jetdhcpc import ( "github.com/insomniacslk/dhcp/dhcpv4" diff --git a/pkg/nmlite/dhclient/state.go b/pkg/nmlite/jetdhcpc/state.go similarity index 98% rename from pkg/nmlite/dhclient/state.go rename to pkg/nmlite/jetdhcpc/state.go index d9afc396..312211e8 100644 --- a/pkg/nmlite/dhclient/state.go +++ b/pkg/nmlite/jetdhcpc/state.go @@ -1,5 +1,4 @@ -// Package nmlite provides DHCP state persistence for the network manager. -package dhclient +package jetdhcpc import ( "encoding/json" diff --git a/pkg/nmlite/dhclient/utils.go b/pkg/nmlite/jetdhcpc/utils.go similarity index 97% rename from pkg/nmlite/dhclient/utils.go rename to pkg/nmlite/jetdhcpc/utils.go index 4825cc4f..86b5d5d8 100644 --- a/pkg/nmlite/dhclient/utils.go +++ b/pkg/nmlite/jetdhcpc/utils.go @@ -1,4 +1,4 @@ -package dhclient +package jetdhcpc import ( "context" diff --git a/pkg/nmlite/link/netlink.go b/pkg/nmlite/link/netlink.go index d8b5e7cd..c8c4e53a 100644 --- a/pkg/nmlite/link/netlink.go +++ b/pkg/nmlite/link/netlink.go @@ -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) { diff --git a/pkg/nmlite/resolvconf_test.go b/pkg/nmlite/resolvconf_test.go new file mode 100644 index 00000000..ddb91854 --- /dev/null +++ b/pkg/nmlite/resolvconf_test.go @@ -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()) +} diff --git a/internal/udhcpc/options.go b/pkg/nmlite/udhcpc/options.go similarity index 100% rename from internal/udhcpc/options.go rename to pkg/nmlite/udhcpc/options.go diff --git a/pkg/nmlite/udhcpc/parser.go b/pkg/nmlite/udhcpc/parser.go new file mode 100644 index 00000000..0c15b031 --- /dev/null +++ b/pkg/nmlite/udhcpc/parser.go @@ -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 +} diff --git a/internal/udhcpc/parser_test.go b/pkg/nmlite/udhcpc/parser_test.go similarity index 100% rename from internal/udhcpc/parser_test.go rename to pkg/nmlite/udhcpc/parser_test.go diff --git a/internal/udhcpc/proc.go b/pkg/nmlite/udhcpc/proc.go similarity index 100% rename from internal/udhcpc/proc.go rename to pkg/nmlite/udhcpc/proc.go diff --git a/internal/udhcpc/udhcpc.go b/pkg/nmlite/udhcpc/udhcpc.go similarity index 86% rename from internal/udhcpc/udhcpc.go rename to pkg/nmlite/udhcpc/udhcpc.go index 7b4d6e4d..1c6664b5 100644 --- a/internal/udhcpc/udhcpc.go +++ b/pkg/nmlite/udhcpc/udhcpc.go @@ -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() +}