From a397685ee1662da4ddd9533095ba85f61295b0e0 Mon Sep 17 00:00:00 2001 From: Tomoki Sugiura Date: Fri, 12 Jan 2024 17:39:12 +0900 Subject: [PATCH] Add socket-tracer Signed-off-by: Tomoki Sugiura --- .dockerignore | 1 + .github/workflows/main.yaml | 24 +++ .github/workflows/release.yaml | 50 +++++ .gitignore | 4 + CHANGELOG.md | 9 + Dockerfile | 14 ++ Makefile | 15 ++ README.md | 26 +++ RELEASE.md | 76 ++++++++ go.mod | 15 ++ go.sum | 26 +++ socket-tracer/Makefile | 22 +++ socket-tracer/bpf/trace_enter_socket.c | 69 +++++++ socket-tracer/cmd/root.go | 38 ++++ socket-tracer/main.go | 7 + socket-tracer/pkg/bpf/trace_enter_socket.go | 196 ++++++++++++++++++++ 16 files changed, 592 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/main.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 CHANGELOG.md create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 RELEASE.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 socket-tracer/Makefile create mode 100644 socket-tracer/bpf/trace_enter_socket.c create mode 100644 socket-tracer/cmd/root.go create mode 100644 socket-tracer/main.go create mode 100644 socket-tracer/pkg/bpf/trace_enter_socket.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e660fd9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +bin/ diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..11756a1 --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,24 @@ +name: main +on: + pull_request: + push: + branches: + - 'main' +jobs: + build: + name: Build Container Image + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version-file: 'go.mod' + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build and push + uses: docker/build-push-action@v5 + with: + push: false + tags: ghcr.io/cybozu-go/neco-bpftools:dev diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..dbc4ccc --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,50 @@ +name: release +on: + push: + tags: + - 'v*' +jobs: + image: + name: Push container image + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version-file: 'go.mod' + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Set Tag + id: set-tag + run: echo "RELEASE_TAG=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT # Remove "v" prefix. + - name: Build and push + uses: docker/build-push-action@v5 + with: + push: true + tags: ghcr.io/cybozu-go/neco-bpftools:${{ steps.set-tag.output.RELEASE_TAG }} + release: + name: Release on GitHub + needs: image + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Create release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + body: | + See [CHANGELOG.md](./CHANGELOG.md) for details. + draft: false + prerelease: ${{ contains(github.ref, '-') }} diff --git a/.gitignore b/.gitignore index a6d81f7..a608cf0 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,7 @@ go.work ### Custom .idea *.o +bin/ +**/vmlinux.h +**/*bpfeb.go +**/*bpfel.go diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6dfdaaf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change Log + +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/). + +## [Unreleased] + + +[Unreleased]: https://github.com/cybozu-go/neco-bpftools/compare/6082b8a847fef66103a84bde4a051c1870710970...HEAD diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a5dc9a6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM ghcr.io/cybozu/golang:1.21-jammy AS build + +COPY . /work/neco-bpftools +WORKDIR /work/neco-bpftools + +RUN apt-get update -y && \ + apt-get install -y llvm clang libbpf-dev + +RUN make build + +FROM ghcr.io/cybozu/ubuntu:22.04 +LABEL org.opencontainers.image.source="https://github.com/cybozu-go/neco-bpftools" + +COPY --from=build /work/neco-bpftools/bin/* /usr/local/bin diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a4e8d61 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +BPFTOOL := /usr/local/bin/bpftool + +.PHONY: build +build: bpftool + $(MAKE) -C socket-tracer build + +.PHONY: bpftool +bpftool: $(BPFTOOL) +$(BPFTOOL): + git clone --recurse-submodules https://github.com/libbpf/bpftool.git + cd bpftool && \ + git submodule update --init && \ + cd src && \ + $(MAKE) && \ + $(SUDO) $(MAKE) install diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c95bfd --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Neco-bpftools + +Neco-bpftools contains utility tools using eBPF. + +## Available tools + +### socket-tracer + +Socket-tracer traces `socket` system call by specified socket family. +Please see [socket(2)](https://man7.org/linux/man-pages/man2/socket.2.html). + +```console +$ socket-tracer -h +trace socket syscall + +Usage: + socket-tracer [flags] + +Flags: + -f, --family man socket Family value for socket system call. See man socket. Accepts AF_* or number + -h, --help help for socket-tracer +``` + +## License + +Neco-bpftools licensed under GNU General Public License, Version 2. diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..8a32cd6 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,76 @@ +Release procedure +================= + +This document describes how to release a new version. + +## Versioning + +Follow [semantic versioning 2.0.0][semver] to choose the new version number. + +## Prepare change log entries + +Add notable changes since the last release to [CHANGELOG.md](CHANGELOG.md). +It should look like: + +```markdown +(snip) +## [Unreleased] + +### Added +- Implement ... (#35) + +### Changed +- Fix a bug in ... (#33) + +### Removed +- Deprecated `-option` is removed ... (#39) + +(snip) +``` + +## Bump version + +1. Determine a new version number. Then set `VERSION` variable. + + ```console + # Set VERSION and confirm it. It should not have "v" prefix. + $ VERSION=x.y.z + $ echo $VERSION + ``` + +2. Make a branch to release + + ```console + $ git neco dev "bump-$VERSION" + ``` + +3. Edit `CHANGELOG.md` for the new version ([example][]). +4. Commit the change and push it. + + ```console + $ git commit -a -m "Bump version to $VERSION" + $ git neco review + ``` + +5. Merge this branch. +6. Add a git tag to the main HEAD, then push it. + + ```console + # Set VERSION again. + $ VERSION=x.y.z + $ echo $VERSION + + $ git checkout main + $ git pull + $ git tag -a -m "Release v$VERSION" "v$VERSION" + + # Make sure the release tag exists. + $ git tag -ln | grep $VERSION + + $ git push origin "v$VERSION" + ``` + +GitHub actions will build and push artifacts such as container images and +create a new GitHub release. + +[semver]: https://semver.org/spec/v2.0.0.html diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..46da1ce --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/cybozu-go/neco-bpftools + +go 1.21.4 + +require ( + github.com/cilium/ebpf v0.12.3 + github.com/spf13/cobra v1.8.0 + golang.org/x/sys v0.14.1-0.20231108175955-e4099bfacb8c +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9136cc1 --- /dev/null +++ b/go.sum @@ -0,0 +1,26 @@ +github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4= +github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= +github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI= +golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/sys v0.14.1-0.20231108175955-e4099bfacb8c h1:3kC/TjQ+xzIblQv39bCOyRk8fbEeJcDHwbyxPUU2BpA= +golang.org/x/sys v0.14.1-0.20231108175955-e4099bfacb8c/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/socket-tracer/Makefile b/socket-tracer/Makefile new file mode 100644 index 0000000..ca96a3a --- /dev/null +++ b/socket-tracer/Makefile @@ -0,0 +1,22 @@ +PROJECT := socket-tracer + +BIN_DIR := ../bin +HEADER_DIR := ./bpf/include +VMLINUX := $(HEADER_DIR)/vmlinux.h + +.PHONY: build +build: vmlinux + go generate ./... + CGO_ENABLED=0 go build -o -ldflags="-w -s" -o $(BIN_DIR)/$(PROJECT) + +.PHONY: clean +clean: + rm $(BIN_DIR)/$(PROJECT) + go clean + + +.PHONY: vmlinux +vmlinux: $(VMLINUX) +$(VMLINUX): + mkdir -p $(HEADER_DIR) + bpftool btf dump file /sys/kernel/btf/vmlinux format c > $(VMLINUX) diff --git a/socket-tracer/bpf/trace_enter_socket.c b/socket-tracer/bpf/trace_enter_socket.c new file mode 100644 index 0000000..bd04397 --- /dev/null +++ b/socket-tracer/bpf/trace_enter_socket.c @@ -0,0 +1,69 @@ +//go:build ignore + +#include "vmlinux.h" +#include + +#define TASK_COMM_LEN 16 + +char __license[] SEC("license") = "Dual MIT/GPL"; + +struct bpf_map_def SEC("maps") event_rb = { + .type = BPF_MAP_TYPE_RINGBUF, + .max_entries = 256 * 1024, +}; + +struct bpf_map_def SEC("maps") target_family = { + .type = BPF_MAP_TYPE_ARRAY, + .key_size = sizeof(u32), + .value_size = sizeof(u32), + .max_entries = 1, +}; + +// ref /sys/kernel/debug/tracing/events/syscalls/sys_enter_socket/format +struct enter_socket_ctx { + /* The first 8 bytes is not allowed to read */ + unsigned long pad; + + int __syscall_nr; + u64 family; + u64 type; + u64 protocol; +}; + +struct event { + u32 pid; + u8 comm[TASK_COMM_LEN]; +}; + +// Force emitting struct event into the ELF. +const struct event *unused __attribute__((unused)); + +SEC("tracepoint/syscalls/sys_enter_socket") +int trace_enter_socket(struct enter_socket_ctx *ctx) { + int zero = 0; + + int *valp; + valp = bpf_map_lookup_elem(&target_family, &zero); + if (!valp) { + return 0; + } + + if (ctx->family != *valp) { + return 0; + } + + struct event *e; + e = bpf_ringbuf_reserve(&event_rb, sizeof(struct event), 0); + if (!e) { + return 0; + } + u64 res; + u64 id = bpf_get_current_pid_tgid(); + + e->pid = (u32)(id >> 32); + res = bpf_get_current_comm(&e->comm, sizeof(e->comm)); + + bpf_ringbuf_submit(e, 0); + + return 0; +} diff --git a/socket-tracer/cmd/root.go b/socket-tracer/cmd/root.go new file mode 100644 index 0000000..55fd06e --- /dev/null +++ b/socket-tracer/cmd/root.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "os" + + "github.com/cybozu-go/neco-bpftools/socket-tracer/pkg/bpf" + + "github.com/spf13/cobra" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "socket-tracer", + Short: "trace socket syscall", + RunE: func(cmd *cobra.Command, args []string) error { + familyStr, err := cmd.Flags().GetString("family") + if err != nil { + return err + } + family, err := bpf.ParseFamily(familyStr) + if err != nil { + return err + } + + return bpf.TraceEnterSocket(family) + }, +} + +func init() { + rootCmd.Flags().StringP("family", "f", "", "Family value for socket system call. See `man socket`. Accepts AF_* or number") +} + +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} diff --git a/socket-tracer/main.go b/socket-tracer/main.go new file mode 100644 index 0000000..788f053 --- /dev/null +++ b/socket-tracer/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/cybozu-go/neco-bpftools/socket-tracer/cmd" + +func main() { + cmd.Execute() +} diff --git a/socket-tracer/pkg/bpf/trace_enter_socket.go b/socket-tracer/pkg/bpf/trace_enter_socket.go new file mode 100644 index 0000000..905432e --- /dev/null +++ b/socket-tracer/pkg/bpf/trace_enter_socket.go @@ -0,0 +1,196 @@ +package bpf + +import ( + "bytes" + "context" + "encoding/binary" + "errors" + "fmt" + "log/slog" + "os" + "os/signal" + "strconv" + "strings" + "syscall" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/link" + "github.com/cilium/ebpf/ringbuf" + "github.com/cilium/ebpf/rlimit" + "golang.org/x/sys/unix" +) + +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -type event bpf ../../bpf/trace_enter_socket.c -g -- -I../../bpf/include + +func TraceEnterSocket(family int) error { + ctx := context.Background() + + logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) + + ctrlC := make(chan os.Signal, 1) + signal.Notify(ctrlC, os.Interrupt, syscall.SIGTERM) + + if err := rlimit.RemoveMemlock(); err != nil { + return err + } + + obj := bpfObjects{} + if err := loadBpfObjects(&obj, &ebpf.CollectionOptions{ + Programs: ebpf.ProgramOptions{ + LogLevel: ebpf.LogLevelInstruction, + LogSize: 1024 * 1024 * 256, + }, + }); err != nil { + return err + } + defer obj.Close() + + tp, err := link.Tracepoint("syscalls", "sys_enter_socket", obj.TraceEnterSocket, nil) + if err != nil { + return err + } + defer tp.Close() + + key := uint32(0) + value := uint32(family) + if err := obj.TargetFamily.Update(&key, &value, ebpf.UpdateAny); err != nil { + logger.ErrorContext(ctx, "failed to update TargetFamily", slog.Int("family", family)) + return err + } + + rb, err := ringbuf.NewReader(obj.EventRb) + if err != nil { + return err + } + defer rb.Close() + + go func() { + <-ctrlC + logger.WarnContext(ctx, "Signal received.. Close ringbuffer.") + if err := rb.Close(); err != nil { + panic(err) + } + }() + + var event bpfEvent + for { + record, err := rb.Read() + if err != nil { + if errors.Is(err, ringbuf.ErrClosed) { + logger.ErrorContext(ctx, "ringbuffer is closed. exit") + return nil + } + logger.ErrorContext(ctx, "failed to read from ringbuffer", slog.Any("error", err)) + continue + } + + if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event); err != nil { + logger.ErrorContext(ctx, "failed to parse data", slog.Any("error", err)) + continue + } + + fmt.Printf("{\"pid\": %d,\"command\": \"%s\"}\n", event.Pid, unix.ByteSliceToString(event.Comm[:])) + } +} + +func ParseFamily(family string) (int, error) { + + n, err := strconv.Atoi(family) + if err != nil { + if !strings.Contains(family, "AF_") { + return 0, fmt.Errorf("invalid family value. family must match AF_*.") + } + return parseFamily(family) + + } + return n, nil +} + +func parseFamily(family string) (int, error) { + switch family { + case "AF_ALG": + return syscall.AF_ALG, nil + case "AF_APPLETAL": + return syscall.AF_APPLETALK, nil + case "AF_ASH": + return syscall.AF_ASH, nil + case "AF_ATMPVC": + return syscall.AF_ATMPVC, nil + case "AF_ATMSVC": + return syscall.AF_ATMSVC, nil + case "AF_AX25": + return syscall.AF_AX25, nil + case "AF_BLUETOOTH": + return syscall.AF_BLUETOOTH, nil + case "AF_BRIDGE": + return syscall.AF_BRIDGE, nil + case "AF_CAIF": + return syscall.AF_CAIF, nil + case "AF_CAN": + return syscall.AF_CAN, nil + case "AF_DECnet": + return syscall.AF_DECnet, nil + case "AF_ECONET": + return syscall.AF_ECONET, nil + case "AF_FILE": + return syscall.AF_FILE, nil + case "AF_IEEE80215": + return syscall.AF_IEEE802154, nil + case "AF_INET": + return syscall.AF_INET, nil + case "AF_INET6": + return syscall.AF_INET6, nil + case "AF_IPX": + return syscall.AF_IPX, nil + case "AF_IRDA": + return syscall.AF_IRDA, nil + case "AF_ISDN": + return syscall.AF_ISDN, nil + case "AF_IUCV": + return syscall.AF_IUCV, nil + case "AF_KEY": + return syscall.AF_KEY, nil + case "AF_LLC": + return syscall.AF_LLC, nil + case "AF_LOCAL": + return syscall.AF_LOCAL, nil + case "AF_MAX": + return syscall.AF_MAX, nil + case "AF_NETBEUI": + return syscall.AF_NETBEUI, nil + case "AF_NETLINK": + return syscall.AF_NETLINK, nil + case "AF_NETROM": + return syscall.AF_NETROM, nil + case "AF_PACKET": + return syscall.AF_PACKET, nil + case "AF_PHONET": + return syscall.AF_PHONET, nil + case "AF_PPPOX": + return syscall.AF_PPPOX, nil + case "AF_RDS": + return syscall.AF_RDS, nil + case "AF_ROSE": + return syscall.AF_ROSE, nil + case "AF_ROUTE": + return syscall.AF_ROUTE, nil + case "AF_RXRPC": + return syscall.AF_RXRPC, nil + case "AF_SECURITY": + return syscall.AF_SECURITY, nil + case "AF_SNA": + return syscall.AF_SNA, nil + case "AF_TIPC": + return syscall.AF_TIPC, nil + case "AF_UNIX": + return syscall.AF_UNIX, nil + case "AF_UNSPEC": + return syscall.AF_UNSPEC, nil + case "AF_WANPIPE": + return syscall.AF_WANPIPE, nil + case "AF_X25": + return syscall.AF_X25, nil + default: + return 0, fmt.Errorf("invalid family %s", family) + } +}