# srg-client-go **Repository Path**: aurawing/srg-client-go ## Basic Information - **Project Name**: srg-client-go - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-06-17 - **Last Updated**: 2026-06-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # srg-client-go `srg-client-go`(Go 包名 `srg`)是 **sursen-ransom-guard** 抗勒索系统的 **用户态 daemon 客户端库**。它封装了与两个 OS 适配器之间的全部底层协议,让策略引擎(`ar-verdictd` / 测试用的 `srg-agent`)只需实现 10 个判定函数即可完成文件操作的同步拦截。 - Linux 适配器:**`srg-fanotify`**(仅 `OpenFile` / `CreateFile`) - Windows 适配器:**`srg-minifilter`**(全部 10 个接口) > 模块路径:`github.com/yottachain/srg-client-go`,包名 `srg`。 > 依赖:`golang.org/x/sys`。要求 Go 1.25+。 --- ## 目录 1. [它做什么](#1-它做什么) 2. [架构与数据流](#2-架构与数据流) 3. [安装](#3-安装) 4. [前置:适配器的安全部署与启用](#4-前置适配器的安全部署与启用) - [4.1 Linux:srg-fanotify](#41-linuxsrg-fanotify) - [4.2 Windows:srg-minifilter](#42-windowssrg-minifilter) 5. [安全模型(fail-open / bypass / enable)](#5-安全模型fail-open--bypass--enable) 6. [接口定义(API 参考)](#6-接口定义api-参考) 7. [代码示例](#7-代码示例) 8. [平台差异(路径 / Open-Create)](#8-平台差异路径--open-create) 9. [线协议与内存布局](#9-线协议与内存布局) 10. [故障排查](#10-故障排查) --- ## 1. 它做什么 适配器(内核/fanotify 侧)在每一次受监控的文件操作上**同步阻塞**,把一条 `srg_req_t` 请求写进共享内存环,等待 daemon 回一个 ALLOW/DENY 裁决。本库负责: - **连接与握手**:连上适配器的控制通道,协商协议版本,映射共享环。 - **收发循环**:从请求环取请求,交给你的 `Decider`,把裁决写回回复环并通知适配器。 - **并发与背压**:多 worker 并发判定,回复环满时退避重试。 - **容错**:判定出错 / 请求损坏时按配置 fail-open 或 fail-close。 - **可选热路径组件**:`Dispatcher`(把 `Action` 路由到 10 个策略函数)、`ObjectIndex`(fanotify 的 Open/Create 兜底)、`ProcCache`(进程身份缓存)。 你只需要实现 `PolicyEngine`(10 个方法)或更底层的 `Decider`(单方法)。 --- ## 2. 架构与数据流 ``` 你的策略代码 ┌─────────────────────────┐ │ PolicyEngine (10 方法) │ ← 你实现这个 └───────────▲─────────────┘ │ Dispatcher 路由 action → 方法 ┌───────────┴─────────────┐ │ srg-client-go (包 srg) │ Dial → Run(Decider) → Close │ client / ring / transport│ └───────────▲─────────────┘ │ 共享内存 SPSC 环 + 事件通知(srg_req_t / srg_reply_t,v4) ┌───────────┴─────────────┐ │ 适配器 │ │ srg-fanotify (Linux) │ AF_UNIX SOCK_SEQPACKET + memfd/eventfd (SCM_RIGHTS) │ srg-minifilter (Windows)│ FilterManager 端口 + 命名事件 + MDL 映射环 └───────────▲─────────────┘ │ OS 原生事件 Linux fanotify / Windows Filter Manager ``` 每个请求的处理是**同步**的:适配器侧的线程在拿到裁决前一直阻塞,因此 `Decider` 必须**只读内存、不做文件/网络 I/O**,并尽快返回(否则会拖慢甚至卡住被监控进程)。 --- ## 3. 安装 库随主仓库一起开发,通常用本地 `replace` 引用: ``` // go.mod require github.com/yottachain/srg-client-go v0.0.0 replace github.com/yottachain/srg-client-go => ../srg-client-go ``` ```bash go mod tidy go test ./... # 运行布局断言 / Dispatcher / ObjectIndex / ProcCache 单测 ``` 同一份代码可编译到 Linux 与 Windows,平台相关实现由构建标签切换(`transport_linux.go` / `transport_windows.go`)。 --- ## 4. 前置:适配器的安全部署与启用 daemon 自己**不拦截**任何东西——拦截发生在适配器(fanotify/minifilter)里。必须先把对应平台的适配器跑起来,daemon 才连得上。 ### 4.1 Linux:srg-fanotify `srg-fanotify` 是用户态进程,需 **root / `CAP_SYS_ADMIN`**(fanotify 许可事件要求)。 ```bash # 1. 编译 cd srg-fanotify && make # 产物 bin/srg-fanotify # 2. 启动适配器(监听控制 socket,打 fanotify mark) sudo ./bin/srg-fanotify \ --watch-path /data \ # 只标记该目录树(冒烟友好;省略则标记更大范围) --sock /run/srg-fanotify/adapter.sock \ --log-level 1 # 常用参数: # --sock 控制 socket(库默认 /run/srg-fanotify/adapter.sock) # --watch-path 只 mark 这棵目录树 # --timeout 裁决超时(超时 fail-open) # --log-level 0..3 0=debug 1=info 2=warn 3=error ``` **安全要点** - fanotify 只能拦 `OpenFile` / `CreateFile`(无 rename/unlink 的许可事件),这是能力边界,不是缺陷。 - 适配器崩溃或 daemon 未连接时,文件操作**正常放行**(fail-open),不会锁死系统。 - 生产部署建议用 `systemd`(仓库自带 `srg-fanotify/systemd/srg-fanotify.service`)。 - 卸载只需停止进程即可(无内核模块)。 ### 4.2 Windows:srg-minifilter `srg-minifilter` 是**内核 minifilter 驱动**(`SrgFilter.sys`),加载需要**管理员 + 测试签名模式**(自签名驱动),并有重启/蓝屏风险,务必先在 VM 或测试机验证。 ```powershell # 0. 一次性:开启测试签名(自签名内核驱动需要),然后重启 bcdedit /set testsigning on # 需重启生效;用 bcdedit /enum {current} 确认 # 1. 编译驱动(需 Visual Studio + WDK) cd srg-minifilter .\scripts\build.ps1 # 产物 SrgFilter\x64\Debug\SrgFilter.sys # 2. 安装并加载(自签名 + sc create + Instances 注册 + fltmc load) .\scripts\install.ps1 # 必须管理员 # 成功后 `fltmc filters` 会列出 SrgFilter(altitude 370020) # 3. 卸载,恢复干净(fltmc unload + sc delete + 删除 sys/inf) .\scripts\uninstall.ps1 # 必须管理员 ``` **安全要点** - 驱动**仅在 daemon 连接并 `SET_ENABLE(1)` 后才强制裁决**;无 daemon 时所有操作放行(fail-open)。本库 `Dial` 成功即自动开启,`Close` 自动关闭。 - 驱动通过 `\SrgFilterPort` 通信,并对外暴露命名事件 `Global\SrgFilterReqEvent` / `Global\SrgFilterRepEvent`。本库的 Windows 传输层会自动连接它们,**无需你手工处理**。 - 驱动对**自身 daemon 的操作 bypass**(按 daemon PID),避免判定时自我死锁。 - minifilter 上报 **NT 设备路径**(`\Device\HarddiskVolumeN\...`),策略前缀匹配前需归一化成盘符——见 [§8](#8-平台差异路径--open-create)。 - 生产应使用 **EV 证书 + WHQL/attestation 签名**,而非自签名 + 测试签名。 --- ## 5. 安全模型(fail-open / bypass / enable) | 机制 | 行为 | |---|---| | **Enable** | `Dial` 成功后自动 `SET_ENABLE(1)` 开始拦截;`Close` 自动 `SET_ENABLE(0)` 并释放环。 | | **Fail-open(判定出错)** | `Decider` 返回 error 时,按 `Options.FailOpenOnError` 决定 ALLOW(true)或 DENY(false)。 | | **Fail-open(请求损坏)** | 环里读到非法请求时,若能取到 `ReqID` 则回 ALLOW(带 `ErrorReason`),不阻塞被监控进程。 | | **Fail-open(无 daemon / 超时 / 环满)** | 由适配器侧负责:未连接、裁决超时、环满、pending 溢出均放行。 | | **裁决合法化** | worker 强制把非 ALLOW/DENY 的裁决纠正为 DENY(带 `ErrorReason`),防止脏值。 | | **daemon bypass** | 适配器按 daemon 进程身份(PID + start_time/image_id)放行自身操作,避免递归死锁。 | > 设计取向:**可用性优先**——任何异常路径都不应让系统卡死;漏放比锁死安全得多。真正的“从严”策略由你的 `PolicyEngine` 决定(如对受保护根下未授权进程一律 DENY)。 --- ## 6. 接口定义(API 参考) ### 6.1 连接与运行 ```go // 连接适配器、映射环、开启拦截。 func Dial(ctx context.Context, opts Options) (*Client, error) // 跑收发/判定循环,直到 ctx 取消或致命错误。 func (c *Client) Run(ctx context.Context, d Decider) error // 关闭拦截并释放环 + 传输(幂等)。 func (c *Client) Close() error ``` ```go type Options struct { LinuxSocket string // Linux 控制 socket;空 = /run/srg-fanotify/adapter.sock。Windows 忽略。 Workers int // 并发判定 worker 数;<=0 视为 1。 QueueSize int // 进程内请求/回复 channel 容量;<=0 时取 Workers*4,下限 16。 FailOpenOnError bool // 判定 error → ALLOW(true) / DENY(false)。 ErrorReason uint32 // 错误/损坏路径回复里的 reason;0 = 0xffff0001。 WaitInterval time.Duration // 平台等待轮询间隔(让 ctx 取消可被及时观察);0 = 500ms。 } ``` ### 6.2 Decider(最底层接口) ```go // 把一个 Request 变成一个 Decision。运行在同步热路径,必须只读内存、无 I/O。 type Decider interface { Decide(context.Context, *Request) (Decision, error) } // 函数适配器 type DeciderFunc func(context.Context, *Request) (Decision, error) ``` ### 6.3 PolicyEngine + Dispatcher(推荐) `Dispatcher` 实现了 `Decider`,把 `Request.Action` 路由到对应的策略函数,并可选地接入 `ObjectIndex` 与 `ProcCache`。你只需实现 `PolicyEngine`: ```go type PolicyVerdict uint32 const ( VerdictAllowPolicy PolicyVerdict = 1 // = VERDICT_ALLOW VerdictDenyPolicy PolicyVerdict = 2 // = VERDICT_DENY ) // 签名与任务规范完全一致;pid 为当前操作进程。 // fanotify 只会驱动 OpenFile / CreateFile;minifilter 驱动全部 10 个。 type PolicyEngine interface { Mkdir(pid uint32, path string, name string) PolicyVerdict Rmdir(pid uint32, path string) PolicyVerdict Unlink(pid uint32, path string) PolicyVerdict Rename(pid uint32, path string, newpath string) PolicyVerdict CreateFile(pid uint32, path string, flags uint32) PolicyVerdict OpenFile(pid uint32, path string, flags uint32) PolicyVerdict Symlink(pid uint32, target string, path string) PolicyVerdict Link(pid uint32, target string, path string) PolicyVerdict Setattr(pid uint32, path string) PolicyVerdict Getattr(pid uint32, path string) PolicyVerdict } func NewDispatcher(engine PolicyEngine, opts ...DispatcherOption) *Dispatcher func WithObjectIndex(x *ObjectIndex) DispatcherOption // 启用 Open/Create 兜底(§8) func WithProcCache(c *ProcCache) DispatcherOption // 启用进程身份缓存 func WithDenyReason(reason uint32) DispatcherOption // DENY 的 reason 码 ``` > 路由细节:`Mkdir` 收到的 `path` 是父目录、`name` 是新目录名(库内部自动拆分); > `Symlink`/`Link` 的 `path` 是 target、`newpath` 是链接路径。 ### 6.4 数据类型 ```go type Request struct { ReqID uint64 Action Action // 见下;决定调哪个策略函数 AccessMask AccessMask // 归一化访问意图(策略判定依据) Flags uint32 // 平台原生 flags(仅审计) ReqFlags ReqFlags // SRG_REQF_*(如 action 是否权威,§8) TimestampNS uint64 PID, TID, PPID, UID uint64 StartTime uint64 // 进程启动时刻(反 PID 复用) ImageID ImageID // 可执行文件身份 Object ObjectID // 目标文件身份 Path string // 主路径 NewPath string // rename/link/symlink 的第二路径;否则 "" } type Decision struct { Verdict Verdict // VerdictAllow=0 / VerdictDeny=1(线值) CacheHintMS uint32 Reason uint32 } func Allow() Decision func Deny(reason uint32) Decision ``` ```go type Action uint32 const ( ActOpen Action = 1; ActCreate; ActMkdir; ActRmdir; ActUnlink ActRename; ActSymlink; ActLink; ActSetattr; ActGetattr ) type AccessMask uint32 const ( AccessRead AccessMask = 1< btime 可用的现代文件系统(ext4/xfs/NTFS)上,适配器会置 `ReqActionResolved`, > 此时 `Dispatcher` **不**调用对象索引,预置与否都不影响 Open/Create 判定。 ### 7.4 Windows:把 NT 设备路径归一化成盘符 minifilter 上报 `\Device\HarddiskVolumeN\...`。若你的策略前缀是 `C:\...`,匹配前需转换(参考 `srg-agent/pathnorm_windows.go`,用 `QueryDosDevice` 建立 设备→盘符 映射): ```go // 仅 Windows;伪代码示意,完整实现见 srg-agent。 func toDrive(ntPath string) string { w := strings.ReplaceAll(ntPath, "/", `\`) for dev, drive := range deviceMap { // dev=\Device\HarddiskVolume3, drive=C: if strings.HasPrefix(strings.ToLower(w), dev+`\`) { return drive + w[len(dev):] } } return w } ``` --- ## 8. 平台差异(路径 / Open-Create) | 维度 | srg-fanotify (Linux) | srg-minifilter (Windows) | |---|---|---| | 实现的接口 | 仅 `OpenFile` / `CreateFile` | 全部 10 个 | | 路径格式 | 绝对路径 `/data/...` | **NT 设备路径** `\Device\HarddiskVolumeN\...`(需归一化,§7.4) | | Open vs Create | btime 启发式为主;不可用时 `ReqActionResolved=0` → 对象索引兜底 | `action` 源自 IRP,`ReqActionResolved` 恒置位 | | `AccessMask` 完整度 | 较弱(WRITE/CREATE 常为 0=未知) | 较完整,可做读/写分流 | | 权限要求 | root / CAP_SYS_ADMIN | 管理员 + 内核驱动 | **`ReqActionResolved` 的意义**:适配器据此声明 `Action` 是否权威。`Dispatcher` **仅在未置位时**才用 `ObjectIndex` 重分类 Open/Create——这避免了“对象索引把权威的 OPEN 误纠成 CREATE”的问题。一般无需关心,`Dispatcher` 已自动处理;只有写自定义 `Decider` 时才需要自行判断 `r.ReqFlags`。 **`AccessMask` 消费原则**:位有效则用、位为 0 当“未知”。fanotify 的请求里 WRITE/CREATE 常缺失,对受保护对象应统一从严(未授权进程的 open 直接 DENY)。 --- ## 9. 线协议与内存布局 库内的 Go 结构与各适配器自带的 C 头(`srg-fanotify/include/srg/`、`srg-minifilter/SrgFilter/include/srg/` 下的 `srg_uapi.h` / `srg_ring.h`)**逐字节对齐**,`Dial` 启动时 `verifyLayout()` 会断言以下尺寸,任何漂移立即 panic: | 结构 | 大小 | 说明 | |---|---|---| | `srg_image_id_t` | 24 B | 可执行文件身份 | | `srg_object_id_t` | 16 B | 目标文件身份 | | `srg_req_t` | 128 B | 请求头(body = `path + new_path` 紧随其后) | | `srg_reply_t` | 32 B | 回复 | | `srg_ring_meta_t` | 384 B | 环元数据(缓存行隔离的 head/tail) | - 协议版本 `SRG_PROTO_VER = 4`(`ProtoVersion`)。 - 线裁决:`ALLOW=0 / DENY=1`(`Verdict`);策略函数的 `PolicyVerdict(ALLOW=1/DENY=2)` 由 `Dispatcher` 映射为线值。 - 请求 slot 默认足以容纳 `header + path + new_path = 128 + 2*4096`。 > 每个适配器各自维护一份 C 头、本库维护一份 Go 布局;修改协议时需**同时手动更新三处** > 并各自跑编译/`go test`,否则 `verifyLayout` 会在 `Dial` 时 panic,或运行期 `GET_VERSION` 拒连。 --- ## 10. 故障排查 | 现象 | 可能原因 / 处理 | |---|---| | `Dial` 报 `connection refused` / `cannot open \SrgFilterPort` | 适配器未启动(Linux)或驱动未加载(Windows)。先按 §4 起适配器。 | | Windows `windows transport ...` 类错误 | 驱动未加载或 `install.ps1` 未成功;`fltmc filters` 确认 `SrgFilter` 在列。 | | `protocol version N, expected 4` | 适配器与库协议版本不一致;各自携带的协议头未手动同步,重编译两端到同一 v4。 | | `... layout mismatch`(panic) | 某适配器的 C 头改了但本库 `uapi.go` 未同步,见 §9。 | | 所有操作都被放行(Windows) | 多半是路径未归一化:策略前缀写成 `C:\...` 但请求是 `\Device\HarddiskVolumeN\...`,见 §7.4。 | | 已有文件首次打开被记成新建(Linux) | btime 不可用且对象索引未预置基线,见 §7.3。 | | 被监控进程明显变慢/卡住 | `Decider` 里有 I/O 或阻塞操作;热路径必须只读内存、快速返回。 | | daemon 退出后系统恢复正常 | 预期行为:`Close` 会 `SET_ENABLE(0)`;无 daemon 时适配器 fail-open。 | --- ## 相关 - 总体设计:`sursen-ransom-guard/DESIGN.md` - 测试 daemon:`sursen-ransom-guard/srg-agent/`(含 Linux `run_e2e*.sh` 与 Windows `run_e2e_win.ps1`) - C 协议头(各适配器自带):`srg-fanotify/include/srg/`、`srg-minifilter/SrgFilter/include/srg/`