Loading

实现网络唤醒启动指定操作系统

背景

客厅玩游戏,已配置steam开机自启和无线手柄,希望远程启动linux或windows系统。

难点

通过wakeonlan命令发送magic packet配合主板支持网络唤醒不难,如何在通电前告知启动顺序?

方案

  1. 使用完整PXE Server并让主板每次从网络启动,决定启动顺序;
  2. 默认linux启动但修改grub菜单,通过api获取启动信息。
    经过评估,仅仅用于实现系统选择,方案2更简单。

实现

启动部分

我的双系统安装在两个独立硬盘,展示lsblk -f结构如下:

nvme0n1
├─p1  FAT32  A50A-CD9F       /boot/efi       ← Ubuntu EFI
└─p2  ext4   9072231f-...    /               ← Ubuntu

nvme1n1
├─p1  FAT32  3D3A-6FA5                       ← Windows EFI
├─p2
├─p3  NTFS   70362B0D...                     ← Windows
└─p4  NTFS   1C6EC09A...                     ← Windows

设计grub文件,通过读取api server上的返回值获取boot-target如遇问题默认启动linux系统:

menuentry 'Remote OS' {
    insmod net
    insmod efinet
    insmod http

    set remote_target=ubuntu

    net_bootp
    source (http,192.168.1.10)/boot-target

    if [ "$remote_target" = "windows" ]; then
        insmod part_gpt
        insmod fat
        insmod chain
        search --no-floppy --set=root --fs-uuid /EFI/Microsoft/Boot/bootmgfw.efi
        chainloader /EFI/Microsoft/Boot/bootmgfw.efi
        boot
    fi

    # fallback → Ubuntu
    search --fs-uuid --set=root 9072231f-e14a-4480-ad42-f53b6dfe0559
    linux /boot/vmlinuz root=UUID=9072231f-e14a-4480-ad42-f53b6dfe0559 ro
    initrd /boot/initrd.img
    boot
}

目标文件添加到/etc/grub.d/40_custom文件末尾并修改/etc/default/grub

GRUB_DEFAULT="Remote OS"

最后执行sudo update-grub生效。
有几个坑需要说明:

  • grub语法支持source因此server需返回可执行grub语句
  • 需要在主板UEFI中开启开启NetworkStack支持否则无法看到网卡
  • 建议关闭SecureBoot选项,开启效果我没测试
  • search命令需要传入--set=root而非自行制定变量,否则报错BCD not found
  • 资料指出需要rmmod peimage才能正常启动windows,我没加也正常

服务部分

使用golang实现一个简易api server,要求:
GET /boot-target返回set remote_target=linuxset remote_target=ubuntu
POST /boot-target/windows将返回值设定为windows
代码如下:

package main

import (
	"flag"
	"fmt"
	"log"
	"net/http"
	"strings"
	"sync"
)

var (
	addr    = flag.String("addr", ":7856", "listen address")
	mu      sync.RWMutex
	content = "ubuntu"
)

func bootTargetHandler(w http.ResponseWriter, r *http.Request) {
	path := strings.TrimPrefix(r.URL.Path, "/boot-target")
	path = strings.TrimPrefix(path, "/")

	switch r.Method {
	case http.MethodGet:
		if path != "" {
			http.Error(w, "not found", http.StatusNotFound)
			return
		}
		mu.RLock()
		c := content
		mu.RUnlock()
		fmt.Fprintf(w, "set remote_target=%s\n", c)

	case http.MethodPost:
		if path == "" {
			http.Error(w, "content path is required, e.g. /boot-target/ubuntu", http.StatusBadRequest)
			return
		}
		mu.Lock()
		content = path
		mu.Unlock()
		log.Printf("content set to: %s", path)
		fmt.Fprintf(w, "content set to %s\n", path)

	default:
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
	}
}

func main() {
	flag.Parse()

	log.Printf("boot_server listening on %s, default content=%q", *addr, content)

	mux := http.NewServeMux()
	mux.HandleFunc("/boot-target", bootTargetHandler)
	mux.HandleFunc("/boot-target/", bootTargetHandler)

	log.Fatal(http.ListenAndServe(*addr, mux))
}

配合golang:1.24-alpine构建镜像仅8M,部署后配置Caddy反向代理:

:80, lab.mydomain.net {
        handle /boot-target* {
       		 reverse_proxy localhost:7856
        }
}

效果

使用iPhone Shortcut绑定远程ssh执行命令实现语音唤醒

#启动linux
curl -X POST http://192.168.1.10/boot-target/ubuntu
wakeonlan xx:xx:xx:xx:xx:xx

#启动windows
curl -X POST http://192.168.1.10/boot-target/windows
wakeonlan xx:xx:xx:xx:xx:xx

参考

boot - Add Windows 10 to GRUB OS list - Ask Ubuntu
Simple configuration (GNU GRUB Manual 2.14)
grub2 - Grub dual boot results in Windows error code 0x000428 - Ask Ubuntu

posted @ 2026-08-24 13:36  azureology  阅读(2)  评论(0)    收藏  举报