mirror of https://github.com/jetkvm/kvm.git
387 lines
10 KiB
Go
387 lines
10 KiB
Go
package kvm
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"reflect"
|
|
|
|
"github.com/jetkvm/kvm/internal/confparser"
|
|
"github.com/jetkvm/kvm/internal/lldp"
|
|
"github.com/jetkvm/kvm/internal/mdns"
|
|
"github.com/jetkvm/kvm/internal/network/types"
|
|
"github.com/jetkvm/kvm/pkg/nmlite"
|
|
)
|
|
|
|
const (
|
|
NetIfName = "eth0"
|
|
)
|
|
|
|
var (
|
|
networkManager *nmlite.NetworkManager
|
|
lldpService *lldp.LLDP
|
|
)
|
|
|
|
type RpcNetworkSettings struct {
|
|
types.NetworkConfig
|
|
}
|
|
|
|
func (s *RpcNetworkSettings) ToNetworkConfig() *types.NetworkConfig {
|
|
return &s.NetworkConfig
|
|
}
|
|
|
|
type PostRebootAction struct {
|
|
HealthCheck string `json:"healthCheck"`
|
|
RedirectTo string `json:"redirectTo"`
|
|
}
|
|
|
|
func toRpcNetworkSettings(config *types.NetworkConfig) *RpcNetworkSettings {
|
|
return &RpcNetworkSettings{
|
|
NetworkConfig: *config,
|
|
}
|
|
}
|
|
|
|
func getMdnsOptions() *mdns.MDNSOptions {
|
|
if networkManager == nil {
|
|
return nil
|
|
}
|
|
|
|
var ipv4, ipv6 bool
|
|
switch config.NetworkConfig.MDNSMode.String {
|
|
case "auto":
|
|
ipv4 = true
|
|
ipv6 = true
|
|
case "ipv4_only":
|
|
ipv4 = true
|
|
case "ipv6_only":
|
|
ipv6 = true
|
|
}
|
|
|
|
return &mdns.MDNSOptions{
|
|
LocalNames: []string{
|
|
networkManager.Hostname(),
|
|
networkManager.FQDN(),
|
|
},
|
|
ListenOptions: &mdns.MDNSListenOptions{
|
|
IPv4: ipv4,
|
|
IPv6: ipv6,
|
|
},
|
|
}
|
|
}
|
|
|
|
func restartMdns() {
|
|
if mDNS == nil {
|
|
return
|
|
}
|
|
|
|
options := getMdnsOptions()
|
|
if options == nil {
|
|
return
|
|
}
|
|
|
|
if err := mDNS.SetOptions(options); err != nil {
|
|
networkLogger.Error().Err(err).Msg("failed to restart mDNS")
|
|
}
|
|
}
|
|
|
|
func triggerTimeSyncOnNetworkStateChange() {
|
|
if timeSync == nil {
|
|
return
|
|
}
|
|
|
|
// set the NTP servers from the network manager
|
|
if networkManager != nil {
|
|
ntpServers := make([]string, len(networkManager.NTPServers()))
|
|
for i, server := range networkManager.NTPServers() {
|
|
ntpServers[i] = server.String()
|
|
}
|
|
networkLogger.Info().Strs("ntpServers", ntpServers).Msg("setting NTP servers from network manager")
|
|
timeSync.SetDhcpNtpAddresses(ntpServers)
|
|
}
|
|
|
|
// sync time
|
|
go func() {
|
|
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 currentSession != nil {
|
|
writeJSONRPCEvent("networkState", state.ToRpcInterfaceState(), currentSession)
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
func validateNetworkConfig() {
|
|
err := confparser.SetDefaultsAndValidate(config.NetworkConfig)
|
|
if err == nil {
|
|
return
|
|
}
|
|
|
|
networkLogger.Error().Err(err).Msg("failed to validate config, reverting to default config")
|
|
if err := SaveBackupConfig(); err != nil {
|
|
networkLogger.Error().Err(err).Msg("failed to save backup config")
|
|
}
|
|
|
|
// do not use a pointer to the default config
|
|
// it has been already changed during LoadConfig
|
|
config.NetworkConfig = &(types.NetworkConfig{})
|
|
if err := SaveConfig(); err != nil {
|
|
networkLogger.Error().Err(err).Msg("failed to save config")
|
|
}
|
|
}
|
|
|
|
func getLLDPAdvertiseOptions(nm *nmlite.NetworkManager) *lldp.AdvertiseOptions {
|
|
return &lldp.AdvertiseOptions{
|
|
SysName: nm.Hostname(),
|
|
SysDescription: toLLDPSysDescription(),
|
|
SysCapabilities: []string{"other", "router", "wlanap"},
|
|
EnabledCapabilities: []string{"other"},
|
|
}
|
|
}
|
|
|
|
func initNetwork() error {
|
|
ensureConfigLoaded()
|
|
|
|
// validate the config, if it's invalid, revert to the default config and save the backup
|
|
validateNetworkConfig()
|
|
|
|
nc := config.NetworkConfig
|
|
|
|
nm := nmlite.NewNetworkManager(context.Background(), networkLogger)
|
|
networkLogger.Info().Interface("networkConfig", nc).Str("hostname", nc.Hostname.String).Str("domain", nc.Domain.String).Msg("initializing network manager")
|
|
_ = setHostname(nm, nc.Hostname.String, nc.Domain.String)
|
|
nm.SetOnInterfaceStateChange(networkStateChanged)
|
|
if err := nm.AddInterface(NetIfName, nc); err != nil {
|
|
return fmt.Errorf("failed to add interface: %w", err)
|
|
}
|
|
_ = nm.CleanUpLegacyDHCPClients()
|
|
|
|
networkManager = nm
|
|
|
|
advertiseOptions := getLLDPAdvertiseOptions(nm)
|
|
lldpService = lldp.NewLLDP(&lldp.Options{
|
|
InterfaceName: NetIfName,
|
|
EnableRx: nc.ShouldEnableLLDPReceive(),
|
|
EnableTx: nc.ShouldEnableLLDPTransmit(),
|
|
AdvertiseOptions: advertiseOptions,
|
|
OnChange: func(neighbors []lldp.Neighbor) {
|
|
writeJSONRPCEvent("lldpNeighbors", neighbors, currentSession)
|
|
},
|
|
Logger: networkLogger,
|
|
})
|
|
if err := lldpService.Start(); err != nil {
|
|
networkLogger.Error().Err(err).Msg("failed to start LLDP service")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func toLLDPSysDescription() string {
|
|
systemVersion, appVersion, err := GetLocalVersion()
|
|
if err == nil {
|
|
return fmt.Sprintf("JetKVM (app: %s)", GetBuiltAppVersion())
|
|
}
|
|
|
|
return fmt.Sprintf("JetKVM (app: %s, system: %s)", appVersion.String(), systemVersion.String())
|
|
}
|
|
|
|
func updateLLDPOptions(nc *types.NetworkConfig) {
|
|
if lldpService == nil {
|
|
return
|
|
}
|
|
|
|
if err := lldpService.SetRxAndTx(nc.ShouldEnableLLDPReceive(), nc.ShouldEnableLLDPTransmit()); err != nil {
|
|
networkLogger.Error().Err(err).Msg("failed to set LLDP RX and TX")
|
|
}
|
|
|
|
advertiseOptions := getLLDPAdvertiseOptions(networkManager)
|
|
if err := lldpService.SetAdvertiseOptions(advertiseOptions); err != nil {
|
|
networkLogger.Error().Err(err).Msg("failed to set LLDP advertise options")
|
|
}
|
|
}
|
|
|
|
func setHostname(nm *nmlite.NetworkManager, hostname, domain string) error {
|
|
if nm == nil {
|
|
return nil
|
|
}
|
|
|
|
if hostname == "" {
|
|
hostname = GetDefaultHostname()
|
|
}
|
|
|
|
return nm.SetHostname(hostname, domain)
|
|
}
|
|
|
|
func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (rebootRequired bool, postRebootAction *PostRebootAction) {
|
|
rebootReasons := []string{}
|
|
defer func() {
|
|
if len(rebootReasons) > 0 {
|
|
networkLogger.Info().Strs("reasons", rebootReasons).Msg("reboot required")
|
|
}
|
|
}()
|
|
oldDhcpClient := oldConfig.DHCPClient.String
|
|
|
|
l := networkLogger.With().
|
|
Interface("old", oldConfig).
|
|
Interface("new", newConfig).
|
|
Logger()
|
|
|
|
// DHCP client change always requires reboot
|
|
newDhcpClient := newConfig.DHCPClient.String
|
|
if newDhcpClient != oldDhcpClient {
|
|
rebootRequired = true
|
|
rebootReasons = append(rebootReasons, fmt.Sprintf("DHCP client changed from %s to %s", oldDhcpClient, newDhcpClient))
|
|
return rebootRequired, postRebootAction
|
|
}
|
|
|
|
oldIPv4Mode := oldConfig.IPv4Mode.String
|
|
newIPv4Mode := newConfig.IPv4Mode.String
|
|
|
|
// IPv4 mode change requires reboot
|
|
if newIPv4Mode != oldIPv4Mode {
|
|
rebootRequired = true
|
|
rebootReasons = append(rebootReasons, fmt.Sprintf("IPv4 mode changed from %s to %s", oldIPv4Mode, newIPv4Mode))
|
|
|
|
if newIPv4Mode == "static" && oldIPv4Mode != "static" {
|
|
postRebootAction = &PostRebootAction{
|
|
HealthCheck: fmt.Sprintf("//%s/device/status", newConfig.IPv4Static.Address.String),
|
|
RedirectTo: fmt.Sprintf("//%s", newConfig.IPv4Static.Address.String),
|
|
}
|
|
l.Info().Interface("postRebootAction", postRebootAction).Msg("IPv4 mode changed to static, reboot required")
|
|
}
|
|
|
|
return rebootRequired, postRebootAction
|
|
}
|
|
|
|
// IPv4 static config changes require reboot
|
|
// but if it's not activated, don't care about the changes
|
|
if !reflect.DeepEqual(oldConfig.IPv4Static, newConfig.IPv4Static) && newIPv4Mode == "static" {
|
|
rebootRequired = true
|
|
// TODO: do not restart if it's just the DNS servers that changed
|
|
rebootReasons = append(rebootReasons, "IPv4 static config changed")
|
|
|
|
// Handle IP change for redirect (only if both are not nil and IP changed)
|
|
if newConfig.IPv4Static != nil && oldConfig.IPv4Static != nil &&
|
|
newConfig.IPv4Static.Address.String != oldConfig.IPv4Static.Address.String {
|
|
postRebootAction = &PostRebootAction{
|
|
HealthCheck: fmt.Sprintf("//%s/device/status", newConfig.IPv4Static.Address.String),
|
|
RedirectTo: fmt.Sprintf("//%s", newConfig.IPv4Static.Address.String),
|
|
}
|
|
}
|
|
|
|
return rebootRequired, postRebootAction
|
|
}
|
|
|
|
// IPv6 mode change requires reboot when using udhcpc
|
|
oldIPv6Mode := oldConfig.IPv6Mode.String
|
|
newIPv6Mode := newConfig.IPv6Mode.String
|
|
if newConfig.IPv6Mode.String != oldConfig.IPv6Mode.String && oldDhcpClient == "udhcpc" {
|
|
rebootRequired = true
|
|
rebootReasons = append(rebootReasons, fmt.Sprintf("IPv6 mode changed from %s to %s when using udhcpc", oldIPv6Mode, newIPv6Mode))
|
|
}
|
|
|
|
return rebootRequired, postRebootAction
|
|
}
|
|
|
|
func rpcGetNetworkState() *types.RpcInterfaceState {
|
|
state, _ := networkManager.GetInterfaceState(NetIfName)
|
|
return state.ToRpcInterfaceState()
|
|
}
|
|
|
|
func rpcGetNetworkSettings() *RpcNetworkSettings {
|
|
return toRpcNetworkSettings(config.NetworkConfig)
|
|
}
|
|
|
|
func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, error) {
|
|
netConfig := settings.ToNetworkConfig()
|
|
|
|
l := networkLogger.With().
|
|
Str("interface", NetIfName).
|
|
Interface("newConfig", netConfig).
|
|
Logger()
|
|
|
|
l.Debug().Msg("setting new config")
|
|
|
|
// TODO: do not restart everything if it's just the LLDP mode that changed
|
|
|
|
// Check if reboot is needed
|
|
rebootRequired, postRebootAction := shouldRebootForNetworkChange(config.NetworkConfig, netConfig)
|
|
|
|
// If reboot required, send willReboot event before applying network config
|
|
if rebootRequired {
|
|
l.Info().Msg("Sending willReboot event before applying network config")
|
|
writeJSONRPCEvent("willReboot", postRebootAction, currentSession)
|
|
}
|
|
|
|
_ = setHostname(networkManager, netConfig.Hostname.String, netConfig.Domain.String)
|
|
|
|
s := networkManager.SetInterfaceConfig(NetIfName, netConfig)
|
|
if s != nil {
|
|
return nil, s
|
|
}
|
|
l.Debug().Msg("new config applied")
|
|
|
|
newConfig, err := networkManager.GetInterfaceConfig(NetIfName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config.NetworkConfig = newConfig
|
|
|
|
// update the LLDP advertise options
|
|
updateLLDPOptions(newConfig)
|
|
|
|
l.Debug().Msg("saving new config")
|
|
if err := SaveConfig(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if rebootRequired {
|
|
l.Info().Msg("Rebooting due to network changes")
|
|
if err := hwReboot(true, postRebootAction, 0); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return toRpcNetworkSettings(newConfig), nil
|
|
}
|
|
|
|
func rpcRenewDHCPLease() error {
|
|
return networkManager.RenewDHCPLease(NetIfName)
|
|
}
|
|
|
|
func rpcToggleDHCPClient() error {
|
|
switch config.NetworkConfig.DHCPClient.String {
|
|
case "jetdhcpc":
|
|
config.NetworkConfig.DHCPClient.String = "udhcpc"
|
|
case "udhcpc":
|
|
config.NetworkConfig.DHCPClient.String = "jetdhcpc"
|
|
}
|
|
|
|
if err := SaveConfig(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return rpcReboot(true)
|
|
}
|
|
|
|
func rpcGetLLDPNeighbors() []lldp.Neighbor {
|
|
if lldpService == nil {
|
|
return []lldp.Neighbor{}
|
|
}
|
|
return lldpService.GetNeighbors()
|
|
}
|