DataDog/go-libddwaf

GitHub: DataDog/go-libddwaf

Datadog 官方 libddwaf 的 Go 绑定库,为 Go 应用提供进程内 WAF 规则检测与实时请求安全防护能力。

Stars: 15 | Forks: 3

# go-libddwaf 本项目的目标是为 [libddwaf](https://github.com/DataDog/libddwaf)(DataDog 的应用内 WAF)的 go 绑定提供一个更高级别的 API。 它由 2 个独立的实体组成:用于调用 libddwaf 的绑定,以及负责将*任意* go 值转换为其 libddwaf 对象表示的编码器。 一个使用示例如下: ``` import waf "github.com/DataDog/go-libddwaf/v5" //go:embed var ruleset []byte func main() { var parsedRuleset any if err := json.Unmarshal(ruleset, &parsedRuleset); err != nil { panic(err) } // v2: NewBuilder no longer takes obfuscator regex parameters builder, err := waf.NewBuilder() if err != nil { panic(err) } _, err = builder.AddOrUpdateConfig("/rules", parsedRuleset) if err != nil { panic(err) } wafHandle, err := builder.Build() if err != nil { panic(err) } defer wafHandle.Close() wafCtx, err := wafHandle.NewContext(context.Background(), timer.WithUnlimitedBudget(), timer.WithComponent("waf", "rasp")) if err != nil { panic(err) } defer wafCtx.Close() // v2: Use Data field instead of Persistent result, err := wafCtx.Run(context.Background(), waf.RunAddressData{ Data: map[string]any{ "server.request.path_params": "/rfiinc.txt", }, TimerKey: "waf", }) // v2: For ephemeral data, use NewSubcontext subCtx, err := wafCtx.NewSubcontext(context.Background()) if err != nil { panic(err) } defer subCtx.Close() result, err = subCtx.Run(context.Background(), waf.RunAddressData{ Data: map[string]any{ "server.request.body": "ephemeral data", }, }) } ``` API 文档详情可以在 [pkg.go.dev](https://pkg.go.dev/github.com/DataDog/go-libddwaf/v5) 上找到。 ## 从 v4 升级到 v5 go-libddwaf v5 跟踪 libddwaf v2 并包含了一些破坏性的 API 变更: - `NewBuilder()` 不再接受混淆器正则表达式参数;混淆现在通过 `AddOrUpdateConfig(..., "obfuscator/config", ...)` 存在于 builder 配置中 - `RunAddressData` 现在使用单一的 `Data` 字段,而不是 `Persistent` 和 `Ephemeral` - 临时评估现在通过 `NewSubcontext()` 进行 - `Context.Run`、`Handle.NewContext` 和 `Context.NewSubcontext` 现在需要一个 `context.Context` - `Builder.Build()` 现在返回 `(*Handle, error)` - `WAFObject` 和 `WAFObjectKV` 现在是内部绑定类型的别名(透明,不再需要导入 `internal/bindings`) - `Encodable` 接口的 `Encode` 方法现在是 `Encode(enc *Encoder, obj *WAFObject, depth int) error`,而不是接受 `*bindings.WAFObject` - 内部的 `depthOf` 函数现在接受一个 `timer.Timer`,而不是依赖于 `context.Background()` ### 迁移指南 v5 更新引入了更符合人体工程学且性能更高的编码 API。主要变化包括: 1. **类型变更**:`WAFObject` 和 `WAFObjectKV` 现在是值类型(绑定的类型别名)。`WAFObject{}` 是一个有效的零值。 2. **直接访问 KV 字段**:直接使用 `kv.Key.SetString(pinner, "...")` 和 `kv.Val.SetBool(true)`。`kv.Key()` 和 `kv.Value()` 访问器已被移除。 3. **Pinner 访问**:外部 `Encodable` 实现者通过 `enc.Config.Pinner` 访问 `*runtime.Pinner`(`internal/pin` 包已被移除)。 4. **截断值类型**:`map[TruncationReason][]int` 已被替换为 `Truncations` 值类型。使用 `t.StringTooLong` 等进行直接访问,或使用 `t.AsMap()` 保持向后兼容性。 5. **编码器辅助工具**:为 `Encodable` 实现者提供的一组工具,包含 `WriteString`、`Map`、`Array` 和 `Timeout` 辅助函数。 6. **MapBuilder / ArrayBuilder**:符合人体工程学的构建器,取代了手动进行切片操作和 `SetMapData`/`SetArrayData` 的模式。 7. **Encodable 接口变更**:新的签名是 `Encode(enc *Encoder, obj *WAFObject, depth int) error`。截断现在累积在 `enc.Truncations` 中。 8. **尽力而为的编码哲学**:只要有可能,错误都应自我恢复。只有像 `ErrTimeout` 或 `ErrMaxDepthExceeded` 这样的致命情况才应传播。 **之前 (v4):** ``` // BEFORE (v4) — manual slice juggling + truncation map merge dance type Encodable struct { data []byte // ... fields elided } func (e *Encodable) Encode(config libddwaf.EncoderConfig, obj *libddwaf.WAFObject, remainingDepth int) (map[libddwaf.TruncationReason][]int, error) { truncations := map[libddwaf.TruncationReason][]int{} // ... manual JSON walk ... // For each map: var wafObjs []libddwaf.WAFObject var length int for /* each (key, value) */ { length++ if config.Timer.Exhausted() { return truncations, waferrors.ErrTimeout } if len(wafObjs) >= config.MaxContainerSize { continue } wafObjs = append(wafObjs, libddwaf.WAFObject{}) entryObj := &wafObjs[len(wafObjs)-1] // Manual key truncation if len(key) > config.MaxStringSize { truncations[libddwaf.StringTooLong] = append( truncations[libddwaf.StringTooLong], len(key)) key = key[:config.MaxStringSize] } entryObj.SetMapKey(config.Pinner, key) // v4-only method // ... encode value into entryObj ... if err := encodeValue(entryObj, value, remainingDepth-1); err != nil { entryObj.SetInvalid() continue } } if len(wafObjs) >= config.MaxContainerSize { truncations[libddwaf.ContainerTooLarge] = append( truncations[libddwaf.ContainerTooLarge], length) } obj.SetMapData(config.Pinner, wafObjs) return truncations, nil } ``` **之后 (v5):** ``` // AFTER (v5) — MapBuilder + Encoder helpers handle truncation, capacity, // key truncation, and finalization automatically. type Encodable struct { data []byte // ... fields elided } func (e *Encodable) Encode(enc *libddwaf.Encoder, obj *libddwaf.WAFObject, depth int) error { if enc.Timeout() { return waferrors.ErrTimeout } if depth < 0 { enc.Truncations.Record(libddwaf.ObjectTooDeep, enc.Config.MaxObjectDepth-depth) return waferrors.ErrMaxDepthExceeded } // ... walk JSON ... // For each map: mb := enc.Map(obj) defer mb.Close() for /* each (key, value) */ { if enc.Timeout() { return waferrors.ErrTimeout } slot := mb.NextValue(key) // auto-truncates key, returns nil at cap if slot == nil { mb.Skip() continue } if err := encodeValue(slot, value, depth-1); err != nil { slot.SetInvalid() // best-effort: key preserved, value invalid if errors.Is(err, waferrors.ErrTimeout) { return err } } } return nil } ``` 注意:有关更详细的示例,请参阅计划中的配套文件 `migration_spike_test.go`。 ``` // v4 builder, _ := waf.NewBuilder("keyRegex", "valueRegex") ctx.Run(waf.RunAddressData{Persistent: data, Ephemeral: ephemeral}) // v5 builder, err := waf.NewBuilder() builder.AddOrUpdateConfig("obfuscator/config", map[string]any{ "key_regex": keyRegex, "value_regex": valueRegex, }) wafHandle, err := builder.Build() ctx.Run(context.Background(), waf.RunAddressData{Data: data}) subCtx, err := ctx.NewSubcontext(context.Background()) defer subCtx.Close() subCtx.Run(context.Background(), waf.RunAddressData{Data: ephemeral}) ``` 有关上游 libddwaf v2 的迁移详情和发布说明,请参阅 libddwaf 代码库中的官方文档: - https://github.com/DataDog/libddwaf/blob/master/docs/upgrading/UPGRADING-v2.0.md - https://github.com/DataDog/libddwaf/blob/master/docs/changelog/CHANGELOG-v2.0.0.md ## 在 v5 内部升级 ### Context.SubContext → Context.NewSubcontext `Context.SubContext(ctx) (*Context, error)` 已重命名为 `Context.NewSubcontext(ctx) (*Subcontext, error)`。 返回的类型现在是 `*Subcontext` 而不是 `*Context`。`Subcontext` 拥有自己的 `Run`、`Close` 和 `Truncations` 方法。 `Subcontext.NewSubcontext` 不可用 —— 只有 `Context` 才能生成 `Subcontext`。要创建同级的 subcontext,请调用 `parentContext.NewSubcontext(...)`。 ``` // Before subCtx, err := ctx.SubContext(context.Background()) // After subCtx, err := ctx.NewSubcontext(context.Background()) defer subCtx.Close() ``` 最初,这个项目仅为调用 libddwaf 提供 CGO 包装器。 随着 `ddwaf_object` 树状结构的出现以及构建无 CGO 绑定的目标,它已发展成为 DataDog tracer 的一个集成组件。 这使得对该项目进行文档记录并保持其可维护性变得十分必要。 ## 支持的平台 该库目前支持以下平台组合: | 操作系统 | 架构 | | ----- | ------- | | Linux | amd64 | | Linux | aarch64 | | OSX | amd64 | | OSX | arm64 | 这意味着当平台不受支持时,顶级函数将返回一个 `WafDisabledError` 来解释原因。 请注意: * Linux 支持包括 glibc 和 musl 变体 * 不支持 10.9 以下的 OSX 版本 * 可以手动添加名为 `datadog.no_waf` 的 build tag 以强制禁用 WAF。 ## 设计 WAF 绑定包含多个需要理解的活动部件: - `Builder`:指向 C WAF Builder 的指针对象包装器 - `Handle`:指向 C WAF Handle 的指针对象包装器 - `Context`:指向 C WAF Context 的指针对象包装器 - 编码器:其目标是构建发送给 WAF 的 Waf 对象树 - 解码器:将 WAF 返回的 Waf 对象转换为常见的 go 对象(例如 map、数组等) - 库:指向 C 库的低级 go 绑定,提供改进的类型定义 ``` flowchart LR START:::hidden -->|NewBuilder| Builder -->|Build| Handle Handle -->|NewContext| Context Context -->|NewSubcontext| Subcontext Context -->|Encode Inputs| Encoder Subcontext -->|Encode Inputs| Encoder Handle -->|Encode Ruleset| Encoder Handle -->|Init WAF| Library Context -->|Decode Result| Decoder Subcontext -->|Decode Result| Decoder Handle -->|Decode Init Errors| Decoder Context -->|Run| Library Subcontext -->|Run| Library Encoder -->|Allocate Waf Objects| runtime.Pinner Library -->|Call C code| libddwaf classDef hidden display: none; ``` ### `runtime.Pinner` 在将 Go 值传递给 WAF 时,必须确保内存保持有效且不发生移动,直到 WAF 不再有任何指向它的指针。我们通过使用标准库中的 `runtime.Pinner` 来实现这一点。 每次调用 `Run()` 都会创建一个新的 `runtime.Pinner`;pinner 按每个 Context(或 Subcontext)进行收集,并在 Context(或 Subcontext)关闭时解除锁定(unpin)。 ### 典型的 Run() 调用 以下是对 `Run()` 进行简单调用的操作流程示例: - 为此调用创建一个 `runtime.Pinner` - 将输入数据编码为 WAF 对象,通过 pinner 锁定 Go 指针 - 锁定 context 互斥锁 - 调用 `ddwuf_run` - 解码匹配项和操作 - 解锁互斥锁;将 pinner 追加到 context 的 pinner 列表中(在 `Close()` 时解除锁定) ### 无 CGO 的 C 绑定 本库使用 [purego](https://github.com/ebitengine/purego) 来实现 C 绑定,而无需在编译时使用 CGO。高级工作流 是使用 `go:embed` 嵌入 C 共享库,将其转储到文件中,使用 `dlopen` 打开库,使用 `dlsym` 加载符号,最后调用它们。在 Linux 系统上,使用 `memfd_create(2)` 使库能够在不写入 文件系统的情况下被加载。 `libddwaf` 的另一个要求是您的机器上必须拥有 FHS 文件系统,并且对于 Linux,需要提供 `libc.so.6`、 `libpthread.so.0` 和 `libdl.so.2` 作为动态库。 ## 贡献注意事项 - 在应用的生命周期内,在 OSX 上不能进行两次 dlopen。它会干扰线程本地存储(Thread Local Storage),通常会导致 `std::bad_alloc()` 错误 - 这里的 `keepAlive()` 调用是为了防止 GC 过早销毁对象 - 由于 Go 代码和 C 代码之间存在堆栈切换,通常您能获得的唯一 C 堆栈跟踪只能来自 GDB - 如果在调用 C 代码期间发生段错误,执行该调用的 goroutine 堆栈跟踪将带有 `[syscall]` 标记 - [GoLand](https://www.jetbrains.com/go/) 不支持 `CGO_ENABLED=0`(截至 2023 年 6 月) - 请记住,我们完全脱离了类型系统。如果您发送了错误的数据,在最好的情况下它会引发段错误,但并非总是如此! - `ctypes.go` 中的结构体是为了重现 `include/ddwaf.h` 中结构体的内存布局,因为指向这些结构体的指针将被直接传递 - 不要将来自 Go 值 `unsafe.Pointer` 转换的 `uintptr` 用作函数参数或结果类型,因为它们会脱离指针分析,这可能会生成错误优化的代码并导致崩溃。在这样的库中,指针算术当然是必要的,但必须保持在同一个函数作用域内。 - GDB 在 arm64 上可用,但并未得到官方支持,因此它通常很快就会崩溃(截至 2023 年 6 月) - 不应将指向栈上变量的指针发送给 C 代码,因为在 C 调用期间 Go 栈可能会被移动。更多相关信息请参见[这里](https://medium.com/@trinad536/escape-analysis-in-golang-fc81b78f3550) ## 调试 可以通过构建(或测试)时将 `DD_APPSEC_WAF_LOG_LEVEL` 环境变量设置为以下值之一:`trace`、`debug`、`info`、`warn`(或 `warning`)、`error`、`off`(这是默认行为且不记录任何日志),来为底层 C/C++ 库启用调试日志。 可以将 `DD_APPSEC_WAF_LOG_FILTER` 环境变量设置为有效的(根据 `regexp` 包)正则表达式, 以将日志限制为仅包含与正则表达式匹配的消息。
标签:AppImage, DataDog, EVTX分析, Go, libddwaf, RASP, Ruby工具, WAF, Web应用防火墙, 日志审计