Files
HomeAgent/cmd/waiter/raw_windows.go
JianFeeeee 1911575352 feat(waiter): 设备命令白名单改为 waiter.yaml 可配置
## 起因

白名单是源码里硬编码的正则(`homeagentAllowCmd`,18 个命令),
而 `waiter.yaml` 里**没有任何键能改它** ⇒ `find` / `grep` / `sed` / `sort` / `tr`
这些排查问题最常用的**只读**命令一律被拒。生产实测:

    device_ctl_cmdrun  device_id:waiter-fnnas  error: command not in whitelist

命令执行完全在 waiter 侧(`device.go` 的 `exec.CommandContext`),插件侧无二次
限制;触发者是 **agent**(经 device_ctl_cmdrun),所以这道闸是机器闸、不是人工确认。

## 改动

waiter.yaml 新增 `device_cmd_allowlist`(字符串数组):

    device_cmd_allowlist:
      - ls
      - find
      - grep
      - sed

- **替换**默认集而非追加:避免"以为加了 find、结果还留着 python3 -c 任意执行"
- 留空 ⇒ 用内置默认集(★ **绝不能变成"全放行"**,那等于静默拆掉闸门)
- 匹配只取命令名**第一段**再整词匹配:`grep -rn x .` 能过,
  而 `grepXxx` / `mygrep` 不会因 contains 蒙混过关;也跳过 `FOO=bar cmd` 的赋值前缀
- `deviceCmdAllowed` 是包级函数变量,由配置赋值 —— 与同文件既有的
  `sendBridgeResult` 同一模式

## ★ 一次真实的疏漏(判据记着)

waiter 有**两条**设备桥启动路径:
- `main.go` 的 `startDeviceBridge` —— 交互/一次性模式
- `daemon.go` 的 `startDaemonDeviceBridge` —— `waiter --daemon`(**生产两台都这么跑**)

我最初只在 `main.go` 里赋值。daemon 路径不经过那里 ⇒ 配置**完全不生效**,
而症状是"配置写了、启动也打了招呼、命令照样被拒",极难定位。
两处都接上了,并加 `TestDaemonPathAppliesAllowlist` 守住。

## 判据(5 条)

- `TestDefaultAllowlistStillBlocksDestructive`  默认集必须挡住
  `rm -rf /`、`dd`、`chmod -R 777`、`mkfs`、fork 炸弹 ——
  **这道闸存在的唯一理由**,谁把它改成"什么都不拦"这条就要失败
- `TestConfigAllowlistExtends`  配置里声明的 `find/grep/sed/sort/tr` 能过;
  配置未含的 `rm -rf /` 仍被拒(证明是"替换"不是"叠加")
- `TestEmptyConfigFallsBackToDefault`  配置为空时回退默认集,**且不放行** `rm -rf /`
- `TestCmdAllowlistFromYAML`  走**真实** `readFile` 解析 yaml(不另写一份解析,
  两处会漂移,而漂移本身就是漏洞)
- `TestDaemonPathAppliesAllowlist`  守住 daemon 路径也应用配置

## 生效方式

106/30 的 `/opt/waiter/waiter.yaml` 追加 `device_cmd_allowlist`,
并更新二进制。启动日志会打印 `device cmd allowlist: N 条(来自 waiter.yaml)`
或 `默认 N 条`,便于确认配置是否真的被读到。
2026-09-27 19:41:43 +08:00

53 lines
1.4 KiB
Go

//go:build windows
package main
import (
"os"
"syscall"
"unsafe"
)
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
procSetConsoleMode = kernel32.NewProc("SetConsoleMode")
procGetStdHandle = kernel32.NewProc("GetStdHandle")
)
const (
stdInputHandle = ^uint32(9) + 1 // -10
enableVirtualTerminalProcessing = 0x0004
enableProcessedOutput = 0x0001
enableWrapAtEOLOutput = 0x0002
disableNewlineAutoReturn = 0x0008
)
func setRawMode(fd int) (func(), error) {
if fd == 0 {
fd = int(os.Stdin.Fd())
}
if !isTerminal(fd) {
return func() {}, nil
}
// Enable virtual terminal processing for ANSI escape sequences on Windows
h, _, _ := procGetStdHandle.Call(uintptr(^uint32(10) + 1)) // STD_OUTPUT_HANDLE = -11
if h != 0 && h != ^uintptr(0) {
var mode uint32
procGetConsoleMode.Call(h, uintptr(unsafe.Pointer(&mode)))
newMode := mode | enableVirtualTerminalProcessing | enableProcessedOutput | enableWrapAtEOLOutput
procSetConsoleMode.Call(h, uintptr(newMode))
}
return func() {}, nil
}
func isTerminal(fd int) bool {
var mode uint32
h, _, _ := procGetStdHandle.Call(uintptr(^uint32(10) + 1)) // STD_OUTPUT_HANDLE = -11
if h == 0 || h == ^uintptr(0) {
return false
}
ret, _, _ := procGetConsoleMode.Call(h, uintptr(unsafe.Pointer(&mode)))
return ret != 0
}