mirror of https://github.com/jetkvm/kvm.git
fix state change detection
This commit is contained in:
parent
6743db6e3d
commit
ad0b86c8a6
|
|
@ -1,5 +1,10 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// DHCPClient is the interface for a DHCP client.
|
// DHCPClient is the interface for a DHCP client.
|
||||||
type DHCPClient interface {
|
type DHCPClient interface {
|
||||||
Domain() string
|
Domain() string
|
||||||
|
|
@ -13,3 +18,75 @@ type DHCPClient interface {
|
||||||
Start() error
|
Start() error
|
||||||
Stop() error
|
Stop() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DHCPLease is a network configuration obtained by DHCP.
|
||||||
|
type DHCPLease 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
|
||||||
|
SearchList []string `env:"search" json:"search_list,omitempty"` // The search list for 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
|
||||||
|
RenewalTime time.Duration `env:"renewal" json:"renewal,omitempty"` // The renewal time, in seconds
|
||||||
|
RebindingTime time.Duration `env:"rebinding" json:"rebinding,omitempty"` // The rebinding 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
|
||||||
|
ClassIdentifier string `env:"classid" json:"class_identifier,omitempty"` // The class identifier
|
||||||
|
LeaseExpiry *time.Time `json:"lease_expiry,omitempty"` // The expiry time of the lease
|
||||||
|
|
||||||
|
InterfaceName string `json:"interface_name,omitempty"` // The name of the interface
|
||||||
|
DHCPClient string `json:"dhcp_client,omitempty"` // The DHCP client that obtained the lease
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsIPv6 returns true if the DHCP lease is for an IPv6 address
|
||||||
|
func (d *DHCPLease) IsIPv6() bool {
|
||||||
|
return d.IPAddress.To4() == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPMask returns the IP mask for the DHCP lease
|
||||||
|
func (d *DHCPLease) IPMask() net.IPMask {
|
||||||
|
if d.IsIPv6() {
|
||||||
|
// TODO: not implemented
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mask := net.ParseIP(d.Netmask.String())
|
||||||
|
return net.IPv4Mask(mask[12], mask[13], mask[14], mask[15])
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPNet returns the IP net for the DHCP lease
|
||||||
|
func (d *DHCPLease) IPNet() *net.IPNet {
|
||||||
|
if d.IsIPv6() {
|
||||||
|
// TODO: not implemented
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &net.IPNet{
|
||||||
|
IP: d.IPAddress,
|
||||||
|
Mask: d.IPMask(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type RpcIPv6Address struct {
|
||||||
|
Address string `json:"address"`
|
||||||
|
Prefix string `json:"prefix"`
|
||||||
|
ValidLifetime *time.Time `json:"valid_lifetime"`
|
||||||
|
PreferredLifetime *time.Time `json:"preferred_lifetime"`
|
||||||
|
Scope int `json:"scope"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RpcInterfaceState struct {
|
||||||
|
InterfaceState
|
||||||
|
IPv6Addresses []RpcIPv6Address `json:"ipv6_addresses"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InterfaceState) ToRpcInterfaceState() *RpcInterfaceState {
|
||||||
|
addrs := make([]RpcIPv6Address, len(s.IPv6Addresses))
|
||||||
|
for i, addr := range s.IPv6Addresses {
|
||||||
|
addrs[i] = RpcIPv6Address{
|
||||||
|
Address: addr.Address.String(),
|
||||||
|
Prefix: addr.Prefix.String(),
|
||||||
|
ValidLifetime: addr.ValidLifetime,
|
||||||
|
PreferredLifetime: addr.PreferredLifetime,
|
||||||
|
Scope: addr.Scope,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &RpcInterfaceState{
|
||||||
|
InterfaceState: *s,
|
||||||
|
IPv6Addresses: addrs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,8 @@ type IPv6Address struct {
|
||||||
Prefix net.IPNet `json:"prefix"`
|
Prefix net.IPNet `json:"prefix"`
|
||||||
ValidLifetime *time.Time `json:"valid_lifetime"`
|
ValidLifetime *time.Time `json:"valid_lifetime"`
|
||||||
PreferredLifetime *time.Time `json:"preferred_lifetime"`
|
PreferredLifetime *time.Time `json:"preferred_lifetime"`
|
||||||
|
valid_lft int `json:"valid_lft"`
|
||||||
|
prefered_lft int `json:"prefered_lft"`
|
||||||
Scope int `json:"scope"`
|
Scope int `json:"scope"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,49 +109,6 @@ func (c *NetworkConfig) GetTransportProxyFunc() func(*http.Request) (*url.URL, e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DHCPLease is a network configuration obtained by DHCP.
|
|
||||||
type DHCPLease 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
|
|
||||||
SearchList []string `env:"search" json:"search_list,omitempty"` // The search list for 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
|
|
||||||
RenewalTime time.Duration `env:"renewal" json:"renewal,omitempty"` // The renewal time, in seconds
|
|
||||||
RebindingTime time.Duration `env:"rebinding" json:"rebinding,omitempty"` // The rebinding 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
|
|
||||||
ClassIdentifier string `env:"classid" json:"class_identifier,omitempty"` // The class identifier
|
|
||||||
LeaseExpiry *time.Time `json:"lease_expiry,omitempty"` // The expiry time of the lease
|
|
||||||
|
|
||||||
InterfaceName string `json:"interface_name,omitempty"` // The name of the interface
|
|
||||||
DHCPClient string `json:"dhcp_client,omitempty"` // The DHCP client that obtained the lease
|
|
||||||
}
|
|
||||||
|
|
||||||
// InterfaceState represents the current state of a network interface
|
// InterfaceState represents the current state of a network interface
|
||||||
type InterfaceState struct {
|
type InterfaceState struct {
|
||||||
InterfaceName string `json:"interface_name"`
|
InterfaceName string `json:"interface_name"`
|
||||||
|
|
@ -175,32 +134,3 @@ type NetworkConfigInterface interface {
|
||||||
IPv4Addresses() []IPAddress
|
IPv4Addresses() []IPAddress
|
||||||
IPv6Addresses() []IPAddress
|
IPv6Addresses() []IPAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsIPv6 returns true if the DHCP lease is for an IPv6 address
|
|
||||||
func (d *DHCPLease) IsIPv6() bool {
|
|
||||||
return d.IPAddress.To4() == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPMask returns the IP mask for the DHCP lease
|
|
||||||
func (d *DHCPLease) IPMask() net.IPMask {
|
|
||||||
if d.IsIPv6() {
|
|
||||||
// TODO: not implemented
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
mask := net.ParseIP(d.Netmask.String())
|
|
||||||
return net.IPv4Mask(mask[12], mask[13], mask[14], mask[15])
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPNet returns the IP net for the DHCP lease
|
|
||||||
func (d *DHCPLease) IPNet() *net.IPNet {
|
|
||||||
if d.IsIPv6() {
|
|
||||||
// TODO: not implemented
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &net.IPNet{
|
|
||||||
IP: d.IPAddress,
|
|
||||||
Mask: d.IPMask(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,10 @@ func networkStateChanged(_ string, state types.InterfaceState) {
|
||||||
// do not block the main thread
|
// do not block the main thread
|
||||||
go waitCtrlAndRequestDisplayUpdate(true, "network_state_changed")
|
go waitCtrlAndRequestDisplayUpdate(true, "network_state_changed")
|
||||||
|
|
||||||
|
if currentSession != nil {
|
||||||
|
writeJSONRPCEvent("networkState", state.ToRpcInterfaceState(), currentSession)
|
||||||
|
}
|
||||||
|
|
||||||
if state.Online {
|
if state.Online {
|
||||||
networkLogger.Info().Msg("network state changed to online, triggering time sync")
|
networkLogger.Info().Msg("network state changed to online, triggering time sync")
|
||||||
triggerTimeSyncOnNetworkStateChange()
|
triggerTimeSyncOnNetworkStateChange()
|
||||||
|
|
@ -108,9 +112,9 @@ func initNetwork() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcGetNetworkState() *types.InterfaceState {
|
func rpcGetNetworkState() *types.RpcInterfaceState {
|
||||||
state, _ := networkManager.GetInterfaceState(NetIfName)
|
state, _ := networkManager.GetInterfaceState(NetIfName)
|
||||||
return state
|
return state.ToRpcInterfaceState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcGetNetworkSettings() *RpcNetworkSettings {
|
func rpcGetNetworkSettings() *RpcNetworkSettings {
|
||||||
|
|
|
||||||
|
|
@ -683,116 +683,6 @@ func (im *InterfaceManager) monitorInterfaceState() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateInterfaceState updates the current interface state
|
|
||||||
func (im *InterfaceManager) updateInterfaceState() error {
|
|
||||||
nl, err := im.link()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get interface: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if state changed
|
|
||||||
stateChanged := false
|
|
||||||
|
|
||||||
attrs := nl.Attrs()
|
|
||||||
isUp := attrs.OperState == netlink.OperUp
|
|
||||||
|
|
||||||
// check if the interface has unicast addresses
|
|
||||||
isOnline := isUp && nl.HasGlobalUnicastAddress()
|
|
||||||
|
|
||||||
// We should release the lock before calling the callbacks
|
|
||||||
// to avoid deadlocks
|
|
||||||
im.stateMu.Lock()
|
|
||||||
if im.state.Up != isUp {
|
|
||||||
im.state.Up = isUp
|
|
||||||
stateChanged = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if im.state.Online != isOnline {
|
|
||||||
im.state.Online = isOnline
|
|
||||||
stateChanged = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if im.state.MACAddress != attrs.HardwareAddr.String() {
|
|
||||||
im.state.MACAddress = attrs.HardwareAddr.String()
|
|
||||||
stateChanged = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update IP addresses
|
|
||||||
if err := im.updateIPAddresses(nl); err != nil {
|
|
||||||
im.logger.Error().Err(err).Msg("failed to update IP addresses")
|
|
||||||
}
|
|
||||||
|
|
||||||
im.state.LastUpdated = time.Now()
|
|
||||||
im.stateMu.Unlock()
|
|
||||||
|
|
||||||
// Notify callback if state changed
|
|
||||||
if stateChanged && im.onStateChange != nil {
|
|
||||||
im.logger.Debug().Interface("state", im.state).Msg("notifying state change")
|
|
||||||
im.onStateChange(*im.state)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateIPAddresses updates the IP addresses in the state
|
|
||||||
func (im *InterfaceManager) updateIPAddresses(nl *link.Link) error {
|
|
||||||
if err := nl.Refresh(); err != nil {
|
|
||||||
return fmt.Errorf("failed to refresh link: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs, err := nl.AddrList(link.AfUnspec)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get addresses: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ipv4Addresses []string
|
|
||||||
ipv6Addresses []types.IPv6Address
|
|
||||||
ipv4Addr, ipv6Addr string
|
|
||||||
ipv6LinkLocal string
|
|
||||||
ipv4Ready, ipv6Ready = false, false
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, addr := range addrs {
|
|
||||||
im.logger.Debug().
|
|
||||||
IPAddr("address", addr.IP).
|
|
||||||
Msg("checking address")
|
|
||||||
if addr.IP.To4() != nil {
|
|
||||||
// IPv4 address
|
|
||||||
ipv4Addresses = append(ipv4Addresses, addr.IPNet.String())
|
|
||||||
if ipv4Addr == "" {
|
|
||||||
ipv4Addr = addr.IP.String()
|
|
||||||
ipv4Ready = true
|
|
||||||
}
|
|
||||||
} else if addr.IP.To16() != nil {
|
|
||||||
// IPv6 address
|
|
||||||
if addr.IP.IsLinkLocalUnicast() {
|
|
||||||
ipv6LinkLocal = addr.IP.String()
|
|
||||||
} else if addr.IP.IsGlobalUnicast() {
|
|
||||||
ipv6Addresses = append(ipv6Addresses, types.IPv6Address{
|
|
||||||
Address: addr.IP,
|
|
||||||
Prefix: *addr.IPNet,
|
|
||||||
Scope: addr.Scope,
|
|
||||||
})
|
|
||||||
if ipv6Addr == "" {
|
|
||||||
ipv6Addr = addr.IP.String()
|
|
||||||
ipv6Ready = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
im.state.IPv4Addresses = ipv4Addresses
|
|
||||||
im.state.IPv6Addresses = ipv6Addresses
|
|
||||||
im.state.IPv6LinkLocal = ipv6LinkLocal
|
|
||||||
im.state.IPv4Address = ipv4Addr
|
|
||||||
im.state.IPv6Address = ipv6Addr
|
|
||||||
im.state.IPv4Ready = ipv4Ready
|
|
||||||
im.state.IPv6Ready = ipv6Ready
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
package nmlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jetkvm/kvm/internal/network/types"
|
||||||
|
"github.com/jetkvm/kvm/pkg/nmlite/link"
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
// updateInterfaceState updates the current interface state
|
||||||
|
func (im *InterfaceManager) updateInterfaceState() error {
|
||||||
|
nl, err := im.link()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get interface: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var stateChanged bool
|
||||||
|
|
||||||
|
attrs := nl.Attrs()
|
||||||
|
|
||||||
|
// We should release the lock before calling the callbacks
|
||||||
|
// to avoid deadlocks
|
||||||
|
im.stateMu.Lock()
|
||||||
|
|
||||||
|
// Check if the interface is up
|
||||||
|
isUp := attrs.OperState == netlink.OperUp
|
||||||
|
if im.state.Up != isUp {
|
||||||
|
im.state.Up = isUp
|
||||||
|
stateChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the interface is online
|
||||||
|
isOnline := isUp && nl.HasGlobalUnicastAddress()
|
||||||
|
if im.state.Online != isOnline {
|
||||||
|
im.state.Online = isOnline
|
||||||
|
stateChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the MAC address has changed
|
||||||
|
if im.state.MACAddress != attrs.HardwareAddr.String() {
|
||||||
|
im.state.MACAddress = attrs.HardwareAddr.String()
|
||||||
|
stateChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update IP addresses
|
||||||
|
if ipChanged, err := im.updateInterfaceStateAddresses(nl); err != nil {
|
||||||
|
im.logger.Error().Err(err).Msg("failed to update IP addresses")
|
||||||
|
} else if ipChanged {
|
||||||
|
stateChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
im.state.LastUpdated = time.Now()
|
||||||
|
im.stateMu.Unlock()
|
||||||
|
|
||||||
|
// Notify callback if state changed
|
||||||
|
if stateChanged && im.onStateChange != nil {
|
||||||
|
im.logger.Debug().Interface("state", im.state).Msg("notifying state change")
|
||||||
|
im.onStateChange(*im.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateIPAddresses updates the IP addresses in the state
|
||||||
|
func (im *InterfaceManager) updateInterfaceStateAddresses(nl *link.Link) (bool, error) {
|
||||||
|
addrs, err := nl.AddrList(link.AfUnspec)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to get addresses: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ipv4Addresses []string
|
||||||
|
ipv6Addresses []types.IPv6Address
|
||||||
|
ipv4Addr, ipv6Addr string
|
||||||
|
ipv6LinkLocal string
|
||||||
|
ipv4Ready, ipv6Ready = false, false
|
||||||
|
stateChanged = false
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
im.logger.Debug().
|
||||||
|
IPAddr("address", addr.IP).
|
||||||
|
Msg("checking address")
|
||||||
|
if addr.IP.To4() != nil {
|
||||||
|
// IPv4 address
|
||||||
|
ipv4Addresses = append(ipv4Addresses, addr.IPNet.String())
|
||||||
|
if ipv4Addr == "" {
|
||||||
|
ipv4Addr = addr.IP.String()
|
||||||
|
ipv4Ready = true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv6 address (if it's not an IPv4 address, it must be an IPv6 address)
|
||||||
|
if addr.IP.IsLinkLocalUnicast() {
|
||||||
|
ipv6LinkLocal = addr.IP.String()
|
||||||
|
continue
|
||||||
|
} else if !addr.IP.IsGlobalUnicast() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ipv6Addresses = append(ipv6Addresses, types.IPv6Address{
|
||||||
|
Address: addr.IP,
|
||||||
|
Prefix: *addr.IPNet,
|
||||||
|
Scope: addr.Scope,
|
||||||
|
ValidLifetime: lifetimeToTime(addr.ValidLft),
|
||||||
|
PreferredLifetime: lifetimeToTime(addr.PreferedLft),
|
||||||
|
})
|
||||||
|
if ipv6Addr == "" {
|
||||||
|
ipv6Addr = addr.IP.String()
|
||||||
|
ipv6Ready = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !compareStringSlices(im.state.IPv4Addresses, ipv4Addresses) {
|
||||||
|
im.state.IPv4Addresses = ipv4Addresses
|
||||||
|
stateChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !compareIPv6AddressSlices(im.state.IPv6Addresses, ipv6Addresses) {
|
||||||
|
im.state.IPv6Addresses = ipv6Addresses
|
||||||
|
stateChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if im.state.IPv4Address != ipv4Addr {
|
||||||
|
im.state.IPv4Address = ipv4Addr
|
||||||
|
stateChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if im.state.IPv6Address != ipv6Addr {
|
||||||
|
im.state.IPv6Address = ipv6Addr
|
||||||
|
stateChanged = true
|
||||||
|
}
|
||||||
|
if im.state.IPv6LinkLocal != ipv6LinkLocal {
|
||||||
|
im.state.IPv6LinkLocal = ipv6LinkLocal
|
||||||
|
stateChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if im.state.IPv4Ready != ipv4Ready {
|
||||||
|
im.state.IPv4Ready = ipv4Ready
|
||||||
|
stateChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if im.state.IPv6Ready != ipv6Ready {
|
||||||
|
im.state.IPv6Ready = ipv6Ready
|
||||||
|
stateChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return stateChanged, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
package nmlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jetkvm/kvm/internal/network/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func lifetimeToTime(lifetime int) *time.Time {
|
||||||
|
if lifetime == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for infinite lifetime (0xFFFFFFFF = 4294967295)
|
||||||
|
// This is used for static/permanent addresses
|
||||||
|
// Use uint32 to avoid int overflow on 32-bit systems
|
||||||
|
const infiniteLifetime uint32 = 0xFFFFFFFF
|
||||||
|
if uint32(lifetime) == infiniteLifetime || lifetime < 0 {
|
||||||
|
return nil // Infinite lifetime - no expiration
|
||||||
|
}
|
||||||
|
|
||||||
|
// For finite lifetimes (SLAAC addresses)
|
||||||
|
t := time.Now().Add(time.Duration(lifetime) * time.Second)
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareStringSlices(a, b []string) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(a)
|
||||||
|
sort.Strings(b)
|
||||||
|
|
||||||
|
for i := range a {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareIPv6AddressSlices(a, b []types.IPv6Address) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.SliceStable(a, func(i, j int) bool {
|
||||||
|
return a[i].Address.String() < b[j].Address.String()
|
||||||
|
})
|
||||||
|
sort.SliceStable(b, func(i, j int) bool {
|
||||||
|
return b[i].Address.String() < a[j].Address.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
for i := range a {
|
||||||
|
if a[i].Address.String() != b[i].Address.String() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if a[i].Prefix.String() != b[i].Prefix.String() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// we don't compare the lifetimes because they are not always same
|
||||||
|
if a[i].Scope != b[i].Scope {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ import validator from "validator";
|
||||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||||
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||||
import { NetworkSettings, NetworkState, useRTCStore } from "@/hooks/stores";
|
import { NetworkSettings, NetworkState, useNetworkStateStore, useRTCStore } from "@/hooks/stores";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import { getNetworkSettings, getNetworkState } from "@/utils/jsonrpc";
|
import { getNetworkSettings, getNetworkState } from "@/utils/jsonrpc";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
|
|
@ -75,7 +75,8 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
||||||
export default function SettingsNetworkRoute() {
|
export default function SettingsNetworkRoute() {
|
||||||
const { send } = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
|
|
||||||
const [networkState, setNetworkState] = useState<NetworkState | null>(null);
|
const networkState = useNetworkStateStore(state => state);
|
||||||
|
const setNetworkState = useNetworkStateStore(state => state.setNetworkState);
|
||||||
|
|
||||||
// Some input needs direct state management. Mostly options that open more details
|
// Some input needs direct state management. Mostly options that open more details
|
||||||
const [customDomain, setCustomDomain] = useState<string>("");
|
const [customDomain, setCustomDomain] = useState<string>("");
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
const params = useParams() as { id: string };
|
const params = useParams() as { id: string };
|
||||||
const { sidebarView, setSidebarView, disableVideoFocusTrap } = useUiStore();
|
const { sidebarView, setSidebarView, disableVideoFocusTrap } = useUiStore();
|
||||||
const [ queryParams, setQueryParams ] = useSearchParams();
|
const [queryParams, setQueryParams] = useSearchParams();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
peerConnection, setPeerConnection,
|
peerConnection, setPeerConnection,
|
||||||
|
|
@ -597,10 +597,10 @@ export default function KvmIdRoute() {
|
||||||
});
|
});
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
const { setNetworkState} = useNetworkStateStore();
|
const { setNetworkState } = useNetworkStateStore();
|
||||||
const { setHdmiState } = useVideoStore();
|
const { setHdmiState } = useVideoStore();
|
||||||
const {
|
const {
|
||||||
keyboardLedState, setKeyboardLedState,
|
keyboardLedState, setKeyboardLedState,
|
||||||
keysDownState, setKeysDownState, setUsbState,
|
keysDownState, setKeysDownState, setUsbState,
|
||||||
} = useHidStore();
|
} = useHidStore();
|
||||||
const setHidRpcDisabled = useRTCStore(state => state.setHidRpcDisabled);
|
const setHidRpcDisabled = useRTCStore(state => state.setHidRpcDisabled);
|
||||||
|
|
@ -756,7 +756,7 @@ export default function KvmIdRoute() {
|
||||||
if (location.pathname !== "/other-session") navigateTo("/");
|
if (location.pathname !== "/other-session") navigateTo("/");
|
||||||
}, [navigateTo, location.pathname]);
|
}, [navigateTo, location.pathname]);
|
||||||
|
|
||||||
const { appVersion, getLocalVersion} = useVersion();
|
const { appVersion, getLocalVersion } = useVersion();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (appVersion) return;
|
if (appVersion) return;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue