libddwaf

package module
v5.0.0-rc.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 23, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

go-libddwaf

This project's goal is to produce a higher level API for the go bindings to libddwaf: DataDog in-app WAF. It consists of 2 separate entities: the bindings for the calls to libddwaf, and the encoder whose job is to convert any go value to its libddwaf object representation.

An example usage would be:

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",
        },
    })
}

The API documentation details can be found on pkg.go.dev.

Upgrading from v4 to v5

go-libddwaf v5 tracks libddwaf v2 and includes a few breaking API changes:

  • NewBuilder() no longer takes obfuscator regex arguments; obfuscation now lives in builder config via AddOrUpdateConfig(..., "obfuscator/config", ...)
  • RunAddressData now uses a single Data field instead of Persistent and Ephemeral
  • ephemeral evaluation now goes through NewSubcontext()
  • Context.Run, Handle.NewContext, and Context.NewSubcontext now require a context.Context
  • Builder.Build() now returns (*Handle, error)
  • WAFObject and WAFObjectKV are now type aliases to internal binding types (transparent but no longer require importing internal/bindings)
  • The Encodable interface's Encode method is now Encode(enc *Encoder, obj *WAFObject, depth int) error instead of taking *bindings.WAFObject
  • The internal depthOf function now takes a timer.Timer instead of relying on context.Background()
Migration Guide

The v5 update introduces a more ergonomic and performant encoding API. Key changes include:

  1. Type changes: WAFObject and WAFObjectKV are now value types (type aliases to bindings). A WAFObject{} is a valid zero-value.
  2. Direct field access for KV: Use kv.Key.SetString(pinner, "...") and kv.Val.SetBool(true) directly. The kv.Key() and kv.Value() accessors have been removed.
  3. Pinner access: External Encodable implementers access *runtime.Pinner via enc.Config.Pinner (the internal/pin package has been removed).
  4. Truncations value type: The map[TruncationReason][]int has been replaced by a Truncations value type. Use t.StringTooLong etc. for direct access, or t.AsMap() for backward compatibility.
  5. Encoder helper: A bundle for Encodable implementers that provides WriteString, Map, Array, and Timeout helpers.
  6. MapBuilder / ArrayBuilder: Ergonomic builders that replace the manual slice juggling and SetMapData/SetArrayData pattern.
  7. Encodable interface change: The new signature is Encode(enc *Encoder, obj *WAFObject, depth int) error. Truncations now accumulate in enc.Truncations.
  8. Best-effort encoding philosophy: Errors should be self-recovered whenever possible. Only fatal conditions like ErrTimeout or ErrMaxDepthExceeded should propagate.

BEFORE (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
}

AFTER (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
}

Best-effort encoding philosophy: The WAF prefers a malformed payload to no payload at all (so at least some inspection happens). When implementing Encodable, treat encoding errors as recoverable: leave the object as the zero value (which is WAFInvalidType) or call obj.SetInvalid() and continue. Only return errors from Encode for fatal conditions: waferrors.ErrTimeout (when enc.Timeout() returns true) or waferrors.ErrMaxDepthExceeded (when depth budget is exhausted). The MapBuilder preserves keys with invalid values on error; the ArrayBuilder lets you DropLast() to remove an entry that couldn't be encoded.

Note: For more detailed examples, see the planned migration_spike_test.go companion.

// 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})

For the upstream libddwaf v2 migration details and release notes, prefer the canonical docs in the libddwaf repository:

Upgrading within v5

Context.SubContext → Context.NewSubcontext

Context.SubContext(ctx) (*Context, error) has been renamed to Context.NewSubcontext(ctx) (*Subcontext, error).

The returned type is now *Subcontext instead of *Context. Subcontext has its own Run, Close, and Truncations methods.

Subcontext.NewSubcontext is not available — only a Context can spawn Subcontexts. To create a sibling subcontext, call parentContext.NewSubcontext(...).

// Before
subCtx, err := ctx.SubContext(context.Background())

// After
subCtx, err := ctx.NewSubcontext(context.Background())
defer subCtx.Close()

Originally this project only provided CGO wrappers for calls to libddwaf. With the appearance of the ddwaf_object tree-like structure and the goal of building CGO-less bindings, it has grown into an integrated component of the DataDog tracer. That made it necessary to document the project and keep it maintainable.

Supported platforms

This library currently supports the following platform pairs:

OS Arch
Linux amd64
Linux aarch64
OSX amd64
OSX arm64

This means that when the platform is not supported, top-level functions will return a WafDisabledError explaining why.

Note that:

  • Linux support includes glibc and musl variants
  • OSX under 10.9 is not supported
  • A build tag named datadog.no_waf can be manually added to force the WAF to be disabled.

Design

The WAF bindings have multiple moving parts that are necessary to understand:

  • Builder: an object wrapper over the pointer to the C WAF Builder
  • Handle: an object wrapper over the pointer to the C WAF Handle
  • Context: an object wrapper over a pointer to the C WAF Context
  • Encoder: its goal is to construct a tree of Waf Objects to send to the WAF
  • Decoder: Transforms Waf Objects returned from the WAF to usual go objects (e.g. maps, arrays, ...)
  • Library: The low-level go bindings to the C library, providing improved typing
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

When passing Go values to the WAF, it is necessary to make sure that memory remains valid and does not move until the WAF no longer has any pointers to it. We do this by using runtime.Pinner from the standard library. Each call to Run() creates a new runtime.Pinner; pinners are collected per-Context (or per-Subcontext) and unpinned when the Context (or Subcontext) is closed.

Typical call to Run()

Here is an example of the flow of operations on a simple call to Run():

  • Create a runtime.Pinner for this call
  • Encode input data into WAF Objects, pinning Go pointers via the pinner
  • Lock the context mutex
  • Call ddwaf_run
  • Decode the matches and actions
  • Unlock the mutex; append the pinner to the context's pinner list (unpinned on Close())
CGO-less C Bindings

This library uses purego to implement C bindings without requiring use of CGO at compilation time. The high-level workflow is to embed the C shared library using go:embed, dump it into a file, open the library using dlopen, load the symbols using dlsym, and finally call them. On Linux systems, using memfd_create(2) enables the library to be loaded without writing to the filesystem.

Another requirement of libddwaf is to have a FHS filesystem on your machine and, for Linux, to provide libc.so.6, libpthread.so.0, and libdl.so.2 as dynamic libraries.

⚠ Keep in mind that purego only works on linux/darwin for amd64/arm64 and so does go-libddwaf.

Contributing pitfalls

  • Cannot dlopen twice in the app lifetime on OSX. It messes with Thread Local Storage and usually finishes with a std::bad_alloc()
  • keepAlive() calls are here to prevent the GC from destroying objects too early
  • Since there is a stack switch between the Go code and the C code, usually the only C stacktrace you will ever get is from GDB
  • If a segfault happens during a call to the C code, the goroutine stacktrace which has done the call is the one annotated with [syscall]
  • GoLand does not support CGO_ENABLED=0 (as of June 2023)
  • Keep in mind that we fully escape the type system. If you send the wrong data it will segfault in the best cases but not always!
  • The structs in ctypes.go are here to reproduce the memory layout of the structs in include/ddwaf.h because pointers to these structs will be passed directly
  • Do not use uintptr as function arguments or results types, coming from unsafe.Pointer casts of Go values, because they escape the pointer analysis which can create wrongly optimized code and crash. Pointer arithmetic is of course necessary in such a library but must be kept in the same function scope.
  • GDB is available on arm64 but is not officially supported so it usually crashes pretty fast (as of June 2023)
  • No pointer to variables on the stack shall be sent to the C code because Go stacks can be moved during the C call. More on this here

Debugging

Debug-logging can be enabled for underlying C/C++ library by building (or testing) by setting the DD_APPSEC_WAF_LOG_LEVEL environment variable to one of: trace, debug, info, warn (or warning), error, off (which is the default behavior and logs nothing).

The DD_APPSEC_WAF_LOG_FILTER environment variable can be set to a valid (per the regexp package) regular expression to limit logging to only messages that match the regular expression.

Documentation

Overview

Package libddwaf provides Go bindings for libddwaf — Datadog's in-app Web Application Firewall evaluation engine.

The package offers a high-level API built around three core types:

  • Builder: constructs a Handle from one or more ruleset fragments.
  • Handle: holds a compiled ruleset; creates per-request [Context]s.
  • Context: evaluates input data against the ruleset.
  • Subcontext: scopes ephemeral data to a shorter lifetime within a Context.

A typical usage pattern:

builder, err := libddwaf.NewBuilder()
if err != nil { /* handle */ }
defer builder.Close()
if _, err := builder.AddOrUpdateConfig("/rules", ruleset); err != nil { /* handle */ }
handle, err := builder.Build()
if err != nil { /* handle */ }
defer handle.Close()
ctx, err := handle.NewContext(context.Background())
if err != nil { /* handle */ }
defer ctx.Close()
result, err := ctx.Run(context.Background(), libddwaf.RunAddressData{Data: input})
subCtx, err := ctx.NewSubcontext(context.Background())
// ...
result, err = subCtx.Run(context.Background(), libddwaf.RunAddressData{Data: ephemeral})

For supported platforms, loading mechanics, and CGO/purego trade-offs, see the repository README.

Index

Constants

View Source
const (
	// EncodeTimeKey is the key used to track the time spent encoding the address data reported in [Result.TimerStats].
	EncodeTimeKey timer.Key = "encode"
	// DurationTimeKey is the key used to track the time spent in libddwaf ddwaf_run C function reported in [Result.TimerStats].
	DurationTimeKey timer.Key = "duration"
	// DecodeTimeKey is the key used to track the time spent decoding the address data reported in [Result.TimerStats].
	DecodeTimeKey timer.Key = "decode"
)
View Source
const (
	// AppsecFieldTag is the struct tag recognized by the encoder when
	// deciding how to process struct fields (for example, `ddwaf:"ignore"`).
	AppsecFieldTag = "ddwaf"
	// AppsecFieldTagValueIgnore is the struct tag value that causes the
	// annotated field to be skipped during encoding.
	AppsecFieldTagValueIgnore = "ignore"
)

Variables

This section is empty.

Functions

func DecodeObject deprecated

func DecodeObject(obj *WAFObject) (any, error)

DecodeObject decodes a WAFObject into a generic Go value.

Deprecated: This is merely wrapping [WAFObject.AnyValue], which should be used directly instead.

func Load

func Load() (bool, error)

Load loads libddwaf's dynamic library. The dynamic library is opened only once by the first call to this function and internally stored globally. No function is currently provided in this API to unload it.

This function is automatically called by NewBuilder, and most users need not explicitly call it. It is however useful in order to explicitly check for the status of the WAF library's initialization.

The function returns true when libddwaf was successfully loaded, along with an error value. An error might still be returned even though the WAF load was successful: in such cases the error is indicative that some non-critical features are not available; but the WAF may still be used.

On macOS, subsequent calls return the cached result from the first call. This is intentional: macOS does not support dlopen-ing the same library twice in the same process lifetime.

func Usable

func Usable() (bool, error)

Usable returns true if the WAF is usable, false and an error otherwise.

If the WAF is usable, an error value may still be returned and should be treated as a warning (it is non-blocking).

The following conditions are checked:

  • The WAF library has been loaded successfully (you need to call Load first for this case to be taken into account)
  • The WAF library has not been manually disabled with the `datadog.no_waf` go build tag
  • The WAF library is not in an unsupported OS/Arch
  • The WAF library is not in an unsupported Go version

func Version

func Version() string

Version returns the version returned by libddwaf. It relies on the dynamic loading of the library, which can fail and return an empty string or the previously loaded version, if any.

Types

type ArrayBuilder

type ArrayBuilder struct {
	// contains filtered or unexported fields
}

ArrayBuilder accumulates WAFObject entries and commits them to a parent WAFObject on Close. It is not safe for concurrent use.

func (*ArrayBuilder) Close

func (b *ArrayBuilder) Close()

Close commits the accumulated entries to the parent WAFObject. If any entries were skipped, a ContainerTooLarge truncation is recorded. Calling Close more than once is safe and has no additional effect.

func (*ArrayBuilder) DropLast

func (b *ArrayBuilder) DropLast()

DropLast removes the most recently appended entry. No-op if the builder has no entries. Use this to undo a NextValue call when encoding the value fails.

func (*ArrayBuilder) NextValue

func (b *ArrayBuilder) NextValue() *WAFObject

NextValue appends a new slot to the array and returns a pointer to the WAFObject for the caller to populate. Returns nil when the builder is at MaxContainerSize capacity. The caller must populate the slot or call DropLast before calling NextValue again.

func (*ArrayBuilder) Skip

func (b *ArrayBuilder) Skip()

Skip records that an array element was intentionally omitted. The count is used by Close to record a ContainerTooLarge truncation if any were skipped.

type Builder

type Builder struct {
	// contains filtered or unexported fields
}

Builder manages an evolving WAF configuration. Builder is not thread-safe. Concurrent use panics under `ci` builds.

func NewBuilder

func NewBuilder() (*Builder, error)

NewBuilder creates a new Builder instance. The caller must call Builder.Close when it is no longer needed.

func (*Builder) AddDefaultRecommendedRuleset

func (b *Builder) AddDefaultRecommendedRuleset() (Diagnostics, error)

AddDefaultRecommendedRuleset adds the default recommended ruleset to the receiving Builder, and returns the Diagnostics produced in the process.

func (*Builder) AddOrUpdateConfig

func (b *Builder) AddOrUpdateConfig(path string, fragment any) (Diagnostics, error)

AddOrUpdateConfig adds or updates a configuration fragment to this Builder. Returns the Diagnostics produced by adding or updating this configuration.

func (*Builder) Build

func (b *Builder) Build() (*Handle, error)

Build creates a new Handle instance that uses the current configuration. Returns an error if the builder is not initialized or the C library fails to build the handle. The caller is responsible for calling Handle.Close when the handle is no longer needed.

func (*Builder) Close

func (b *Builder) Close()

Close releases all resources associated with this builder.

func (*Builder) ConfigPaths

func (b *Builder) ConfigPaths(filter string) ([]string, error)

ConfigPaths returns the list of currently loaded configuration paths.

func (*Builder) RemoveConfig

func (b *Builder) RemoveConfig(path string) bool

RemoveConfig removes the configuration associated with the given path from this Builder. Returns true if the removal was successful.

func (*Builder) RemoveDefaultRecommendedRuleset

func (b *Builder) RemoveDefaultRecommendedRuleset() bool

RemoveDefaultRecommendedRuleset removes the default recommended ruleset from the receiving Builder. Returns true if the removal occurred (meaning the default recommended ruleset was indeed present in the builder).

type Context

type Context struct {
	// Timer registers the time spent in the WAF and go-libddwaf. It is created alongside the Context using the options
	// passed in to NewContext. Once its time budget is exhausted, each new call to Context.Run will return a timeout error.
	Timer timer.NodeTimer
	// contains filtered or unexported fields
}

Context is a WAF execution context. It allows running the WAF incrementally when calling it multiple times to run its rules every time new addresses become available. Each request must have its own Context. New Context instances can be created by calling Handle.NewContext.

Subcontexts can be created via Context.NewSubcontext. Data passed to a subcontext is stored and persists across multiple calls to Run on that subcontext, until the subcontext is closed.

Concurrency

Context.Run may be called concurrently; same-Context Run calls serialize internally via a per-instance mutex. Context.Run may run concurrently with Subcontext.Run calls under the same Context.

Context.Close waits for all in-flight Run and NewSubcontext operations to complete before destroying the underlying ddwaf_context.

func (*Context) Close

func (context *Context) Close()

Close disposes of the context: it destroys the underlying ddwaf_context, releases associated data, and decreases the reference count of the Handle created for this Context.

Close cascades to subcontexts: any still-open Subcontext derived from this Context via Context.NewSubcontext is fully torn down as part of Close — its underlying ddwaf_subcontext is destroyed and its pinned input data released. (The underlying ddwaf_context_destroy does not itself cascade, so go-libddwaf destroys each live subcontext explicitly before destroying the context.) Calling Subcontext.Close afterwards remains safe and becomes a no-op.

Close blocks until all in-flight Run and NewSubcontext operations complete, and is safe to call more than once.

func (*Context) NewSubcontext

func (context *Context) NewSubcontext(ctx context.Context) (*Subcontext, error)

NewSubcontext creates a subcontext derived from this context. The provided ctx only scopes the construction call itself and is not retained after NewSubcontext returns; per-run deadlines and cancellation must be supplied to Context.Run via its own ctx argument. Data passed to the subcontext's Run() is stored and persists across multiple calls to Run on that subcontext, but does not persist in the parent context. When the subcontext is closed, its data is released.

A subcontext gets its own timer whose initial budget is snapshotted from the caller's remaining budget when the subcontext is created.

Usage:

subCtx, err := ctx.NewSubcontext(context.Background())
if err != nil {
    return err
}
defer subCtx.Close()
result, err := subCtx.Run(context.Background(), RunAddressData{Data: data})

func (*Context) Run

func (context *Context) Run(ctx context.Context, addressData RunAddressData) (res Result, err error)

Run encodes the given RunAddressData values and runs them against the WAF rules. Callers must check the returned Result object even when an error is returned, as the WAF might have been able to match some rules and generate events or actions before the error was reached.

Deadline precedence is the minimum of ctx's deadline and the remaining Context.Timer budget. If ctx fires first, Run returns ctx.Err(). If the timer budget fires first, Run returns waferrors.ErrTimeout.

func (*Context) Truncations

func (context *Context) Truncations() Truncations

Truncations returns the truncations that occurred while encoding address data for WAF execution. The returned value is a snapshot; subsequent Run calls may accumulate more truncations.

type Diagnostics

type Diagnostics struct {
	// Rules contains information about the loaded rules.
	Rules *Feature
	// CustomRules contains information about the loaded custom rules.
	CustomRules *Feature
	// Actions contains information about the loaded actions.
	Actions *Feature
	// Exclusions contains information about the loaded exclusions.
	Exclusions *Feature
	// RulesOverrides contains information about the loaded rules overrides.
	RulesOverrides *Feature
	// RulesData contains information about the loaded rules data.
	RulesData *Feature
	// ExclusionData contains information about the loaded exclusion data.
	ExclusionData *Feature
	// Processors contains information about the loaded processors.
	Processors *Feature
	// ProcessorOverrides contains information about the loaded processor overrides.
	ProcessorOverrides *Feature
	// Scanners contains information about the loaded scanners.
	Scanners *Feature
	// Version is the version of the parsed ruleset if available.
	Version string
}

Diagnostics stores the information as provided by the WAF about WAF rules parsing and loading. It is returned by Builder.AddOrUpdateConfig.

func (*Diagnostics) EachFeature

func (d *Diagnostics) EachFeature(cb func(string, *Feature))

EachFeature calls the provided callback for each (non-nil) feature in this diagnostics object.

func (*Diagnostics) TopLevelError

func (d *Diagnostics) TopLevelError() error

TopLevelError returns the list of top-level errors reported by the WAF on any of the Diagnostics entries, rolled up into a single error value. Returns nil if no top-level errors were reported. Individual, item-level errors might still exist.

type Encodable

type Encodable interface {
	Encode(enc *Encoder, obj *WAFObject, depth int) error
}

Encodable represents a type that can encode itself into a WAFObject.

The encoder writes into obj using the provided *Encoder for size limits, pinning, timer, and truncation accounting.

Best-effort encoding philosophy: implementers should self-recover from input parsing errors by leaving obj as invalid (zero value) or calling [WAFObject.SetInvalid]. Only fatal conditions should be returned as errors:

The depth parameter is the remaining recursion budget. Implementers must decrement and check before recursing into nested structures.

The *Encoder is owned by the caller; implementations must NOT retain the pointer past return. Truncations accumulate in enc.Truncations (shared across nested calls).

type Encoder

type Encoder struct {
	Config      EncoderConfig
	Truncations Truncations
}

Encoder is the public encoding helper exposed to Encodable implementers. It provides string-writing utilities, map/array builders, timer checking, and truncation accounting.

func (*Encoder) Array

func (e *Encoder) Array(parent *WAFObject, capacityHint int) *ArrayBuilder

Array returns a new ArrayBuilder that will commit its entries into parent. capacityHint pre-allocates the backing slice (capped at MaxContainerSize); pass 0 when the eventual size is unknown.

func (*Encoder) Map

func (e *Encoder) Map(parent *WAFObject, capacityHint int) *MapBuilder

Map returns a new MapBuilder that will commit its entries into parent. capacityHint pre-allocates the backing slice (capped at MaxContainerSize); pass 0 when the eventual size is unknown.

func (*Encoder) Timeout

func (e *Encoder) Timeout() bool

func (*Encoder) WriteLiteralString

func (e *Encoder) WriteLiteralString(obj *WAFObject, str string)

func (*Encoder) WriteString

func (e *Encoder) WriteString(obj *WAFObject, str string)

type EncoderConfig

type EncoderConfig struct {
	// Pinner is used to pin the data referenced by the encoded wafObjects.
	Pinner *runtime.Pinner
	// Timer makes sure the encoder doesn't spend too much time doing its job.
	Timer timer.Timer
	// MaxContainerSize is the maximum number of elements in a container (list, map, struct) that will be encoded.
	MaxContainerSize uint16
	// MaxStringSize is the maximum length of a string that will be encoded.
	MaxStringSize uint16
	// MaxObjectDepth is the maximum depth of the object that will be encoded.
	MaxObjectDepth uint16
}

EncoderConfig provides configuration for an [encoder], including pinning, timing, and size limits for strings, containers, and object depth.

type EncoderOption

type EncoderOption func(*EncoderConfig)

EncoderOption mutates an EncoderConfig created by [newEncoderConfig].

func WithTimer

func WithTimer(timer timer.Timer) EncoderOption

WithTimer configures [newEncoderConfig] to use the provided timer.

func WithUnlimitedLimits

func WithUnlimitedLimits() EncoderOption

WithUnlimitedLimits configures [newEncoderConfig] to disable encoder truncation limits.

type Feature

type Feature struct {
	// Errors is a map of parsing errors to a list of unique identifiers from the elements which
	// failed loading due to this specific error.
	Errors map[string][]string
	// Warnings is a map of parsing warnings to a list of unique identifiers from the elements which
	// resulted in this specific warning.
	Warnings map[string][]string
	// Error is the single error which prevented parsing this feature.
	Error string
	// Loaded is a list of the unique identifiers from successfully loaded elements.
	Loaded []string
	// Failed is a list of the unique identifiers from the elements which couldn't be loaded.
	Failed []string
	// Skipped is a list of the unique identifiers from the elements which were skipped.
	Skipped []string
}

Feature stores the information as provided by the WAF about loaded and failed rules for a specific feature of the WAF ruleset.

type Handle

type Handle struct {
	// contains filtered or unexported fields
}

Handle represents an instance of the WAF for a given ruleset. It is obtained from Builder.Build; and must be disposed of by calling Handle.Close once no longer in use.

The reference count equals the number of alive Context values plus the number of alive Subcontext values. Each Handle.NewContext call retains the handle, each Context.Close releases it, each Context.NewSubcontext call retains it, and each Subcontext.Close releases it.

Library lifetime boundary: [Handle.retain] and Handle.Close only govern the lifetime of the underlying C ddwaf_handle. They do not keep the libddwaf shared library loaded. That shared library is a process-wide singleton loaded once via purego.Dlopen (see internal/bindings) and is never Dlclosed during normal operation. As a consequence, symbols such as bindings.Lib.ContextDestroy or bindings.Lib.SubcontextDestroy remain resolvable for the entire lifetime of the process, and Context/SubContext teardown paths that call into bindings.Lib after the Handle's refcount has reached zero are safe with respect to the library itself.

func (*Handle) Actions

func (handle *Handle) Actions() []string

Actions returns the list of actions the WAF has been configured to monitor based on the input ruleset.

func (*Handle) Addresses

func (handle *Handle) Addresses() []string

Addresses returns the list of addresses the WAF has been configured to monitor based on the input ruleset.

func (*Handle) Close

func (handle *Handle) Close()

Close decrements the reference counter of this Handle, possibly allowing it to be destroyed and all the resources associated with it to be released.

func (*Handle) NewContext

func (handle *Handle) NewContext(ctx context.Context, timerOptions ...timer.Option) (*Context, error)

NewContext returns a new WAF context for the given WAF handle. The provided ctx only scopes the construction call itself and is not retained after NewContext returns; per-run deadlines and cancellation must be supplied to Context.Run via its own ctx argument. An error is returned when the WAF handle was released or when the WAF context couldn't be created.

type MapBuilder

type MapBuilder struct {
	// contains filtered or unexported fields
}

MapBuilder accumulates WAFObjectKV entries and commits them to a parent WAFObject on Close. It is not safe for concurrent use.

func (*MapBuilder) Close

func (b *MapBuilder) Close()

Close commits the accumulated entries to the parent WAFObject. If any entries were skipped, a ContainerTooLarge truncation is recorded. Calling Close more than once is safe and has no additional effect.

func (*MapBuilder) DropLast

func (b *MapBuilder) DropLast()

DropLast removes the most recently appended entry. No-op if the builder has no entries. Use this to undo a NextValue call when encoding the value fails.

func (*MapBuilder) NextValue

func (b *MapBuilder) NextValue(key string) *WAFObject

NextValue appends a new key-value slot to the map and returns a pointer to the value WAFObject for the caller to populate. Returns nil when the builder is at MaxContainerSize capacity. The caller must populate the value or call DropLast before calling NextValue again.

func (*MapBuilder) Skip

func (b *MapBuilder) Skip()

Skip records that a key-value pair was intentionally omitted (e.g. due to an encoding error). The count is used by Close to record a ContainerTooLarge truncation if any entries were skipped.

type Result

type Result struct {
	// Events is the list of events the WAF detected, together with any relevant
	// details. These are typically forwarded as opaque objects to the Datadog
	// backend.
	Events []any

	// Derivatives is the set of key-value pairs generated by the WAF, and which
	// need to be reported on the trace to provide additional data to the Datadog
	// backend.
	Derivatives map[string]any

	// Actions is the set of actions the WAF decided on when evaluating rules
	// against the provided address data. It maps action types to their dynamic
	// parameter values.
	Actions map[string]any

	// TimerStats records the time spent in the different parts of the run.
	TimerStats map[timer.Key]time.Duration

	// Keep is true if the WAF instructs the trace to be kept with manual priority.
	Keep bool
}

Result stores the multiple values returned by a call to Context.Run.

func (*Result) HasActions

func (r *Result) HasActions() bool

HasActions returns true if the Result holds at least 1 action.

func (*Result) HasDerivatives

func (r *Result) HasDerivatives() bool

HasDerivatives returns true if the Result holds at least 1 derivative.

func (*Result) HasEvents

func (r *Result) HasEvents() bool

HasEvents returns true if the Result holds at least 1 event.

type RunAddressData

type RunAddressData struct {
	// Data is passed to the WAF and persists across multiple calls to Run
	// until the context or subcontext closes.
	Data map[string]any

	// TimerKey tracks time spent in the WAF for this run.
	// Leave it empty to start a new timer with unlimited budget.
	TimerKey timer.Key
}

RunAddressData provides address data to the Context.Run method. Fields tagged `ddwaf:"ignore"` are omitted when encoding Go structs to the WAF-compatible format.

Data passed to Run persists for the lifetime of the context or subcontext. Use NewSubcontext() for shorter-lived data.

type Subcontext

type Subcontext struct {
	Timer timer.NodeTimer
	// contains filtered or unexported fields
}

Subcontext is a derived ephemeral evaluation scope. Spawned via Context.NewSubcontext. Data passed to a Subcontext's Run persists for the Subcontext's lifetime and is released when the Subcontext is closed.

Concurrency

Subcontext.Run may be called concurrently across different Subcontexts of the same parent; same-Subcontext Run calls serialize internally.

Subcontext.Close should be called before its parent Context.Close for clean teardown; if the parent is already closed, Subcontext.Close is still safe (it skips the underlying destroy).

func (*Subcontext) Close

func (s *Subcontext) Close()

Close disposes of the underlying subcontext.

func (*Subcontext) Run

func (s *Subcontext) Run(ctx context.Context, addressData RunAddressData) (res Result, err error)

Run encodes the given RunAddressData values and runs them against the WAF rules.

func (*Subcontext) Truncations

func (s *Subcontext) Truncations() Truncations

Truncations returns the truncations that occurred while encoding address data for WAF execution.

type TruncationReason

type TruncationReason uint8

TruncationReason is a flag representing reasons why some input was not encoded in full.

const (
	// StringTooLong indicates a string exceeded the maximum string length configured. The truncation
	// values indicate the actual length of truncated strings.
	StringTooLong TruncationReason = 1 << iota
	// ContainerTooLarge indicates a container (list, map, struct) exceeded the maximum number of
	// elements configured. The truncation values indicate the actual number of elements in the
	// truncated container.
	ContainerTooLarge
	// ObjectTooDeep indicates an overall object exceeded the maximum encoding depths configured. The
	// truncation values indicate an estimated actual depth of the truncated object. The value is
	// guaranteed to be less than or equal to the actual depth (it may not be more).
	ObjectTooDeep
)

func (TruncationReason) String

func (reason TruncationReason) String() string

type Truncations

type Truncations struct {
	StringTooLong     []int
	ContainerTooLarge []int
	ObjectTooDeep     []int
}

Truncations accumulates truncation events that occurred during encoding.

func (Truncations) AsMap

func (t Truncations) AsMap() map[TruncationReason][]int

func (Truncations) IsEmpty

func (t Truncations) IsEmpty() bool

func (*Truncations) Merge

func (t *Truncations) Merge(other Truncations)

func (*Truncations) Record

func (t *Truncations) Record(reason TruncationReason, size int)

type WAFObject

type WAFObject = bindings.WAFObject

WAFObject is a 16-byte value type matching the C ddwaf_object union. It can be constructed as a zero value (which is invalid by default) and mutated in place via Set* methods.

type WAFObjectKV

type WAFObjectKV = bindings.WAFObjectKV

WAFObjectKV is a key-value pair used in v2 maps. Its zero value is two invalid WAFObjects.

type WAFObjectType

type WAFObjectType = bindings.WAFObjectType

WAFObjectType is the type discriminator for a WAFObject.

Type constants for WAFObject.

Directories

Path Synopsis
internal
invariant
Package invariant provides a runtime-invariant assertion helper that panics when the condition is violated and the build is compiled with the "ci" build tag.
Package invariant provides a runtime-invariant assertion helper that panics when the condition is violated and the build is compiled with the "ci" build tag.
lib
Package lib provides a built-in WAF library version for the relevant runtime platform.
Package lib provides a built-in WAF library version for the relevant runtime platform.
log
unsafeutil
Package unsafeutil provides helpers for unsafe pointer operations at the FFI boundary.
Package unsafeutil provides helpers for unsafe pointer operations at the FFI boundary.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL