waf

package module
v1.7.0 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2023 License: Apache-2.0 Imports: 18 Imported by: 1

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 which job is to convert any go value to its libddwaf object representation.

An example usage would be:

import waf "github.com/DataDog/go-libddwaf"

//go:embed
var ruleset []byte

func main() {
    var parsedRuleset any

    if err := json.Unmarshal(ruleset, &parsedRuleset); err != nil {
        return 1
    }

    wafHandle, err := waf.NewHandle(parsedRuleset, "", "")
    if err != nil {
        return 1
    }

    defer wafHandle.Close()

    wafCtx := wafHandle.NewContext()
    defer wafCtx.Close()

    matches, actions := wafCtx.Run(map[string]any{
        "server.request.path_params": "/rfiinc.txt",
    }, time.Minute)
}

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

Originally this project was only here to provide CGO Wrappers to the calls to libddwaf. But with the appearance of ddwaf_object tree like structure, but also with the intention to build CGO-less bindings, this project size has grown to be a fully integrated brick in the DataDog tracer structure. Which in turn made it necessary to document the project, to maintain it in an orderly fashion.

Design

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

  • Handle: a object wrapper over the pointer to the C WAF Handle
  • Context: a 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
  • CGORefPool: Does all allocation operations for the construction of Waf Objects and keeps track of the equivalent go pointers
  • 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 -->|NewHandle| Handle -->|NewContext| Context

    Context -->|Encode Inputs| Encoder

    Handle -->|Encode Ruleset| Encoder
    Handle -->|Init WAF| Library
    Context -->|Decode Result| Decoder

    Handle -->|Decode Init Errors| Decoder

    Context -->|Run| Library
    Context -->|Store Go References| CGORefPool

    Encoder -->|Allocate Waf Objects| TempCGORefPool

    TempCGORefPool -->|Copy after each encoding| CGORefPool

    Library -->|Call C code| libddwaf

    classDef hidden display: none;
CGO Reference Pool

The cgoRefPool type is a pure Go pointer pool of ddwaf_object C values on the Go memory heap. the cgoRefPool go type is a way to make sure we can safely send Go allocated data to the C side of the WAF The main issue is the following: the wafObject uses a C union to store the tree structure of the full object, union equivalent in go are interfaces and they are not compatible with C unions. The only way to be 100% sure that the Go wafObject struct has the same layout as the C one is to only use primitive types. So the only way to store a raw pointer is to use the uintptr type. But since uintptr do not have pointer semantics (and are just basically integers), we need another method to store the value as Go pointer because the GC will delete our data if it is not referenced by Go pointers.

That's where the cgoRefPool object comes into play: all new wafObject elements are created via this API which is especially built to make sure there is no gap for the Garbage Collector to exploit. From there, since underlying values of the wafObject are either arrays of wafObjects (for maps, structs and arrays) or string (for all ints, booleans and strings), we can store 2 slices of arrays and use runtime.KeepAlive in each code path to protect them from the GC.

All these objects stored in the reference pool need to live throughout the use of the associated Waf Context.

Typical call to Run()

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

  • Encode input data into WAF Objects and store references in the temporary pool
  • Lock the context mutex until the end of the call
  • Store references from the temporary pool into the context level pool
  • Call ddwaf_run
  • Decode the matches and actions
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.

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

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

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

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Load added in v1.3.0

func Load() (ok bool, err 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, and no function is currently provided in this API to close the opened handle. Calling this function is not mandatory and is automatically performed by calls to NewHandle, the entrypoint of libddwaf, but Load is useful in order to explicitly check libddwaf's general health where calling NewHandle doesn't necessarily apply nor is doable. The function returns ok when libddwaf was successfully loaded, along with a non-nil error if any. Note that both ok and err can be set, meaning that libddwaf is usable but some non-critical errors happened, such as failures to remove temporary files. It is safe to continue using libddwaf in such case.

func SupportsTarget added in v1.4.0

func SupportsTarget() (bool, error)

SupportsTarget returns true and a nil error when the target host environment is supported by this package and can be further used. Otherwise, it returns false along with an error detailing why.

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 Context

type Context struct {
	// 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.

func NewContext

func NewContext(handle *Handle) *Context

NewContext returns a new WAF context of to the given WAF handle. A nil value is returned when the WAF handle was released or when the WAF context couldn't be created. handle. A nil value is returned when the WAF handle can no longer be used or the WAF context couldn't be created.

func (*Context) Close

func (context *Context) Close()

Close calls handle.closeContext which calls ddwaf_context_destroy and maybe also close the handle if it in termination state.

func (*Context) Run

func (context *Context) Run(addressesToData map[string]any, timeout time.Duration) (matches []byte, actions []string, err error)

Run encodes the given addressesToData values and runs them against the WAF rules within the given timeout value. It returns the matches as a JSON string (usually opaquely used) along with the corresponding actions in any. In case of an error, matches and actions can still be returned, for instance in the case of a timeout error. Errors can be tested against the RunError type.

func (*Context) TotalRuntime

func (context *Context) TotalRuntime() (overallRuntimeNs, internalRuntimeNs uint64)

TotalRuntime returns the cumulated WAF runtime across various run calls within the same WAF context. Returned time is in nanoseconds.

func (*Context) TotalTimeouts

func (context *Context) TotalTimeouts() uint64

TotalTimeouts returns the cumulated amount of WAF timeouts across various run calls within the same WAF context.

type Handle

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

Handle represents an instance of the WAF for a given ruleset.

func NewHandle

func NewHandle(rules any, keyObfuscatorRegex string, valueObfuscatorRegex string) (*Handle, error)

NewHandle creates and returns a new instance of the WAF with the given security rules and configuration of the sensitive data obfuscator. The returned handle is nil in case of an error. Rules-related metrics, including errors, are accessible with the `RulesetInfo()` method.

func (*Handle) Addresses

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

Addresses returns the list of addresses the WAF rule is expecting.

func (*Handle) Close

func (handle *Handle) Close()

Close puts the handle in termination state, when all the contexts are closed the handle will be destroyed

func (*Handle) RulesetInfo

func (handle *Handle) RulesetInfo() RulesetInfo

RulesetInfo returns the rules initialization metrics for the current WAF handle

func (*Handle) Update added in v1.5.0

func (handle *Handle) Update(newRules any) (*Handle, error)

Update the ruleset of a WAF instance into a new handle on its own the previous handle still needs to be closed manually

type PanicError added in v1.3.0

type PanicError struct {

	// The recovered panic error while executing the function `in`.
	Err error
	// contains filtered or unexported fields
}

PanicError is an error type wrapping a recovered panic value that happened during a function call. Such error must be considered unrecoverable and be used to try to gracefully abort. Keeping using this package after such an error is unreliable and the caller must rather stop using the library. Examples include safety checks errors.

func (*PanicError) Error added in v1.3.0

func (e *PanicError) Error() string

Error returns the error string representation.

func (*PanicError) Unwrap added in v1.3.0

func (e *PanicError) Unwrap() error

Unwrap the error and return it. Required by errors.Is and errors.As functions.

type RulesetInfo

type RulesetInfo struct {
	// Number of rules successfully loaded
	Loaded uint16
	// Number of rules which failed to parse
	Failed uint16
	// Map from an error string to an array of all the rule ids for which
	// that error was raised. {error: [rule_ids]}
	Errors map[string][]string
	// Ruleset version
	Version string
}

RulesetInfo stores the information - provided by the WAF - about WAF rules initialization.

type RunError

type RunError int

RunError the WAF can return when running it.

const (
	ErrInternal RunError = iota + 1
	ErrInvalidObject
	ErrInvalidArgument
	ErrTimeout
	ErrOutOfMemory
	ErrEmptyRuleAddresses
)

Errors the WAF can return when running it.

func (RunError) Error

func (e RunError) Error() string

Error returns the string representation of the RunError.

type UnsupportedTargetError added in v1.3.0

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

UnsupportedTargetError is a wrapper error type helping to handle the error case of trying to execute this package on an unsupported target environment.

func (*UnsupportedTargetError) Unwrap added in v1.3.0

func (e *UnsupportedTargetError) Unwrap() error

Unwrap the error and return it. Required by errors.Is and errors.As functions.

Directories

Path Synopsis
Package include is required to help go tools support vendoring.
Package include is required to help go tools support vendoring.
internal
noopfree
Package noopfree provides a noop-ed free function.
Package noopfree provides a noop-ed free function.
lib
darwin-amd64
Package vendor is required to help go tools support vendoring.
Package vendor is required to help go tools support vendoring.
darwin-arm64
Package vendor is required to help go tools support vendoring.
Package vendor is required to help go tools support vendoring.
linux-amd64
Package vendor is required to help go tools support vendoring.
Package vendor is required to help go tools support vendoring.
linux-arm64
Package vendor is required to help go tools support vendoring.
Package vendor is required to help go tools support vendoring.

Jump to

Keyboard shortcuts

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