实现网络唤醒启动指定操作系统
背景
客厅玩游戏,已配置steam开机自启和无线手柄,希望远程启动linux或windows系统。
难点
通过wakeonlan命令发送magic packet配合主板支持网络唤醒不难,如何在通电前告知启动顺序?
方案
- 使用完整PXE Server并让主板每次从网络启动,决定启动顺序;
- 默认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=linux或set 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

浙公网安备 33010602011771号