kvm/network.go

424 lines
12 KiB
Go

package kvm
import (
"context"
"fmt"
"net"
"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()
}
// update the LLDP advertise options
if lldpService != nil {
_ = lldpService.SetAdvertiseOptions(getLLDPAdvertiseOptions(&state))
}
// 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(state *types.InterfaceState) *lldp.AdvertiseOptions {
a := &lldp.AdvertiseOptions{
SysDescription: toLLDPSysDescription(),
SysCapabilities: []string{"other", "router", "wlanap"},
EnabledCapabilities: []string{"other"},
}
if state == nil {
return a
}
a.SysName = state.Hostname
ip4String := state.IPv4Address
if ip4String != "" {
ip4 := net.ParseIP(ip4String)
a.IPv4Address = &ip4
}
ip6String := state.IPv6Address
if ip6String != "" {
ip6 := net.ParseIP(ip6String)
a.IPv6Address = &ip6
}
networkLogger.Info().Interface("advertiseOptions", a).Msg("LLDP advertise options")
return a
}
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
ifState, err := nm.GetInterfaceState(NetIfName)
if err != nil {
networkLogger.Warn().Err(err).Msg("failed to get interface state, LLDP will use the default options")
}
advertiseOptions := getLLDPAdvertiseOptions(ifState)
lldpService = lldp.NewLLDP(&lldp.Options{
InterfaceName: NetIfName,
EnableRx: nc.ShouldEnableLLDPReceive(),
EnableTx: nc.ShouldEnableLLDPTransmit(),
AdvertiseOptions: advertiseOptions,
OnChange: func(neighbors []lldp.Neighbor) {
// TODO: send deltas instead of the whole list
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, ifState *types.InterfaceState) {
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")
}
if ifState == nil {
newIfState, err := networkManager.GetInterfaceState(NetIfName)
if err != nil {
networkLogger.Warn().Err(err).Msg("failed to get interface state, LLDP will use the default options")
return
}
ifState = newIfState
}
advertiseOptions := getLLDPAdvertiseOptions(ifState)
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, nil)
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()
}