From a9767b650ca7bd1fb4100380a45e1be127e8934c Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Tue, 11 Feb 2025 13:51:18 +0000 Subject: [PATCH 01/12] fix(cloud): only start WS Client if config.CloudToken is set (#27) --- cloud.go | 5 +++++ main.go | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/cloud.go b/cloud.go index db47727..5088ec7 100644 --- a/cloud.go +++ b/cloud.go @@ -68,6 +68,11 @@ func handleCloudRegister(c *gin.Context) { return } + if config.CloudToken == "" { + logger.Info("Starting websocket client due to adoption") + go RunWebsocketClient() + } + config.CloudToken = tokenResp.SecretToken config.CloudURL = req.CloudAPI diff --git a/main.go b/main.go index f94b24e..7ff771f 100644 --- a/main.go +++ b/main.go @@ -66,7 +66,11 @@ func Main() { }() //go RunFuseServer() go RunWebServer() - go RunWebsocketClient() + // If the cloud token isn't set, the client won't be started by default. + // However, if the user adopts the device via the web interface, handleCloudRegister will start the client. + if config.CloudToken != "" { + go RunWebsocketClient() + } sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs From 727561738ef1c220e2e1184b599f709a30e489f1 Mon Sep 17 00:00:00 2001 From: Brandon Tuttle <11356668+tutman96@users.noreply.github.com> Date: Tue, 11 Feb 2025 08:55:02 -0500 Subject: [PATCH 02/12] Clean up native subprocess is main process dies (#19) --- native.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/native.go b/native.go index 89e6803..d34ab07 100644 --- a/native.go +++ b/native.go @@ -11,6 +11,7 @@ import ( "os" "os/exec" "sync" + "syscall" "time" "github.com/pion/webrtc/v4/pkg/media" @@ -224,6 +225,12 @@ func ExtractAndRunNativeBin() error { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr + // Set the process group ID so we can kill the process and its children when this process exits + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + Pdeathsig: syscall.SIGKILL, + } + // Start the command if err := cmd.Start(); err != nil { return fmt.Errorf("failed to start binary: %w", err) From 2e8ea8cccc86b65c94ae12c8e15829cb5a3b8947 Mon Sep 17 00:00:00 2001 From: Antony Messerli Date: Tue, 11 Feb 2025 08:13:29 -0600 Subject: [PATCH 03/12] Update to latest ISO versions (#78) * Fedora 38 is EOL, bump to 41 and use main Fedora mirror * Bumps Arch Linux and Debian to latest builds --- ui/src/components/MountMediaDialog.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/src/components/MountMediaDialog.tsx b/ui/src/components/MountMediaDialog.tsx index 505d233..4aca608 100644 --- a/ui/src/components/MountMediaDialog.tsx +++ b/ui/src/components/MountMediaDialog.tsx @@ -534,17 +534,17 @@ function UrlView({ }, { name: "Debian 12", - url: "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-12.7.0-amd64-netinst.iso", + url: "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-12.9.0-amd64-netinst.iso", icon: DebianIcon, }, { - name: "Fedora 38", - url: "https://mirror.ihost.md/fedora/releases/38/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-38-1.6.iso", + name: "Fedora 41", + url: "https://download.fedoraproject.org/pub/fedora/linux/releases/41/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-41-1.4.iso", icon: FedoraIcon, }, { name: "Arch Linux", - url: "https://archlinux.doridian.net/iso/2024.10.01/archlinux-2024.10.01-x86_64.iso", + url: "https://archlinux.doridian.net/iso/2025.02.01/archlinux-2025.02.01-x86_64.iso", icon: ArchIcon, }, { From 15768ee0ab15a3f0ce9da58fa1a097f6fe11e9c7 Mon Sep 17 00:00:00 2001 From: Brandon Tuttle <11356668+tutman96@users.noreply.github.com> Date: Tue, 11 Feb 2025 09:13:41 -0500 Subject: [PATCH 04/12] Remove rounded corners from video stream (#86) --- ui/src/components/WebRTCVideo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index f5f083b..7603369 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -425,7 +425,7 @@ export default function WebRTCVideo() { disablePictureInPicture controlsList="nofullscreen" className={cx( - "outline-50 max-h-full max-w-full rounded-md object-contain transition-all duration-1000", + "outline-50 max-h-full max-w-full object-contain transition-all duration-1000", { "cursor-none": settings.isCursorHidden, "opacity-0": isLoading || isConnectionError || hdmiError, From 0d7efe5c0e240143dc88674748ffe5dd53f57842 Mon Sep 17 00:00:00 2001 From: Aveline Date: Tue, 11 Feb 2025 15:45:14 +0100 Subject: [PATCH 05/12] feat: add ICE servers and local IP address returned by the API to fix connectivity issues behind NAT (#146) Add ICE servers and local IP address returned by the API to fix connectivity issues behind NAT --- cloud.go | 11 ++++++++--- web.go | 8 +++++--- webrtc.go | 33 ++++++++++++++++++++++++++++++--- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/cloud.go b/cloud.go index 5088ec7..3520e2f 100644 --- a/cloud.go +++ b/cloud.go @@ -7,13 +7,14 @@ import ( "fmt" "net/http" "net/url" - "github.com/coder/websocket/wsjson" "time" + "github.com/coder/websocket/wsjson" + "github.com/coreos/go-oidc/v3/oidc" - "github.com/gin-gonic/gin" "github.com/coder/websocket" + "github.com/gin-gonic/gin" ) type CloudRegisterRequest struct { @@ -192,7 +193,11 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess return fmt.Errorf("google identity mismatch") } - session, err := newSession() + session, err := newSession(SessionConfig{ + ICEServers: req.ICEServers, + LocalIP: req.IP, + IsCloud: true, + }) if err != nil { _ = wsjson.Write(context.Background(), c, gin.H{"error": err}) return err diff --git a/web.go b/web.go index 64f8de7..02c7eea 100644 --- a/web.go +++ b/web.go @@ -17,8 +17,10 @@ import ( var staticFiles embed.FS type WebRTCSessionRequest struct { - Sd string `json:"sd"` - OidcGoogle string `json:"OidcGoogle,omitempty"` + Sd string `json:"sd"` + OidcGoogle string `json:"OidcGoogle,omitempty"` + IP string `json:"ip,omitempty"` + ICEServers []string `json:"iceServers,omitempty"` } type SetPasswordRequest struct { @@ -116,7 +118,7 @@ func handleWebRTCSession(c *gin.Context) { return } - session, err := newSession() + session, err := newSession(SessionConfig{}) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err}) return diff --git a/webrtc.go b/webrtc.go index 20ffb99..27084fc 100644 --- a/webrtc.go +++ b/webrtc.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "net" "strings" "github.com/pion/webrtc/v4" @@ -19,6 +20,12 @@ type Session struct { shouldUmountVirtualMedia bool } +type SessionConfig struct { + ICEServers []string + LocalIP string + IsCloud bool +} + func (s *Session) ExchangeOffer(offerStr string) (string, error) { b, err := base64.StdEncoding.DecodeString(offerStr) if err != nil { @@ -61,9 +68,29 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) { return base64.StdEncoding.EncodeToString(localDescription), nil } -func newSession() (*Session, error) { - peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{ - ICEServers: []webrtc.ICEServer{{}}, +func newSession(config SessionConfig) (*Session, error) { + webrtcSettingEngine := webrtc.SettingEngine{} + iceServer := webrtc.ICEServer{} + + if config.IsCloud { + if config.ICEServers == nil { + fmt.Printf("ICE Servers not provided by cloud") + } else { + iceServer.URLs = config.ICEServers + fmt.Printf("Using ICE Servers provided by cloud: %v\n", iceServer.URLs) + } + + if config.LocalIP == "" || net.ParseIP(config.LocalIP) == nil { + fmt.Printf("Local IP address %v not provided or invalid, won't set NAT1To1IPs\n", config.LocalIP) + } else { + webrtcSettingEngine.SetNAT1To1IPs([]string{config.LocalIP}, webrtc.ICECandidateTypeSrflx) + fmt.Printf("Setting NAT1To1IPs to %s\n", config.LocalIP) + } + } + + api := webrtc.NewAPI(webrtc.WithSettingEngine(webrtcSettingEngine)) + peerConnection, err := api.NewPeerConnection(webrtc.Configuration{ + ICEServers: []webrtc.ICEServer{iceServer}, }) if err != nil { return nil, err From 69168ff062b475155a6bf7b16e9b83fbc63649ee Mon Sep 17 00:00:00 2001 From: Brandon Tuttle <11356668+tutman96@users.noreply.github.com> Date: Tue, 11 Feb 2025 14:00:50 -0500 Subject: [PATCH 06/12] Fix fullscreen video relative mouse movements (#85) --- ui/package.json | 1 + ui/src/components/ActionBar.tsx | 3 +++ ui/src/components/WebRTCVideo.tsx | 41 +++++++++++++++++++++++++------ ui/vite.config.ts | 24 +++++++++++++++--- 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/ui/package.json b/ui/package.json index 592a300..9a7fae5 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,6 +10,7 @@ "dev": "vite dev --mode=development", "build": "npm run build:prod", "build:device": "tsc && vite build --mode=device --emptyOutDir", + "dev:device": "vite dev --mode=device", "build:prod": "tsc && vite build --mode=production", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" }, diff --git a/ui/src/components/ActionBar.tsx b/ui/src/components/ActionBar.tsx index cd5432c..13ab896 100644 --- a/ui/src/components/ActionBar.tsx +++ b/ui/src/components/ActionBar.tsx @@ -4,6 +4,7 @@ import { useMountMediaStore, useUiStore, useSettingsStore, + useVideoStore, } from "@/hooks/stores"; import { MdOutlineContentPasteGo } from "react-icons/md"; import Container from "@components/Container"; @@ -33,6 +34,7 @@ export default function Actionbar({ state => state.remoteVirtualMediaState, ); const developerMode = useSettingsStore(state => state.developerMode); + const hdmiState = useVideoStore(state => state.hdmiState); // This is the only way to get a reliable state change for the popover // at time of writing this there is no mount, or unmount event for the popover @@ -247,6 +249,7 @@ export default function Actionbar({ size="XS" theme="light" text="Fullscreen" + disabled={hdmiState !== 'ready'} LeadingIcon={LuMaximize} onClick={() => requestFullscreen()} /> diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 7603369..1e5699c 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -30,6 +30,8 @@ export default function WebRTCVideo() { const { setClientSize: setVideoClientSize, setSize: setVideoSize, + width: videoWidth, + height: videoHeight, clientWidth: videoClientWidth, clientHeight: videoClientHeight, } = useVideoStore(); @@ -102,20 +104,43 @@ export default function WebRTCVideo() { const mouseMoveHandler = useCallback( (e: MouseEvent) => { if (!videoClientWidth || !videoClientHeight) return; - const { buttons } = e; + // Get the aspect ratios of the video element and the video stream + const videoElementAspectRatio = videoClientWidth / videoClientHeight; + const videoStreamAspectRatio = videoWidth / videoHeight; - // Clamp mouse position within the video boundaries - const currMouseX = Math.min(Math.max(1, e.offsetX), videoClientWidth); - const currMouseY = Math.min(Math.max(1, e.offsetY), videoClientHeight); + // Calculate the effective video display area + let effectiveWidth = videoClientWidth; + let effectiveHeight = videoClientHeight; + let offsetX = 0; + let offsetY = 0; - // Normalize mouse position to 0-32767 range (HID absolute coordinate system) - const x = Math.round((currMouseX / videoClientWidth) * 32767); - const y = Math.round((currMouseY / videoClientHeight) * 32767); + if (videoElementAspectRatio > videoStreamAspectRatio) { + // Pillarboxing: black bars on the left and right + effectiveWidth = videoClientHeight * videoStreamAspectRatio; + offsetX = (videoClientWidth - effectiveWidth) / 2; + } else if (videoElementAspectRatio < videoStreamAspectRatio) { + // Letterboxing: black bars on the top and bottom + effectiveHeight = videoClientWidth / videoStreamAspectRatio; + offsetY = (videoClientHeight - effectiveHeight) / 2; + } + + // Clamp mouse position within the effective video boundaries + const clampedX = Math.min(Math.max(offsetX, e.offsetX), offsetX + effectiveWidth); + const clampedY = Math.min(Math.max(offsetY, e.offsetY), offsetY + effectiveHeight); + + // Map clamped mouse position to the video stream's coordinate system + const relativeX = (clampedX - offsetX) / effectiveWidth; + const relativeY = (clampedY - offsetY) / effectiveHeight; + + // Convert to HID absolute coordinate system (0-32767 range) + const x = Math.round(relativeX * 32767); + const y = Math.round(relativeY * 32767); // Send mouse movement + const { buttons } = e; sendMouseMovement(x, y, buttons); }, - [sendMouseMovement, videoClientHeight, videoClientWidth], + [sendMouseMovement, videoClientHeight, videoClientWidth, videoWidth, videoHeight], ); const mouseWheelHandler = useCallback( diff --git a/ui/vite.config.ts b/ui/vite.config.ts index e9c7fe5..b6d26f6 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -2,13 +2,31 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react-swc"; import tsconfigPaths from "vite-tsconfig-paths"; -export default defineConfig(({ mode }) => { +declare const process: { + env: { + JETKVM_PROXY_URL: string; + }; +}; + +export default defineConfig(({ mode, command }) => { const isCloud = mode === "production"; const onDevice = mode === "device"; + const { JETKVM_PROXY_URL } = process.env; + return { plugins: [tsconfigPaths(), react()], build: { outDir: isCloud ? "dist" : "../static" }, - server: { host: "0.0.0.0" }, - base: onDevice ? "/static" : "/", + server: { + host: "0.0.0.0", + proxy: JETKVM_PROXY_URL ? { + '/me': JETKVM_PROXY_URL, + '/device': JETKVM_PROXY_URL, + '/webrtc': JETKVM_PROXY_URL, + '/auth': JETKVM_PROXY_URL, + '/storage': JETKVM_PROXY_URL, + '/cloud': JETKVM_PROXY_URL, + } : undefined + }, + base: onDevice && command === 'build' ? "/static" : "/", }; }); From 63b3ef015188501ffcc3d9f63a5376d07b018c6a Mon Sep 17 00:00:00 2001 From: Andrew Nicholson Date: Wed, 12 Feb 2025 14:08:03 +0000 Subject: [PATCH 07/12] Enable "Boot Interface Subclass" for keyboard and mouse. (#113) This is often required for the keyboard/mouse to be recognized in BIOS/UEFI firmware. --- usb.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/usb.go b/usb.go index 075409a..e302815 100644 --- a/usb.go +++ b/usb.go @@ -132,7 +132,7 @@ func writeGadgetConfig() error { } err = writeGadgetAttrs(hid0Path, [][]string{ {"protocol", "1"}, - {"subclass", "0"}, + {"subclass", "1"}, {"report_length", "8"}, }) if err != nil { @@ -152,7 +152,7 @@ func writeGadgetConfig() error { } err = writeGadgetAttrs(hid1Path, [][]string{ {"protocol", "2"}, - {"subclass", "0"}, + {"subclass", "1"}, {"report_length", "6"}, }) if err != nil { From aa0f38bc0bbcb8c581a91edda8bd4c3772a3293b Mon Sep 17 00:00:00 2001 From: Dominik Heidler Date: Thu, 13 Feb 2025 14:05:07 +0100 Subject: [PATCH 08/12] Add openSUSE ISOs (#151) --- ui/src/components/MountMediaDialog.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ui/src/components/MountMediaDialog.tsx b/ui/src/components/MountMediaDialog.tsx index 4aca608..c3ef59d 100644 --- a/ui/src/components/MountMediaDialog.tsx +++ b/ui/src/components/MountMediaDialog.tsx @@ -26,6 +26,7 @@ import { InputFieldWithLabel } from "./InputField"; import DebianIcon from "@/assets/debian-icon.png"; import UbuntuIcon from "@/assets/ubuntu-icon.png"; import FedoraIcon from "@/assets/fedora-icon.png"; +import OpenSUSEIcon from "@/assets/opensuse-icon.png"; import ArchIcon from "@/assets/arch-icon.png"; import NetBootIcon from "@/assets/netboot-icon.svg"; import { TrashIcon } from "@heroicons/react/16/solid"; @@ -542,6 +543,16 @@ function UrlView({ url: "https://download.fedoraproject.org/pub/fedora/linux/releases/41/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-41-1.4.iso", 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", + icon: OpenSUSEIcon, + }, + { + name: "openSUSE Tumbleweed", + url: "https://download.opensuse.org/tumbleweed/iso/openSUSE-Tumbleweed-NET-x86_64-Current.iso", + icon: OpenSUSEIcon, + }, { name: "Arch Linux", url: "https://archlinux.doridian.net/iso/2025.02.01/archlinux-2025.02.01-x86_64.iso", From d07bedb323f8cdfaa5ec29a978b17bbd878b3743 Mon Sep 17 00:00:00 2001 From: Scai <59282365+alexevladgabriel@users.noreply.github.com> Date: Thu, 13 Feb 2025 15:33:03 +0200 Subject: [PATCH 09/12] Invert colors on Icons (#123) * feat(ui): invert colors on icons * feat(ui): fix tailwindcss class for invert --- ui/src/components/sidebar/settings.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/components/sidebar/settings.tsx b/ui/src/components/sidebar/settings.tsx index ec606a6..db43d75 100644 --- a/ui/src/components/sidebar/settings.tsx +++ b/ui/src/components/sidebar/settings.tsx @@ -466,7 +466,7 @@ export default function SettingsSidebar() {
Finger touching a screen @@ -490,7 +490,7 @@ export default function SettingsSidebar() { >
- Mouse icon + Mouse icon

From 0b5033f798e6179775a02eb50bba837b7f6fa2e8 Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Thu, 13 Feb 2025 13:42:07 +0000 Subject: [PATCH 10/12] feat: restore EDID on reboot (#34) This commit adds the config entry "EdidString" and saves the EDID string when it's modified via the RPC. The EDID is restored when the jetkvm_native control socket connects (usually at boot) Signed-off-by: Cameron Fleming --- config.go | 1 + jsonrpc.go | 6 ++++++ native.go | 16 ++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/config.go b/config.go index 1636434..ceacfe5 100644 --- a/config.go +++ b/config.go @@ -22,6 +22,7 @@ type Config struct { LocalAuthToken string `json:"local_auth_token"` LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` + EdidString string `json:"hdmi_edid_string"` } const configPath = "/userdata/kvm_config.json" diff --git a/jsonrpc.go b/jsonrpc.go index 2ce5f18..4f6519b 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -183,6 +183,12 @@ func rpcSetEDID(edid string) error { if err != nil { return err } + + // Save EDID to config, allowing it to be restored on reboot. + LoadConfig() + config.EdidString = edid + SaveConfig() + return nil } diff --git a/native.go b/native.go index d34ab07..1bd8429 100644 --- a/native.go +++ b/native.go @@ -152,6 +152,9 @@ func handleCtrlClient(conn net.Conn) { ctrlSocketConn = conn + // Restore HDMI EDID if applicable + go restoreHdmiEdid() + readBuf := make([]byte, 4096) for { n, err := conn.Read(readBuf) @@ -304,3 +307,16 @@ func ensureBinaryUpdated(destPath string) error { return nil } + +// Restore the HDMI EDID value from the config. +// Called after successful connection to jetkvm_native. +func restoreHdmiEdid() { + LoadConfig() + if config.EdidString != "" { + logger.Infof("Restoring HDMI EDID to %v", config.EdidString) + _, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": config.EdidString}) + if err != nil { + logger.Errorf("Failed to restore HDMI EDID: %v", err) + } + } +} From 2a99c2db9d23640e254e67bdfb9957db3d2634aa Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Thu, 13 Feb 2025 14:41:10 +0000 Subject: [PATCH 11/12] fix(net): stop dhcp client and release all v4 addr on linkdown (#16) This commit fixes jetkvm/kvm#12 by disabling the udhcpc client when the link goes down, it then removes all the active IPv4 addresses from the deivce. Once the link comes back up, it re-activates the udhcpc client so it can fetch a new IPv4 address for the device. This doesn't make any changes to the IPv6 side of things yet. --- network.go | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/network.go b/network.go index f461e45..ee88d05 100644 --- a/network.go +++ b/network.go @@ -6,6 +6,7 @@ import ( "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" "net" + "os/exec" "time" "github.com/vishvananda/netlink" @@ -25,6 +26,23 @@ type LocalIpInfo struct { MAC string } +// setDhcpClientState sends signals to udhcpc to change it's current mode +// of operation. Setting active to true will force udhcpc to renew the DHCP lease. +// Setting active to false will put udhcpc into idle mode. +func setDhcpClientState(active bool) { + var signal string; + if active { + signal = "-SIGUSR1" + } else { + signal = "-SIGUSR2" + } + + cmd := exec.Command("/usr/bin/killall", signal, "udhcpc"); + if err := cmd.Run(); err != nil { + fmt.Printf("network: setDhcpClientState: failed to change udhcpc state: %s\n", err) + } +} + func checkNetworkState() { iface, err := netlink.LinkByName("eth0") if err != nil { @@ -47,9 +65,26 @@ func checkNetworkState() { fmt.Printf("failed to get addresses for eth0: %v\n", err) } + // If the link is going down, put udhcpc into idle mode. + // If the link is coming back up, activate udhcpc and force it to renew the lease. + if newState.Up != networkState.Up { + setDhcpClientState(newState.Up) + } + for _, addr := range addrs { if addr.IP.To4() != nil { - newState.IPv4 = addr.IP.String() + if !newState.Up && networkState.Up { + // If the network is going down, remove all IPv4 addresses from the interface. + fmt.Printf("network: state transitioned to down, removing IPv4 address %s\n", addr.IP.String()) + err := netlink.AddrDel(iface, &addr) + if err != nil { + fmt.Printf("network: failed to delete %s", addr.IP.String()) + } + + newState.IPv4 = "..." + } else { + newState.IPv4 = addr.IP.String() + } } else if addr.IP.To16() != nil && newState.IPv6 == "" { newState.IPv6 = addr.IP.String() } From 951173ba19a59f2deab31cf728c77444a053f1ea Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 13 Feb 2025 12:10:47 -0500 Subject: [PATCH 12/12] Restart mDNS every time the connection information changes (#155) --- network.go | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/network.go b/network.go index ee88d05..c9ef919 100644 --- a/network.go +++ b/network.go @@ -13,6 +13,8 @@ import ( "github.com/vishvananda/netlink/nl" ) +var mDNSConn *mdns.Conn + var networkState struct { Up bool IPv4 string @@ -91,13 +93,26 @@ func checkNetworkState() { } if newState != networkState { - networkState = newState fmt.Println("network state changed") + //restart MDNS + startMDNS() + networkState = newState requestDisplayUpdate() } } func startMDNS() error { + //If server was previously running, stop it + if mDNSConn != nil { + fmt.Printf("Stopping mDNS server\n") + err := mDNSConn.Close() + if err != nil { + fmt.Printf("failed to stop mDNS server: %v\n", err) + } + } + + //Start a new server + fmt.Printf("Starting mDNS server on jetkvm.local\n") addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4) if err != nil { return err @@ -118,10 +133,11 @@ func startMDNS() error { return err } - _, err = mdns.Server(ipv4.NewPacketConn(l4), ipv6.NewPacketConn(l6), &mdns.Config{ + mDNSConn, err = mdns.Server(ipv4.NewPacketConn(l4), ipv6.NewPacketConn(l6), &mdns.Config{ LocalNames: []string{"jetkvm.local"}, //TODO: make it configurable }) if err != nil { + mDNSConn = nil return err } //defer server.Close() @@ -157,7 +173,6 @@ func init() { } } }() - fmt.Println("Starting mDNS server") err := startMDNS() if err != nil { fmt.Println("failed to run mDNS: %v", err)