mirror of https://github.com/jetkvm/kvm.git
feat: enhance network change handling and reboot logic
This commit is contained in:
parent
895cd5cc39
commit
1cf7dd4be9
61
network.go
61
network.go
|
|
@ -3,6 +3,7 @@ package kvm
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/confparser"
|
"github.com/jetkvm/kvm/internal/confparser"
|
||||||
"github.com/jetkvm/kvm/internal/mdns"
|
"github.com/jetkvm/kvm/internal/mdns"
|
||||||
|
|
@ -170,6 +171,54 @@ func setHostname(nm *nmlite.NetworkManager, hostname, domain string) error {
|
||||||
return nm.SetHostname(hostname, domain)
|
return nm.SetHostname(hostname, domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (bool, *string) {
|
||||||
|
var rebootRequired bool
|
||||||
|
var suggestedIp *string
|
||||||
|
|
||||||
|
oldDhcpClient := oldConfig.DHCPClient.String
|
||||||
|
|
||||||
|
// DHCP client change always requires reboot
|
||||||
|
if newConfig.DHCPClient.String != oldDhcpClient {
|
||||||
|
rebootRequired = true
|
||||||
|
networkLogger.Info().Str("old", oldDhcpClient).Str("new", newConfig.DHCPClient.String).Msg("DHCP client changed, reboot required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv4 mode change requires reboot when using udhcpc
|
||||||
|
if newConfig.IPv4Mode.String != oldConfig.IPv4Mode.String && oldDhcpClient == "udhcpc" {
|
||||||
|
rebootRequired = true
|
||||||
|
networkLogger.Info().Str("old", oldConfig.IPv4Mode.String).Str("new", newConfig.IPv4Mode.String).Msg("IPv4 mode changed with udhcpc, reboot required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv4 static config changes require reboot
|
||||||
|
if newConfig.IPv4Static != nil && oldConfig.IPv4Static != nil {
|
||||||
|
if newConfig.IPv4Static.Address.String != oldConfig.IPv4Static.Address.String {
|
||||||
|
rebootRequired = true
|
||||||
|
suggestedIp = &newConfig.IPv4Static.Address.String
|
||||||
|
networkLogger.Info().Str("old", oldConfig.IPv4Static.Address.String).Str("new", newConfig.IPv4Static.Address.String).Msg("IPv4 address changed, reboot required")
|
||||||
|
}
|
||||||
|
if newConfig.IPv4Static.Netmask.String != oldConfig.IPv4Static.Netmask.String {
|
||||||
|
rebootRequired = true
|
||||||
|
networkLogger.Info().Str("old", oldConfig.IPv4Static.Netmask.String).Str("new", newConfig.IPv4Static.Netmask.String).Msg("IPv4 netmask changed, reboot required")
|
||||||
|
}
|
||||||
|
if newConfig.IPv4Static.Gateway.String != oldConfig.IPv4Static.Gateway.String {
|
||||||
|
rebootRequired = true
|
||||||
|
networkLogger.Info().Str("old", oldConfig.IPv4Static.Gateway.String).Str("new", newConfig.IPv4Static.Gateway.String).Msg("IPv4 gateway changed, reboot required")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(newConfig.IPv4Static.DNS, oldConfig.IPv4Static.DNS) {
|
||||||
|
rebootRequired = true
|
||||||
|
networkLogger.Info().Strs("old", oldConfig.IPv4Static.DNS).Strs("new", newConfig.IPv4Static.DNS).Msg("IPv4 DNS changed, reboot required")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv6 mode change requires reboot when using udhcpc
|
||||||
|
if newConfig.IPv6Mode.String != oldConfig.IPv6Mode.String && oldDhcpClient == "udhcpc" {
|
||||||
|
rebootRequired = true
|
||||||
|
networkLogger.Info().Str("old", oldConfig.IPv6Mode.String).Str("new", newConfig.IPv6Mode.String).Msg("IPv6 mode changed with udhcpc, reboot required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return rebootRequired, suggestedIp
|
||||||
|
}
|
||||||
|
|
||||||
func rpcGetNetworkState() *types.RpcInterfaceState {
|
func rpcGetNetworkState() *types.RpcInterfaceState {
|
||||||
state, _ := networkManager.GetInterfaceState(NetIfName)
|
state, _ := networkManager.GetInterfaceState(NetIfName)
|
||||||
return state.ToRpcInterfaceState()
|
return state.ToRpcInterfaceState()
|
||||||
|
|
@ -189,9 +238,13 @@ func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, er
|
||||||
|
|
||||||
l.Debug().Msg("setting new config")
|
l.Debug().Msg("setting new config")
|
||||||
|
|
||||||
var rebootRequired bool
|
// Check if reboot is needed
|
||||||
if netConfig.DHCPClient.String != config.NetworkConfig.DHCPClient.String {
|
rebootRequired, suggestedIp := shouldRebootForNetworkChange(config.NetworkConfig, netConfig)
|
||||||
rebootRequired = true
|
|
||||||
|
// If reboot required, send willReboot event before applying network config
|
||||||
|
if rebootRequired {
|
||||||
|
l.Info().Msg("Sending willReboot event before applying network config")
|
||||||
|
writeJSONRPCEvent("willReboot", suggestedIp, currentSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = setHostname(networkManager, netConfig.Hostname.String, netConfig.Domain.String)
|
_ = setHostname(networkManager, netConfig.Hostname.String, netConfig.Domain.String)
|
||||||
|
|
@ -238,5 +291,5 @@ func rpcToggleDHCPClient() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return rpcReboot(false)
|
return rpcReboot(true)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import {
|
||||||
ConnectionFailedOverlay,
|
ConnectionFailedOverlay,
|
||||||
LoadingConnectionOverlay,
|
LoadingConnectionOverlay,
|
||||||
PeerConnectionDisconnectedOverlay,
|
PeerConnectionDisconnectedOverlay,
|
||||||
|
RebootingOverlay,
|
||||||
} from "@/components/VideoOverlay";
|
} from "@/components/VideoOverlay";
|
||||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||||
import { FeatureFlagProvider } from "@/providers/FeatureFlagProvider";
|
import { FeatureFlagProvider } from "@/providers/FeatureFlagProvider";
|
||||||
|
|
@ -122,7 +123,7 @@ export default function KvmIdRoute() {
|
||||||
const authMode = "authMode" in loaderResp ? loaderResp.authMode : null;
|
const authMode = "authMode" in loaderResp ? loaderResp.authMode : null;
|
||||||
|
|
||||||
const params = useParams() as { id: string };
|
const params = useParams() as { id: string };
|
||||||
const { sidebarView, setSidebarView, disableVideoFocusTrap } = useUiStore();
|
const { sidebarView, setSidebarView, disableVideoFocusTrap, rebootState, setRebootState } = useUiStore();
|
||||||
const [queryParams, setQueryParams] = useSearchParams();
|
const [queryParams, setQueryParams] = useSearchParams();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -241,7 +242,7 @@ export default function KvmIdRoute() {
|
||||||
{
|
{
|
||||||
heartbeat: true,
|
heartbeat: true,
|
||||||
retryOnError: true,
|
retryOnError: true,
|
||||||
reconnectAttempts: 15,
|
reconnectAttempts: 2000,
|
||||||
reconnectInterval: 1000,
|
reconnectInterval: 1000,
|
||||||
onReconnectStop: () => {
|
onReconnectStop: () => {
|
||||||
console.debug("Reconnect stopped");
|
console.debug("Reconnect stopped");
|
||||||
|
|
@ -250,8 +251,7 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
shouldReconnect(event) {
|
shouldReconnect(event) {
|
||||||
console.debug("[Websocket] shouldReconnect", event);
|
console.debug("[Websocket] shouldReconnect", event);
|
||||||
// TODO: Why true?
|
return isLegacySignalingEnabled.current;
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onClose(event) {
|
onClose(event) {
|
||||||
|
|
@ -265,6 +265,16 @@ export default function KvmIdRoute() {
|
||||||
},
|
},
|
||||||
onOpen() {
|
onOpen() {
|
||||||
console.debug("[Websocket] onOpen");
|
console.debug("[Websocket] onOpen");
|
||||||
|
// We want to clear the reboot state when the websocket connection is opened
|
||||||
|
// Currently the flow is:
|
||||||
|
// 1. User clicks reboot
|
||||||
|
// 2. Device sends event 'willReboot'
|
||||||
|
// 3. We set the reboot state
|
||||||
|
// 4. Reboot modal is shown
|
||||||
|
// 5. WS tries to reconnect
|
||||||
|
// 6. WS reconnects
|
||||||
|
// 7. This function is called and now we clear the reboot state
|
||||||
|
setRebootState({ isRebooting: false, suggestedIp: null });
|
||||||
},
|
},
|
||||||
|
|
||||||
onMessage: message => {
|
onMessage: message => {
|
||||||
|
|
@ -340,10 +350,7 @@ export default function KvmIdRoute() {
|
||||||
peerConnection.addIceCandidate(candidate);
|
peerConnection.addIceCandidate(candidate);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
|
|
||||||
// Don't even retry once we declare failure
|
|
||||||
!connectionFailed && isLegacySignalingEnabled.current === false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const sendWebRTCSignal = useCallback(
|
const sendWebRTCSignal = useCallback(
|
||||||
|
|
@ -594,6 +601,8 @@ export default function KvmIdRoute() {
|
||||||
api.POST(`${CLOUD_API}/webrtc/turn_activity`, {
|
api.POST(`${CLOUD_API}/webrtc/turn_activity`, {
|
||||||
bytesReceived: bytesReceivedDelta,
|
bytesReceived: bytesReceivedDelta,
|
||||||
bytesSent: bytesSentDelta,
|
bytesSent: bytesSentDelta,
|
||||||
|
}).catch(() => {
|
||||||
|
// we don't care about errors here, but we don't want unhandled promise rejections
|
||||||
});
|
});
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
|
|
@ -666,6 +675,12 @@ export default function KvmIdRoute() {
|
||||||
window.location.href = currentUrl.toString();
|
window.location.href = currentUrl.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resp.method === "willReboot") {
|
||||||
|
const suggestedIp = resp.params as unknown as string | null;
|
||||||
|
setRebootState({ isRebooting: true, suggestedIp });
|
||||||
|
navigateTo("/");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { send } = useJsonRpc(onJsonRpcRequest);
|
const { send } = useJsonRpc(onJsonRpcRequest);
|
||||||
|
|
@ -765,6 +780,14 @@ export default function KvmIdRoute() {
|
||||||
}, [appVersion, getLocalVersion]);
|
}, [appVersion, getLocalVersion]);
|
||||||
|
|
||||||
const ConnectionStatusElement = useMemo(() => {
|
const ConnectionStatusElement = useMemo(() => {
|
||||||
|
const isOtherSession = location.pathname.includes("other-session");
|
||||||
|
if (isOtherSession) return null;
|
||||||
|
|
||||||
|
// Rebooting takes priority over connection status
|
||||||
|
if (rebootState?.isRebooting) {
|
||||||
|
return <RebootingOverlay show={true} suggestedIp={rebootState.suggestedIp} />;
|
||||||
|
}
|
||||||
|
|
||||||
const hasConnectionFailed =
|
const hasConnectionFailed =
|
||||||
connectionFailed || ["failed", "closed"].includes(peerConnectionState ?? "");
|
connectionFailed || ["failed", "closed"].includes(peerConnectionState ?? "");
|
||||||
|
|
||||||
|
|
@ -774,9 +797,6 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
const isDisconnected = peerConnectionState === "disconnected";
|
const isDisconnected = peerConnectionState === "disconnected";
|
||||||
|
|
||||||
const isOtherSession = location.pathname.includes("other-session");
|
|
||||||
|
|
||||||
if (isOtherSession) return null;
|
|
||||||
if (peerConnectionState === "connected") return null;
|
if (peerConnectionState === "connected") return null;
|
||||||
if (isDisconnected) {
|
if (isDisconnected) {
|
||||||
return <PeerConnectionDisconnectedOverlay show={true} />;
|
return <PeerConnectionDisconnectedOverlay show={true} />;
|
||||||
|
|
@ -792,14 +812,7 @@ export default function KvmIdRoute() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, [
|
}, [location.pathname, rebootState?.isRebooting, rebootState?.suggestedIp, connectionFailed, peerConnectionState, peerConnection, setupPeerConnection, loadingMessage]);
|
||||||
connectionFailed,
|
|
||||||
loadingMessage,
|
|
||||||
location.pathname,
|
|
||||||
peerConnection,
|
|
||||||
peerConnectionState,
|
|
||||||
setupPeerConnection,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FeatureFlagProvider appVersion={appVersion}>
|
<FeatureFlagProvider appVersion={appVersion}>
|
||||||
|
|
@ -841,7 +854,7 @@ export default function KvmIdRoute() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="relative flex h-full w-full overflow-hidden">
|
<div className="relative flex h-full w-full overflow-hidden">
|
||||||
<WebRTCVideo />
|
<WebRTCVideo hasConnectionIssues={!!ConnectionStatusElement} />
|
||||||
<div
|
<div
|
||||||
style={{ animationDuration: "500ms" }}
|
style={{ animationDuration: "500ms" }}
|
||||||
className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center p-4"
|
className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center p-4"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue