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应用防火墙, 日志审计