Skip to main content
Version: latest

Hello world gadget with Wasm

This guide explores the wasm support to implement complex logic in our gadget. This is a continuation of hello world gadget, be sure you are familiar with that guide before continuing with this.

Creating our first wasm program

Create a folder named go next to the program.bpf.c file. In there we create a new file named program.go. As a first step, let's define the init, start and stop functions and emit some log messages from them:

package main

import (
api "github.com/inspektor-gadget/inspektor-gadget/wasmapi/go"
)

//export gadgetInit
func gadgetInit() int {
api.Info("init: hello from wasm")
return 0
}

//export gadgetStart
func gadgetStart() int {
api.Info("start: hello from wasm")
return 0
}

//export gadgetStop
func gadgetStop() int {
api.Info("stop: hello from wasm")
return 0
}

// The main function is not used, but it's still required by the compiler
func main() {}

Run the following commands in the go directory to initialize the Golang module:

$ cd go
$ go mod init mygadget
go: creating new go.mod: module mygadget
go: to add module requirements and sums:
go mod tidy
$ go mod tidy
go: finding module for package github.com/inspektor-gadget/inspektor-gadget/wasmapi/go
go: found github.com/inspektor-gadget/inspektor-gadget/wasmapi/go in github.com/inspektor-gadget/inspektor-gadget v0.31.0

We also need a build.yaml file that indicates the gadget includes a Golang program that needs to be compiled to a wasm module:

wasm: go/program.go

Build the gadget

$ sudo ig image build . -t mygadget:latest

and run it:

$ sudo ig run mygadget:latest --verify-image=false
WARN[0000] image signature verification is disabled due to using corresponding option
INFO[0000] init: hello from wasm
WARN[0000] image signature verification is disabled due to using corresponding option
INFO[0000] init: hello from wasm
RUNTIME.CONTAINERNAME MNTNS_ID PID COMM FILENAME
INFO[0001] start: hello from wasm
...
^CINFO[0009] stop: hello from wasm

You can see how the different messages coming from wasm are printed in the terminal.

Manipulating fields

Now let's do something more interesting. Let's suppose we want to redact the user's name in the path of the file. Manipulating strings in eBPF is usually complicated, leave aside using regular expressions.

Our goal is to look for strings like /home/<user-name>/... and redact (replace by ***) the user name part. This can be done by using a regular expression.

Let's add it to the gadgetInit function like this:

//export gadgetInit
func gadgetInit() int {
api.Info("init: hello from wasm")

// Get the "open" datasource (name used in the GADGET_TRACER macro)
ds, err := api.GetDataSource("open")
if err != nil {
api.Warnf("failed to get datasource: %s", err)
return 1
}

// Get the field we're interested in
filenameF, err := ds.GetField("filename")
if err != nil {
api.Warnf("failed to get field: %s", err)
return 1
}

pattern := regexp.MustCompile(`^(/home/)(.*?)/(.*)$`)

// Subscribe to all events from "open" so we manipulate the data in the callback
ds.Subscribe(func(source api.DataSource, data api.Data) {
fileName, err := filenameF.String(data)
if err != nil {
api.Warnf("failed to get field fileName: %s", err)
return 1
}
replaced := pattern.ReplaceAllString(fileName, "${1}***/${3}")
filenameF.SetString(data, replaced)
}, 0)

return 0
}

Build and run the gadget again:

$ sudo ig image build . -t mygadget:latest
...


$ sudo ig run mygadget:latest --verify-image=false
WARN[0000] image signature verification is disabled due to using corresponding option
INFO[0000] init: hello from wasm
WARN[0000] image signature verification is disabled due to using corresponding option
INFO[0000] init: hello from wasm
RUNTIME.CONTAINERNAME MNTNS_ID PID COMM FILENAME
INFO[0001] start: hello from wasm

Let's generate some events from a container:

$ docker run --name c3 --rm -it busybox sh

# inside the container:
$ mkdir /home/mvb
$ touch /home/mvb/xxx.txt
$ cat /home/mvb/xxx.txt

The gadget redacts the user name as expected:

RUNTIME.CONTAINERNAME        MNTNS_ID            PID            COMM           FILENAME
c3 4026534569 226136 cat /home/***/xxx.txt

Adding new fields

There are cases where we want to add new fields from wasm. For instance, let's add a field that contains a human readable representation of the event.

The gadgetInit functions now looks like:

//export gadgetInit
func gadgetInit() int {
api.Info("init: hello from wasm")

// Get the "open" datasource (name used in the GADGET_TRACER macro)
ds, err := api.GetDataSource("open")
if err != nil {
api.Warnf("failed to get datasource: %s", err)
return 1
}

// Get the field we're interested in
filenameF, err := ds.GetField("filename")
if err != nil {
api.Warnf("failed to get field: %s", err)
return 1
}

pidF, err := ds.GetField("pid")
if err != nil {
api.Warnf("failed to get field: %s", err)
return 1
}

humanF, err := ds.AddField("human", api.Kind_String)
if err != nil {
api.Warnf("failed to add field: %s", err)
return 1
}

pattern := regexp.MustCompile(`^(/home/)(.*?)/(.*)$`)

// Subscribe to all events from "open" so we manipulate the data in the callback
ds.Subscribe(func(source api.DataSource, data api.Data) {
fileName, err:= filenameF.String(data)
if err != nil {
api.Warnf("failed to get field fileName: %s", err)
return
}
replaced := pattern.ReplaceAllString(fileName, "${1}***/${3}")
filenameF.SetString(data, replaced)

pid, err:= pidF.Uint32(data)
if err != nil {
api.Warnf("failed to get pid: %s", err)
return
}
human := fmt.Sprintf("file %q was opened by %d", fileName, pid)
humanF.SetString(data, human)
}, 0)

return 0
}

Build and run the gadget again. This time using -o json to easily see the output from it:

$ sudo ig image build . -t mygadget:latest
...

$ sudo ig run mygadget:latest --verify-image=false -o jsonpretty
{
"comm": "cat",
"filename": "/home/***/xxx.txt",
"human": "file '/home/mvb/xxx.txt' was opened by 121351",
"k8s": {
"container": "",
"hostnetwork": false,
"namespace": "",
"node": "",
"pod": ""
},
"mntns_id": 4026534661,
"pid": 121351,
"runtime": {
"containerId": "2de33de4d1c73be918916322bf488a32f8b7a6eea0903422278fa13766e36f8f",
"containerImageDigest": "",
"containerImageName": "busybox",
"containerName": "c3",
"runtimeName": "docker"
}
}

Notice how the human field is there when cat /home/mvb/xxx.txt is executed in the container.

Dropping events

TBD!