mirror of https://github.com/jetkvm/kvm.git
Compare commits
9 Commits
f1d41de2ca
...
f16a4bc020
| Author | SHA1 | Date |
|---|---|---|
|
|
f16a4bc020 | |
|
|
97844a8caf | |
|
|
ad0b86c8a6 | |
|
|
6743db6e3d | |
|
|
05f2e5babe | |
|
|
8cc7ead032 | |
|
|
b144d9926f | |
|
|
e755a6e1b1 | |
|
|
99a8c2711c |
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "JetKVM",
|
||||
"name": "JetKVM docker devcontainer",
|
||||
"image": "mcr.microsoft.com/devcontainers/go:1.25-trixie",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
|
|
@ -32,4 +32,5 @@ wget https://github.com/jetkvm/rv1106-system/releases/download/${BUILDKIT_VERSIO
|
|||
sudo mkdir -p /opt/jetkvm-native-buildkit && \
|
||||
sudo tar --use-compress-program="unzstd --long=31" -xvf buildkit.tar.zst -C /opt/jetkvm-native-buildkit && \
|
||||
rm buildkit.tar.zst
|
||||
popd
|
||||
popd
|
||||
rm -rf "${BUILDKIT_TMPDIR}"
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "JetKVM podman devcontainer",
|
||||
"image": "mcr.microsoft.com/devcontainers/go:1.25-trixie",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
// Should match what is defined in ui/package.json
|
||||
"version": "22.19.0"
|
||||
}
|
||||
},
|
||||
"runArgs": [
|
||||
"--userns=keep-id",
|
||||
"--security-opt=label=disable",
|
||||
"--security-opt=label=nested"
|
||||
],
|
||||
"containerUser": "vscode",
|
||||
"containerEnv": {
|
||||
"HOME": "/home/vscode"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,10 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DHCPClient is the interface for a DHCP client.
|
||||
type DHCPClient interface {
|
||||
Domain() string
|
||||
|
|
@ -13,3 +18,75 @@ type DHCPClient interface {
|
|||
Start() 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,11 @@ import (
|
|||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/guregu/null/v6"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
// IPAddress represents a network interface address
|
||||
|
|
@ -19,6 +21,44 @@ type IPAddress struct {
|
|||
Permanent bool
|
||||
}
|
||||
|
||||
func (a *IPAddress) String() string {
|
||||
return a.Address.String()
|
||||
}
|
||||
|
||||
func (a *IPAddress) Compare(n netlink.Addr) bool {
|
||||
if !a.Address.IP.Equal(n.IP) {
|
||||
return false
|
||||
}
|
||||
if slices.Compare(a.Address.Mask, n.IPNet.Mask) != 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *IPAddress) NetlinkAddr() netlink.Addr {
|
||||
return netlink.Addr{
|
||||
IPNet: &a.Address,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *IPAddress) DefaultRoute(linkIndex int) netlink.Route {
|
||||
return netlink.Route{
|
||||
Dst: nil,
|
||||
Gw: a.Gateway,
|
||||
LinkIndex: linkIndex,
|
||||
}
|
||||
}
|
||||
|
||||
// ParsedIPConfig represents the parsed IP configuration
|
||||
type ParsedIPConfig struct {
|
||||
Addresses []IPAddress
|
||||
Nameservers []net.IP
|
||||
SearchList []string
|
||||
Domain string
|
||||
MTU int
|
||||
Interface string
|
||||
}
|
||||
|
||||
// IPv6Address represents an IPv6 address with lifetime information
|
||||
type IPv6Address struct {
|
||||
Address net.IP `json:"address"`
|
||||
|
|
@ -107,49 +147,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
|
||||
type InterfaceState struct {
|
||||
InterfaceName string `json:"interface_name"`
|
||||
|
|
@ -161,6 +158,7 @@ type InterfaceState struct {
|
|||
IPv4Address string `json:"ipv4_address,omitempty"`
|
||||
IPv6Address string `json:"ipv6_address,omitempty"`
|
||||
IPv6LinkLocal string `json:"ipv6_link_local,omitempty"`
|
||||
IPv6Gateway string `json:"ipv6_gateway,omitempty"`
|
||||
IPv4Addresses []string `json:"ipv4_addresses,omitempty"`
|
||||
IPv6Addresses []IPv6Address `json:"ipv6_addresses,omitempty"`
|
||||
NTPServers []net.IP `json:"ntp_servers,omitempty"`
|
||||
|
|
@ -175,32 +173,3 @@ type NetworkConfigInterface interface {
|
|||
IPv4Addresses() []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(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ type TimeSync struct {
|
|||
rtcLock *sync.Mutex
|
||||
|
||||
syncSuccess bool
|
||||
timer *time.Timer
|
||||
|
||||
preCheckFunc PreCheckFunc
|
||||
preCheckIPv4 PreCheckFunc
|
||||
|
|
@ -78,6 +79,7 @@ func NewTimeSync(opts *TimeSyncOptions) *TimeSync {
|
|||
preCheckIPv4: opts.PreCheckIPv4,
|
||||
preCheckIPv6: opts.PreCheckIPv6,
|
||||
networkConfig: opts.NetworkConfig,
|
||||
timer: time.NewTimer(timeSyncWaitNetUpInt),
|
||||
}
|
||||
|
||||
if t.rtcDevicePath != "" {
|
||||
|
|
@ -130,45 +132,51 @@ func (t *TimeSync) getSyncMode() SyncMode {
|
|||
|
||||
return syncMode
|
||||
}
|
||||
func (t *TimeSync) doTimeSync() {
|
||||
func (t *TimeSync) timeSyncLoop() {
|
||||
metricTimeSyncStatus.Set(0)
|
||||
for {
|
||||
|
||||
// use a timer here instead of sleep
|
||||
|
||||
for range t.timer.C {
|
||||
if ok, err := t.preCheckFunc(); !ok {
|
||||
if err != nil {
|
||||
t.l.Error().Err(err).Msg("pre-check failed")
|
||||
}
|
||||
time.Sleep(timeSyncWaitNetChkInt)
|
||||
t.timer.Reset(timeSyncWaitNetChkInt)
|
||||
continue
|
||||
}
|
||||
|
||||
t.l.Info().Msg("syncing system time")
|
||||
start := time.Now()
|
||||
err := t.Sync()
|
||||
err := t.sync()
|
||||
if err != nil {
|
||||
t.l.Error().Str("error", err.Error()).Msg("failed to sync system time")
|
||||
|
||||
// retry after a delay
|
||||
timeSyncRetryInterval += timeSyncRetryStep
|
||||
time.Sleep(timeSyncRetryInterval)
|
||||
t.timer.Reset(timeSyncRetryInterval)
|
||||
// reset the retry interval if it exceeds the max interval
|
||||
if timeSyncRetryInterval > timeSyncRetryMaxInt {
|
||||
timeSyncRetryInterval = 0
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
isInitialSync := !t.syncSuccess
|
||||
t.syncSuccess = true
|
||||
|
||||
t.l.Info().Str("now", time.Now().Format(time.RFC3339)).
|
||||
Str("time_taken", time.Since(start).String()).
|
||||
Bool("is_initial_sync", isInitialSync).
|
||||
Msg("time sync successful")
|
||||
|
||||
metricTimeSyncStatus.Set(1)
|
||||
|
||||
time.Sleep(timeSyncInterval) // after the first sync is done
|
||||
t.timer.Reset(timeSyncInterval) // after the first sync is done
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TimeSync) Sync() error {
|
||||
func (t *TimeSync) sync() error {
|
||||
t.syncLock.Lock()
|
||||
defer t.syncLock.Unlock()
|
||||
|
||||
|
|
@ -256,12 +264,25 @@ Orders:
|
|||
return nil
|
||||
}
|
||||
|
||||
// Sync triggers a manual time sync
|
||||
func (t *TimeSync) Sync() error {
|
||||
if !t.syncLock.TryLock() {
|
||||
t.l.Warn().Msg("sync already in progress, skipping")
|
||||
return nil
|
||||
}
|
||||
t.syncLock.Unlock()
|
||||
|
||||
return t.sync()
|
||||
}
|
||||
|
||||
// IsSyncSuccess returns true if the system time is synchronized
|
||||
func (t *TimeSync) IsSyncSuccess() bool {
|
||||
return t.syncSuccess
|
||||
}
|
||||
|
||||
// Start starts the time sync
|
||||
func (t *TimeSync) Start() {
|
||||
go t.doTimeSync()
|
||||
go t.timeSyncLoop()
|
||||
}
|
||||
|
||||
func (t *TimeSync) setSystemTime(now time.Time) error {
|
||||
|
|
|
|||
42
network.go
42
network.go
|
|
@ -47,31 +47,39 @@ func restartMdns() {
|
|||
}, true)
|
||||
}
|
||||
|
||||
func networkStateChanged(iface string, state *types.InterfaceState) {
|
||||
func triggerTimeSyncOnNetworkStateChange() {
|
||||
if timeSync == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// set the NTP servers from the network manager
|
||||
if networkManager != nil {
|
||||
timeSync.SetDhcpNtpAddresses(networkManager.NTPServerStrings())
|
||||
}
|
||||
|
||||
// sync time
|
||||
if err := timeSync.Sync(); err != nil {
|
||||
networkLogger.Error().Err(err).Msg("failed to sync time after network state change")
|
||||
}
|
||||
}
|
||||
|
||||
func networkStateChanged(_ string, state types.InterfaceState) {
|
||||
// do not block the main thread
|
||||
go waitCtrlAndRequestDisplayUpdate(true, "network_state_changed")
|
||||
|
||||
if timeSync != nil {
|
||||
if networkManager != nil {
|
||||
timeSync.SetDhcpNtpAddresses(networkManager.NTPServerStrings())
|
||||
}
|
||||
if currentSession != nil {
|
||||
writeJSONRPCEvent("networkState", state.ToRpcInterfaceState(), currentSession)
|
||||
}
|
||||
|
||||
if err := timeSync.Sync(); err != nil {
|
||||
networkLogger.Error().Err(err).Msg("failed to sync time after network state change")
|
||||
}
|
||||
if state.Online {
|
||||
networkLogger.Info().Msg("network state changed to online, triggering time sync")
|
||||
triggerTimeSyncOnNetworkStateChange()
|
||||
}
|
||||
|
||||
// always restart mDNS when the network state changes
|
||||
if mDNS != nil {
|
||||
restartMdns()
|
||||
}
|
||||
|
||||
// if the network is now online, trigger an NTP sync if still needed
|
||||
if state.Up && timeSync != nil && (isTimeSyncNeeded() || !timeSync.IsSyncSuccess()) {
|
||||
if err := timeSync.Sync(); err != nil {
|
||||
logger.Warn().Str("error", err.Error()).Msg("unable to sync time on network state change")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateNetworkConfig() {
|
||||
|
|
@ -104,9 +112,9 @@ func initNetwork() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func rpcGetNetworkState() *types.InterfaceState {
|
||||
func rpcGetNetworkState() *types.RpcInterfaceState {
|
||||
state, _ := networkManager.GetInterfaceState(NetIfName)
|
||||
return state
|
||||
return state.ToRpcInterfaceState()
|
||||
}
|
||||
|
||||
func rpcGetNetworkSettings() *RpcNetworkSettings {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ type InterfaceManager struct {
|
|||
hostname *HostnameManager
|
||||
|
||||
// Callbacks
|
||||
onStateChange func(state *types.InterfaceState)
|
||||
onStateChange func(state types.InterfaceState)
|
||||
onConfigChange func(config *types.NetworkConfig)
|
||||
onDHCPLeaseChange func(lease *types.DHCPLease)
|
||||
|
||||
|
|
@ -321,7 +321,7 @@ func (im *InterfaceManager) RenewDHCPLease() error {
|
|||
}
|
||||
|
||||
// SetOnStateChange sets the callback for state changes
|
||||
func (im *InterfaceManager) SetOnStateChange(callback func(state *types.InterfaceState)) {
|
||||
func (im *InterfaceManager) SetOnStateChange(callback func(state types.InterfaceState)) {
|
||||
im.onStateChange = callback
|
||||
}
|
||||
|
||||
|
|
@ -403,13 +403,23 @@ func (im *InterfaceManager) applyIPv4Static() error {
|
|||
return fmt.Errorf("IPv4 static configuration is nil")
|
||||
}
|
||||
|
||||
im.logger.Info().Msg("stopping DHCP")
|
||||
|
||||
// Disable DHCP
|
||||
if im.dhcpClient != nil {
|
||||
im.dhcpClient.SetIPv4(false)
|
||||
}
|
||||
|
||||
// Apply static configuration
|
||||
return im.staticConfig.ApplyIPv4Static(im.config.IPv4Static)
|
||||
im.logger.Info().Interface("config", im.config.IPv4Static).Msg("applying IPv4 static configuration")
|
||||
|
||||
config, err := im.staticConfig.ToIPv4Static(im.config.IPv4Static)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert IPv4 static configuration: %w", err)
|
||||
}
|
||||
|
||||
im.logger.Info().Interface("config", config).Msg("converted IPv4 static configuration")
|
||||
|
||||
return im.ReconcileLinkAddrs(config.Addresses, link.AfInet)
|
||||
}
|
||||
|
||||
// applyIPv4DHCP applies DHCP IPv4 configuration
|
||||
|
|
@ -440,13 +450,20 @@ func (im *InterfaceManager) applyIPv6Static() error {
|
|||
return fmt.Errorf("IPv6 static configuration is nil")
|
||||
}
|
||||
|
||||
im.logger.Info().Msg("stopping DHCPv6")
|
||||
// Disable DHCPv6
|
||||
if im.dhcpClient != nil {
|
||||
im.dhcpClient.SetIPv6(false)
|
||||
}
|
||||
|
||||
// Apply static configuration
|
||||
return im.staticConfig.ApplyIPv6Static(im.config.IPv6Static)
|
||||
config, err := im.staticConfig.ToIPv6Static(im.config.IPv6Static)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert IPv6 static configuration: %w", err)
|
||||
}
|
||||
im.logger.Info().Interface("config", config).Msg("converted IPv6 static configuration")
|
||||
|
||||
return im.ReconcileLinkAddrs(config.Addresses, link.AfInet6)
|
||||
}
|
||||
|
||||
// applyIPv6DHCP applies DHCPv6 configuration
|
||||
|
|
@ -683,119 +700,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)
|
||||
}
|
||||
|
||||
attrs := nl.Attrs()
|
||||
isUp := attrs.OperState == netlink.OperUp
|
||||
|
||||
hasAddrs := false
|
||||
addrs, err := nl.AddrList(link.AfUnspec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get addresses: %w", err)
|
||||
}
|
||||
if len(addrs) > 0 {
|
||||
hasAddrs = true
|
||||
}
|
||||
|
||||
// Check if state changed
|
||||
stateChanged := false
|
||||
// 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 != hasAddrs {
|
||||
im.state.Online = hasAddrs
|
||||
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 {
|
||||
state := *im.state
|
||||
im.logger.Debug().Interface("state", state).Msg("notifying state change")
|
||||
im.onStateChange(&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().Str("address", addr.IP.String()).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
|
||||
func (im *InterfaceManager) updateStateFromDHCPLease(lease *types.DHCPLease) {
|
||||
im.stateMu.Lock()
|
||||
|
|
@ -808,7 +712,8 @@ func (im *InterfaceManager) updateStateFromDHCPLease(lease *types.DHCPLease) {
|
|||
}
|
||||
}
|
||||
|
||||
func (im *InterfaceManager) ReconcileLinkAddrs(addrs []*types.IPAddress) error {
|
||||
// ReconcileLinkAddrs reconciles the link addresses
|
||||
func (im *InterfaceManager) ReconcileLinkAddrs(addrs []types.IPAddress, family int) error {
|
||||
nl := getNetlinkManager()
|
||||
link, err := im.link()
|
||||
if err != nil {
|
||||
|
|
@ -817,7 +722,7 @@ func (im *InterfaceManager) ReconcileLinkAddrs(addrs []*types.IPAddress) error {
|
|||
if link == nil {
|
||||
return fmt.Errorf("failed to get interface: %w", err)
|
||||
}
|
||||
return nl.ReconcileLink(link, addrs)
|
||||
return nl.ReconcileLink(link, addrs, family)
|
||||
}
|
||||
|
||||
// applyDHCPLease applies DHCP lease configuration using ReconcileLinkAddrs
|
||||
|
|
@ -826,7 +731,7 @@ func (im *InterfaceManager) applyDHCPLease(lease *types.DHCPLease) error {
|
|||
ipv4Config := im.convertDHCPLeaseToIPv4Config(lease)
|
||||
|
||||
// Apply the configuration using ReconcileLinkAddrs
|
||||
return im.ReconcileLinkAddrs([]*types.IPAddress{ipv4Config})
|
||||
return im.ReconcileLinkAddrs([]types.IPAddress{*ipv4Config}, link.AfInet)
|
||||
}
|
||||
|
||||
// convertDHCPLeaseToIPv4Config converts a DHCP lease to IPv4Config
|
||||
|
|
|
|||
|
|
@ -0,0 +1,162 @@
|
|||
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) {
|
||||
mgr := getNetlinkManager()
|
||||
|
||||
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
|
||||
ipv6Gateway string
|
||||
ipv4Ready, ipv6Ready = false, false
|
||||
stateChanged = false
|
||||
)
|
||||
|
||||
routes, _ := mgr.ListDefaultRoutes(link.AfInet6)
|
||||
if len(routes) > 0 {
|
||||
ipv6Gateway = routes[0].Gw.String()
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
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.IPv6Gateway != ipv6Gateway {
|
||||
im.state.IPv6Gateway = ipv6Gateway
|
||||
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
|
||||
}
|
||||
|
|
@ -279,7 +279,7 @@ func (c *Client) handleLeaseChange(lease *Lease) {
|
|||
}
|
||||
|
||||
// clear all current jobs with the same tags
|
||||
c.scheduler.RemoveByTags(version)
|
||||
// c.scheduler.RemoveByTags(version)
|
||||
|
||||
// add scheduler job to renew the lease
|
||||
if lease.RenewalTime > 0 {
|
||||
|
|
@ -357,6 +357,9 @@ func (c *Client) SetIPv4(ipv4 bool) {
|
|||
}
|
||||
|
||||
func (c *Client) SetIPv6(ipv6 bool) {
|
||||
c.cfgMu.Lock()
|
||||
defer c.cfgMu.Unlock()
|
||||
|
||||
c.cfg.IPv6 = ipv6
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -302,26 +302,28 @@ func (nm *NetlinkManager) RouteReplace(route *netlink.Route) error {
|
|||
return netlink.RouteReplace(route)
|
||||
}
|
||||
|
||||
// ListDefaultRoutes lists the default routes for the given family
|
||||
func (nm *NetlinkManager) ListDefaultRoutes(family int) ([]netlink.Route, error) {
|
||||
routes, err := netlink.RouteListFiltered(
|
||||
family,
|
||||
&netlink.Route{Dst: nil, Table: 254},
|
||||
netlink.RT_FILTER_DST|netlink.RT_FILTER_TABLE,
|
||||
)
|
||||
if err != nil {
|
||||
nm.logger.Error().Err(err).Int("family", family).Msg("failed to list default routes")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
// HasDefaultRoute checks if a default route exists for the given family
|
||||
func (nm *NetlinkManager) HasDefaultRoute(family int) bool {
|
||||
routes, err := netlink.RouteList(nil, family)
|
||||
routes, err := nm.ListDefaultRoutes(family)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, route := range routes {
|
||||
if route.Dst == nil {
|
||||
return true
|
||||
}
|
||||
if family == AfInet && route.Dst.IP.Equal(net.IPv4zero) && route.Dst.Mask.String() == "0.0.0.0/0" {
|
||||
return true
|
||||
}
|
||||
if family == AfInet6 && route.Dst.IP.Equal(net.IPv6zero) && route.Dst.Mask.String() == "::/0" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return len(routes) > 0
|
||||
}
|
||||
|
||||
// AddDefaultRoute adds a default route
|
||||
|
|
@ -370,59 +372,152 @@ func (nm *NetlinkManager) RemoveDefaultRoute(family int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ReconcileLink reconciles the addresses and routes of a link
|
||||
func (nm *NetlinkManager) ReconcileLink(link *Link, expected []*types.IPAddress) error {
|
||||
expectedAddrs := make(map[string]bool)
|
||||
existingAddrs := make(map[string]bool)
|
||||
func (nm *NetlinkManager) reconcileDefaultRoute(link *Link, expected map[string]net.IP, family int) error {
|
||||
linkIndex := link.Attrs().Index
|
||||
|
||||
for _, addr := range expected {
|
||||
ipCidr := addr.Address.IP.String() + "/" + addr.Address.Mask.String()
|
||||
expectedAddrs[ipCidr] = true
|
||||
added := 0
|
||||
toRemove := make([]*netlink.Route, 0)
|
||||
|
||||
defaultRoutes, err := nm.ListDefaultRoutes(family)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get default routes: %w", err)
|
||||
}
|
||||
|
||||
addrs, err := nm.AddrList(link, AfUnspec)
|
||||
// check existing default routes
|
||||
for _, defaultRoute := range defaultRoutes {
|
||||
// only check the default routes for the current link
|
||||
// TODO: we should also check others later
|
||||
if defaultRoute.LinkIndex != linkIndex {
|
||||
continue
|
||||
}
|
||||
|
||||
key := defaultRoute.Gw.String()
|
||||
if _, ok := expected[key]; !ok {
|
||||
toRemove = append(toRemove, &defaultRoute)
|
||||
continue
|
||||
}
|
||||
|
||||
nm.logger.Warn().Str("gateway", key).Msg("keeping default route")
|
||||
delete(expected, key)
|
||||
}
|
||||
|
||||
// remove remaining default routes
|
||||
for _, defaultRoute := range toRemove {
|
||||
nm.logger.Warn().Str("gateway", defaultRoute.Gw.String()).Msg("removing default route")
|
||||
if err := nm.RouteDel(defaultRoute); err != nil {
|
||||
nm.logger.Warn().Err(err).Msg("failed to remove default route")
|
||||
}
|
||||
}
|
||||
|
||||
// add remaining expected default routes
|
||||
for _, gateway := range expected {
|
||||
nm.logger.Warn().Str("gateway", gateway.String()).Msg("adding default route")
|
||||
|
||||
route := &netlink.Route{
|
||||
Dst: &ipv4DefaultRoute,
|
||||
Gw: gateway,
|
||||
LinkIndex: linkIndex,
|
||||
}
|
||||
if family == AfInet6 {
|
||||
route.Dst = &ipv6DefaultRoute
|
||||
}
|
||||
if err := nm.RouteAdd(route); err != nil {
|
||||
nm.logger.Warn().Err(err).Interface("route", route).Msg("failed to add default route")
|
||||
}
|
||||
added++
|
||||
}
|
||||
|
||||
nm.logger.Info().
|
||||
Int("added", added).
|
||||
Int("removed", len(toRemove)).
|
||||
Msg("default routes reconciled")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReconcileLink reconciles the addresses and routes of a link
|
||||
func (nm *NetlinkManager) ReconcileLink(link *Link, expected []types.IPAddress, family int) error {
|
||||
toAdd := make([]*types.IPAddress, 0)
|
||||
toRemove := make([]*netlink.Addr, 0)
|
||||
toUpdate := make([]*types.IPAddress, 0)
|
||||
expectedAddrs := make(map[string]*types.IPAddress)
|
||||
|
||||
expectedGateways := make(map[string]net.IP)
|
||||
|
||||
// add all expected addresses to the map
|
||||
for _, addr := range expected {
|
||||
expectedAddrs[addr.String()] = &addr
|
||||
if addr.Gateway != nil {
|
||||
expectedGateways[addr.String()] = addr.Gateway
|
||||
}
|
||||
}
|
||||
|
||||
addrs, err := nm.AddrList(link, family)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get addresses: %w", err)
|
||||
}
|
||||
|
||||
// check existing addresses
|
||||
for _, addr := range addrs {
|
||||
ipCidr := addr.IP.String() + "/" + addr.IPNet.Mask.String()
|
||||
existingAddrs[ipCidr] = true
|
||||
// skip the link-local address
|
||||
if addr.IP.IsLinkLocalUnicast() {
|
||||
continue
|
||||
}
|
||||
|
||||
expectedAddr, ok := expectedAddrs[addr.IPNet.String()]
|
||||
if !ok {
|
||||
toRemove = append(toRemove, &addr)
|
||||
continue
|
||||
}
|
||||
|
||||
// if it's not fully equal, we need to update it
|
||||
if !expectedAddr.Compare(addr) {
|
||||
toUpdate = append(toUpdate, expectedAddr)
|
||||
continue
|
||||
}
|
||||
|
||||
// remove it from expected addresses
|
||||
delete(expectedAddrs, addr.IPNet.String())
|
||||
}
|
||||
|
||||
for _, addr := range expected {
|
||||
family := AfUnspec
|
||||
if addr.Address.IP.To4() != nil {
|
||||
family = AfInet
|
||||
} else if addr.Address.IP.To16() != nil {
|
||||
family = AfInet6
|
||||
// add remaining expected addresses
|
||||
for _, addr := range expectedAddrs {
|
||||
toAdd = append(toAdd, addr)
|
||||
}
|
||||
|
||||
for _, addr := range toUpdate {
|
||||
netlinkAddr := addr.NetlinkAddr()
|
||||
if err := nm.AddrDel(link, &netlinkAddr); err != nil {
|
||||
nm.logger.Warn().Err(err).Str("address", addr.Address.String()).Msg("failed to update address")
|
||||
}
|
||||
// we'll add it again later
|
||||
toAdd = append(toAdd, addr)
|
||||
}
|
||||
|
||||
ipCidr := addr.Address.IP.String() + "/" + addr.Address.Mask.String()
|
||||
ipNet := &net.IPNet{
|
||||
IP: addr.Address.IP,
|
||||
Mask: addr.Address.Mask,
|
||||
for _, addr := range toAdd {
|
||||
netlinkAddr := addr.NetlinkAddr()
|
||||
if err := nm.AddrAdd(link, &netlinkAddr); err != nil {
|
||||
nm.logger.Warn().Err(err).Str("address", addr.Address.String()).Msg("failed to add address")
|
||||
}
|
||||
}
|
||||
|
||||
l := nm.logger.With().Str("address", ipNet.String()).Logger()
|
||||
if ok := existingAddrs[ipCidr]; !ok {
|
||||
l.Trace().Msg("adding address")
|
||||
|
||||
if err := nm.AddrAdd(link, &netlink.Addr{IPNet: ipNet}); err != nil {
|
||||
return fmt.Errorf("failed to add address %s: %w", ipCidr, err)
|
||||
}
|
||||
|
||||
l.Info().Msg("address added")
|
||||
for _, netlinkAddr := range toRemove {
|
||||
if err := nm.AddrDel(link, netlinkAddr); err != nil {
|
||||
nm.logger.Warn().Err(err).Str("address", netlinkAddr.IP.String()).Msg("failed to remove address")
|
||||
}
|
||||
}
|
||||
|
||||
if addr.Gateway != nil {
|
||||
gl := l.With().Str("gateway", addr.Gateway.String()).Logger()
|
||||
gl.Trace().Msg("adding default route")
|
||||
if err := nm.AddDefaultRoute(link, addr.Gateway, family); err != nil {
|
||||
return fmt.Errorf("failed to add default route for address %s: %w", ipCidr, err)
|
||||
}
|
||||
gl.Info().Msg("default route added")
|
||||
}
|
||||
actualToAdd := len(toAdd) - len(toUpdate)
|
||||
if len(toAdd) > 0 || len(toUpdate) > 0 || len(toRemove) > 0 {
|
||||
nm.logger.Info().
|
||||
Int("added", actualToAdd).
|
||||
Int("updated", len(toUpdate)).
|
||||
Int("removed", len(toRemove)).
|
||||
Msg("addresses reconciled")
|
||||
}
|
||||
|
||||
if err := nm.reconcileDefaultRoute(link, expectedGateways, family); err != nil {
|
||||
nm.logger.Warn().Err(err).Msg("failed to reconcile default route")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -118,6 +118,22 @@ func (l *Link) AddrList(family int) ([]netlink.Addr, error) {
|
|||
return netlink.AddrList(l.Link, family)
|
||||
}
|
||||
|
||||
// HasGlobalUnicastAddress returns true if the link has a global unicast address
|
||||
func (l *Link) HasGlobalUnicastAddress() bool {
|
||||
addrs, err := l.AddrList(AfUnspec)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
if addr.IP.IsGlobalUnicast() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsSame checks if the link is the same as another link
|
||||
func (l *Link) IsSame(other *Link) bool {
|
||||
if l == nil || other == nil {
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -11,13 +11,3 @@ type IPv4Address struct {
|
|||
Secondary bool
|
||||
Permanent bool
|
||||
}
|
||||
|
||||
// IPv4Config represents the configuration for an IPv4 interface
|
||||
type IPv4Config struct {
|
||||
Addresses []IPv4Address
|
||||
Nameservers []net.IP
|
||||
SearchList []string
|
||||
Domain string
|
||||
MTU int
|
||||
Interface string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ type NetworkManager struct {
|
|||
cancel context.CancelFunc
|
||||
|
||||
// Callback functions for state changes
|
||||
onInterfaceStateChange func(iface string, state *types.InterfaceState)
|
||||
onInterfaceStateChange func(iface string, state types.InterfaceState)
|
||||
onConfigChange func(iface string, config *types.NetworkConfig)
|
||||
onDHCPLeaseChange func(iface string, lease *types.DHCPLease)
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ func (nm *NetworkManager) AddInterface(iface string, config *types.NetworkConfig
|
|||
}
|
||||
|
||||
// Set up callbacks
|
||||
im.SetOnStateChange(func(state *types.InterfaceState) {
|
||||
im.SetOnStateChange(func(state types.InterfaceState) {
|
||||
if nm.onInterfaceStateChange != nil {
|
||||
nm.onInterfaceStateChange(iface, state)
|
||||
}
|
||||
|
|
@ -179,7 +179,7 @@ func (nm *NetworkManager) RenewDHCPLease(iface string) error {
|
|||
}
|
||||
|
||||
// SetOnInterfaceStateChange sets the callback for interface state changes
|
||||
func (nm *NetworkManager) SetOnInterfaceStateChange(callback func(iface string, state *types.InterfaceState)) {
|
||||
func (nm *NetworkManager) SetOnInterfaceStateChange(callback func(iface string, state types.InterfaceState)) {
|
||||
nm.onInterfaceStateChange = callback
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import (
|
|||
"github.com/jetkvm/kvm/internal/network/types"
|
||||
"github.com/jetkvm/kvm/pkg/nmlite/link"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
// StaticConfigManager manages static network configuration
|
||||
|
|
@ -32,81 +31,90 @@ func NewStaticConfigManager(ifaceName string, logger *zerolog.Logger) (*StaticCo
|
|||
}, nil
|
||||
}
|
||||
|
||||
// ApplyIPv4Static applies static IPv4 configuration
|
||||
func (scm *StaticConfigManager) ApplyIPv4Static(config *types.IPv4StaticConfig) error {
|
||||
scm.logger.Info().Msg("applying static IPv4 configuration")
|
||||
// ToIPv4Static applies static IPv4 configuration
|
||||
func (scm *StaticConfigManager) ToIPv4Static(config *types.IPv4StaticConfig) (*types.ParsedIPConfig, error) {
|
||||
if config == nil {
|
||||
return nil, fmt.Errorf("config is nil")
|
||||
}
|
||||
|
||||
// Parse and validate configuration
|
||||
ipv4Config, err := scm.parseIPv4Config(config)
|
||||
// Parse IP address and netmask
|
||||
ipNet, err := link.ParseIPv4Netmask(config.Address.String, config.Netmask.String)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse IPv4 config: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
scm.logger.Info().Str("ipNet", ipNet.String()).Interface("ipc", config).Msg("parsed IPv4 address and netmask")
|
||||
|
||||
// Parse gateway
|
||||
gateway := net.ParseIP(config.Gateway.String)
|
||||
if gateway == nil {
|
||||
return nil, fmt.Errorf("invalid gateway: %s", config.Gateway.String)
|
||||
}
|
||||
|
||||
// Get interface
|
||||
netlinkMgr := getNetlinkManager()
|
||||
link, err := netlinkMgr.GetLinkByName(scm.ifaceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get interface: %w", err)
|
||||
// Parse DNS servers
|
||||
var dns []net.IP
|
||||
for _, dnsStr := range config.DNS {
|
||||
if err := link.ValidateIPAddress(dnsStr, false); err != nil {
|
||||
return nil, fmt.Errorf("invalid DNS server: %w", err)
|
||||
}
|
||||
dns = append(dns, net.ParseIP(dnsStr))
|
||||
}
|
||||
|
||||
// Ensure interface is up
|
||||
if err := netlinkMgr.EnsureInterfaceUp(link); err != nil {
|
||||
return fmt.Errorf("failed to bring interface up: %w", err)
|
||||
address := types.IPAddress{
|
||||
Family: link.AfInet,
|
||||
Address: *ipNet,
|
||||
Gateway: gateway,
|
||||
Secondary: false,
|
||||
Permanent: true,
|
||||
}
|
||||
|
||||
// Apply IP address
|
||||
if err := scm.applyIPv4Address(link, ipv4Config); err != nil {
|
||||
return fmt.Errorf("failed to apply IPv4 address: %w", err)
|
||||
}
|
||||
|
||||
// Apply default route
|
||||
if err := scm.applyIPv4Route(link, ipv4Config); err != nil {
|
||||
return fmt.Errorf("failed to apply IPv4 route: %w", err)
|
||||
}
|
||||
|
||||
scm.logger.Info().Msg("static IPv4 configuration applied successfully")
|
||||
return nil
|
||||
return &types.ParsedIPConfig{
|
||||
Addresses: []types.IPAddress{address},
|
||||
Nameservers: dns,
|
||||
Interface: scm.ifaceName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ApplyIPv6Static applies static IPv6 configuration
|
||||
func (scm *StaticConfigManager) ApplyIPv6Static(config *types.IPv6StaticConfig) error {
|
||||
scm.logger.Info().Msg("applying static IPv6 configuration")
|
||||
// ToIPv6Static applies static IPv6 configuration
|
||||
func (scm *StaticConfigManager) ToIPv6Static(config *types.IPv6StaticConfig) (*types.ParsedIPConfig, error) {
|
||||
if config == nil {
|
||||
return nil, fmt.Errorf("config is nil")
|
||||
}
|
||||
|
||||
// Parse and validate configuration
|
||||
ipv6Config, err := scm.parseIPv6Config(config)
|
||||
// Parse IP address and prefix
|
||||
ipNet, err := link.ParseIPv6Prefix(config.Prefix.String, 64) // Default to /64 if not specified
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse IPv6 config: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get interface
|
||||
netlinkMgr := getNetlinkManager()
|
||||
link, err := netlinkMgr.GetLinkByName(scm.ifaceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get interface: %w", err)
|
||||
// Parse gateway
|
||||
gateway := net.ParseIP(config.Gateway.String)
|
||||
if gateway == nil {
|
||||
return nil, fmt.Errorf("invalid gateway: %s", config.Gateway.String)
|
||||
}
|
||||
|
||||
// Enable IPv6
|
||||
if err := scm.enableIPv6(); err != nil {
|
||||
return fmt.Errorf("failed to enable IPv6: %w", err)
|
||||
// Parse DNS servers
|
||||
var dns []net.IP
|
||||
for _, dnsStr := range config.DNS {
|
||||
dnsIP := net.ParseIP(dnsStr)
|
||||
if dnsIP == nil {
|
||||
return nil, fmt.Errorf("invalid DNS server: %s", dnsStr)
|
||||
}
|
||||
dns = append(dns, dnsIP)
|
||||
}
|
||||
|
||||
// Ensure interface is up
|
||||
if err := netlinkMgr.EnsureInterfaceUp(link); err != nil {
|
||||
return fmt.Errorf("failed to bring interface up: %w", err)
|
||||
address := types.IPAddress{
|
||||
Family: link.AfInet6,
|
||||
Address: *ipNet,
|
||||
Gateway: gateway,
|
||||
Secondary: false,
|
||||
Permanent: true,
|
||||
}
|
||||
|
||||
// Apply IP address
|
||||
if err := scm.applyIPv6Address(link, ipv6Config); err != nil {
|
||||
return fmt.Errorf("failed to apply IPv6 address: %w", err)
|
||||
}
|
||||
|
||||
// Apply default route
|
||||
if err := scm.applyIPv6Route(link, ipv6Config); err != nil {
|
||||
return fmt.Errorf("failed to apply IPv6 route: %w", err)
|
||||
}
|
||||
|
||||
scm.logger.Info().Msg("static IPv6 configuration applied successfully")
|
||||
return nil
|
||||
return &types.ParsedIPConfig{
|
||||
Addresses: []types.IPAddress{address},
|
||||
Nameservers: dns,
|
||||
Interface: scm.ifaceName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DisableIPv4 disables IPv4 on the interface
|
||||
|
|
@ -169,156 +177,6 @@ func (scm *StaticConfigManager) EnableIPv6LinkLocal() error {
|
|||
return netlinkMgr.EnsureInterfaceUp(link)
|
||||
}
|
||||
|
||||
// parseIPv4Config parses and validates IPv4 static configuration
|
||||
func (scm *StaticConfigManager) parseIPv4Config(config *types.IPv4StaticConfig) (*parsedIPv4Config, error) {
|
||||
if config == nil {
|
||||
return nil, fmt.Errorf("config is nil")
|
||||
}
|
||||
|
||||
// Parse IP address and netmask
|
||||
ipNet, err := link.ParseIPv4Netmask(config.Address.String, config.Netmask.String)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scm.logger.Info().Str("ipNet", ipNet.String()).Interface("ipc", config).Msg("parsed IPv4 address and netmask")
|
||||
|
||||
// Parse gateway
|
||||
gateway := net.ParseIP(config.Gateway.String)
|
||||
if gateway == nil {
|
||||
return nil, fmt.Errorf("invalid gateway: %s", config.Gateway.String)
|
||||
}
|
||||
|
||||
// Parse DNS servers
|
||||
var dns []net.IP
|
||||
for _, dnsStr := range config.DNS {
|
||||
if err := link.ValidateIPAddress(dnsStr, false); err != nil {
|
||||
return nil, fmt.Errorf("invalid DNS server: %w", err)
|
||||
}
|
||||
dns = append(dns, net.ParseIP(dnsStr))
|
||||
}
|
||||
|
||||
return &parsedIPv4Config{
|
||||
network: *ipNet,
|
||||
gateway: gateway,
|
||||
dns: dns,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseIPv6Config parses and validates IPv6 static configuration
|
||||
func (scm *StaticConfigManager) parseIPv6Config(config *types.IPv6StaticConfig) (*parsedIPv6Config, error) {
|
||||
if config == nil {
|
||||
return nil, fmt.Errorf("config is nil")
|
||||
}
|
||||
|
||||
// Parse IP address and prefix
|
||||
ipNet, err := link.ParseIPv6Prefix(config.Prefix.String, 64) // Default to /64 if not specified
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse gateway
|
||||
gateway := net.ParseIP(config.Gateway.String)
|
||||
if gateway == nil {
|
||||
return nil, fmt.Errorf("invalid gateway: %s", config.Gateway.String)
|
||||
}
|
||||
|
||||
// Parse DNS servers
|
||||
var dns []net.IP
|
||||
for _, dnsStr := range config.DNS {
|
||||
dnsIP := net.ParseIP(dnsStr)
|
||||
if dnsIP == nil {
|
||||
return nil, fmt.Errorf("invalid DNS server: %s", dnsStr)
|
||||
}
|
||||
dns = append(dns, dnsIP)
|
||||
}
|
||||
|
||||
return &parsedIPv6Config{
|
||||
prefix: *ipNet,
|
||||
gateway: gateway,
|
||||
dns: dns,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// applyIPv4Address applies IPv4 address to interface
|
||||
func (scm *StaticConfigManager) applyIPv4Address(iface *link.Link, config *parsedIPv4Config) error {
|
||||
netlinkMgr := getNetlinkManager()
|
||||
|
||||
// Remove existing IPv4 addresses
|
||||
if err := netlinkMgr.RemoveAllAddresses(iface, link.AfInet); err != nil {
|
||||
return fmt.Errorf("failed to remove existing IPv4 addresses: %w", err)
|
||||
}
|
||||
|
||||
// Add new address
|
||||
addr := &netlink.Addr{
|
||||
IPNet: &config.network,
|
||||
}
|
||||
if err := netlinkMgr.AddrAdd(iface, addr); err != nil {
|
||||
return fmt.Errorf("failed to add IPv4 address: %w", err)
|
||||
}
|
||||
|
||||
scm.logger.Info().Str("address", config.network.String()).Msg("IPv4 address applied")
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyIPv6Address applies IPv6 address to interface
|
||||
func (scm *StaticConfigManager) applyIPv6Address(iface *link.Link, config *parsedIPv6Config) error {
|
||||
netlinkMgr := getNetlinkManager()
|
||||
|
||||
// Remove existing global IPv6 addresses
|
||||
if err := netlinkMgr.RemoveNonLinkLocalIPv6Addresses(iface); err != nil {
|
||||
return fmt.Errorf("failed to remove existing IPv6 addresses: %w", err)
|
||||
}
|
||||
|
||||
// Add new address
|
||||
addr := &netlink.Addr{
|
||||
IPNet: &config.prefix,
|
||||
}
|
||||
if err := netlinkMgr.AddrAdd(iface, addr); err != nil {
|
||||
return fmt.Errorf("failed to add IPv6 address: %w", err)
|
||||
}
|
||||
|
||||
scm.logger.Info().Str("address", config.prefix.String()).Msg("IPv6 address applied")
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyIPv4Route applies IPv4 default route
|
||||
func (scm *StaticConfigManager) applyIPv4Route(iface *link.Link, config *parsedIPv4Config) error {
|
||||
netlinkMgr := getNetlinkManager()
|
||||
|
||||
// Check if default route already exists
|
||||
if netlinkMgr.HasDefaultRoute(link.AfInet) {
|
||||
scm.logger.Info().Msg("IPv4 default route already exists")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add default route
|
||||
if err := netlinkMgr.AddDefaultRoute(iface, config.gateway, link.AfInet); err != nil {
|
||||
return fmt.Errorf("failed to add IPv4 default route: %w", err)
|
||||
}
|
||||
|
||||
scm.logger.Info().Str("gateway", config.gateway.String()).Msg("IPv4 default route applied")
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyIPv6Route applies IPv6 default route
|
||||
func (scm *StaticConfigManager) applyIPv6Route(iface *link.Link, config *parsedIPv6Config) error {
|
||||
netlinkMgr := getNetlinkManager()
|
||||
|
||||
// Check if default route already exists
|
||||
if netlinkMgr.HasDefaultRoute(link.AfInet6) {
|
||||
scm.logger.Info().Msg("IPv6 default route already exists")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add default route
|
||||
if err := netlinkMgr.AddDefaultRoute(iface, config.gateway, link.AfInet6); err != nil {
|
||||
return fmt.Errorf("failed to add IPv6 default route: %w", err)
|
||||
}
|
||||
|
||||
scm.logger.Info().Str("gateway", config.gateway.String()).Msg("IPv6 default route applied")
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeIPv4DefaultRoute removes IPv4 default route
|
||||
func (scm *StaticConfigManager) removeIPv4DefaultRoute() error {
|
||||
netlinkMgr := getNetlinkManager()
|
||||
|
|
@ -330,17 +188,3 @@ func (scm *StaticConfigManager) enableIPv6() error {
|
|||
netlinkMgr := getNetlinkManager()
|
||||
return netlinkMgr.EnableIPv6(scm.ifaceName)
|
||||
}
|
||||
|
||||
// parsedIPv4Config represents parsed IPv4 configuration
|
||||
type parsedIPv4Config struct {
|
||||
network net.IPNet
|
||||
gateway net.IP
|
||||
dns []net.IP
|
||||
}
|
||||
|
||||
// parsedIPv6Config represents parsed IPv6 configuration
|
||||
type parsedIPv6Config struct {
|
||||
prefix net.IPNet
|
||||
gateway net.IP
|
||||
dns []net.IP
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -25,6 +25,14 @@ export default function Ipv6NetworkCard({
|
|||
{networkState?.ipv6_link_local}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Gateway
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.ipv6_gateway}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 pt-2">
|
||||
|
|
|
|||
|
|
@ -709,6 +709,7 @@ export interface NetworkState {
|
|||
ipv6?: string;
|
||||
ipv6_addresses?: IPv6Address[];
|
||||
ipv6_link_local?: string;
|
||||
ipv6_gateway?: string;
|
||||
dhcp_lease?: DhcpLease;
|
||||
|
||||
setNetworkState: (state: NetworkState) => void;
|
||||
|
|
|
|||
|
|
@ -374,8 +374,8 @@ function UrlView({
|
|||
icon: FedoraIcon,
|
||||
},
|
||||
{
|
||||
name: "openSUSE Leap 15.6",
|
||||
url: "https://download.opensuse.org/distribution/leap/15.6/iso/openSUSE-Leap-15.6-NET-x86_64-Media.iso",
|
||||
name: "openSUSE Leap 16.0",
|
||||
url: "https://download.opensuse.org/distribution/leap/16.0/offline/Leap-16.0-online-installer-x86_64.install.iso",
|
||||
icon: OpenSUSEIcon,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import validator from "validator";
|
|||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||
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 { getNetworkSettings, getNetworkState } from "@/utils/jsonrpc";
|
||||
import { Button } from "@components/Button";
|
||||
|
|
@ -75,7 +75,8 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
|||
export default function SettingsNetworkRoute() {
|
||||
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
|
||||
const [customDomain, setCustomDomain] = useState<string>("");
|
||||
|
|
|
|||
|
|
@ -123,9 +123,9 @@ export default function KvmIdRoute() {
|
|||
|
||||
const params = useParams() as { id: string };
|
||||
const { sidebarView, setSidebarView, disableVideoFocusTrap } = useUiStore();
|
||||
const [ queryParams, setQueryParams ] = useSearchParams();
|
||||
const [queryParams, setQueryParams] = useSearchParams();
|
||||
|
||||
const {
|
||||
const {
|
||||
peerConnection, setPeerConnection,
|
||||
peerConnectionState, setPeerConnectionState,
|
||||
setMediaStream,
|
||||
|
|
@ -597,10 +597,10 @@ export default function KvmIdRoute() {
|
|||
});
|
||||
}, 10000);
|
||||
|
||||
const { setNetworkState} = useNetworkStateStore();
|
||||
const { setNetworkState } = useNetworkStateStore();
|
||||
const { setHdmiState } = useVideoStore();
|
||||
const {
|
||||
keyboardLedState, setKeyboardLedState,
|
||||
const {
|
||||
keyboardLedState, setKeyboardLedState,
|
||||
keysDownState, setKeysDownState, setUsbState,
|
||||
} = useHidStore();
|
||||
const setHidRpcDisabled = useRTCStore(state => state.setHidRpcDisabled);
|
||||
|
|
@ -756,7 +756,7 @@ export default function KvmIdRoute() {
|
|||
if (location.pathname !== "/other-session") navigateTo("/");
|
||||
}, [navigateTo, location.pathname]);
|
||||
|
||||
const { appVersion, getLocalVersion} = useVersion();
|
||||
const { appVersion, getLocalVersion } = useVersion();
|
||||
|
||||
useEffect(() => {
|
||||
if (appVersion) return;
|
||||
|
|
|
|||
Loading…
Reference in New Issue