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