| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- package ebpftracer
- import (
- "bytes"
- "fmt"
- "github.com/containerd/cgroups"
- "github.com/opencontainers/runtime-spec/specs-go"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "golang.org/x/sys/unix"
- "net"
- "os"
- "os/exec"
- "path"
- "strconv"
- "strings"
- "syscall"
- "testing"
- "time"
- )
- func skipIfNotRoot(t *testing.T) {
- if os.Getuid() != 0 {
- t.SkipNow()
- }
- }
- func TestProcessEvents(t *testing.T) {
- skipIfNotRoot(t)
- src := `
- package main
-
- import (
- "bytes"
- "os"
- "strconv"
- "time"
- )
-
- func main() {
- mb, _ := strconv.Atoi(os.Args[1])
- sleep, _ := time.ParseDuration(os.Args[2])
- bytes.Repeat([]byte("x"), mb*1024*1024)
- time.Sleep(sleep)
- }
- `
- program := path.Join(t.TempDir(), "program")
- require.NoError(t, os.WriteFile(program+".go", []byte(src), 0644))
- require.NoError(t, exec.Command("go", "build", "-o", program, program+".go").Run())
- getEvent, stop := runTracer(t, false)
- defer stop()
- for {
- if e := getEvent(); e == nil {
- break
- }
- }
- p1 := exec.Command(program, "600", "10s")
- require.NoError(t, p1.Start())
- time.Sleep(time.Second)
- assert.Equal(t, Event{Type: EventTypeProcessStart, Pid: uint32(p1.Process.Pid)}, *getEvent())
- // p1 should be killed by the OOM killer, because VM have only 1 GB of memory total
- p2 := exec.Command(program, "400", "1s")
- require.NoError(t, p2.Run())
- assert.Equal(t, Event{Type: EventTypeProcessStart, Pid: uint32(p2.Process.Pid)}, *getEvent())
- require.Error(t, p1.Wait())
- assert.Equal(t, Event{Type: EventTypeProcessExit, Reason: EventReasonOOMKill, Pid: uint32(p1.Process.Pid)}, *getEvent())
- assert.Equal(t, Event{Type: EventTypeProcessExit, Pid: uint32(p2.Process.Pid)}, *getEvent())
- var limit int64 = 200 * 1024 * 1024
- control, err := cgroups.New(cgroups.V1, cgroups.StaticPath("/program"), &specs.LinuxResources{
- Memory: &specs.LinuxMemory{Limit: &limit},
- })
- require.NoError(t, err)
- defer control.Delete()
- // p3 should be killed by the OOM killer, because 300 MB > 200 MB cgroup limit
- p3 := exec.Command("cgexec", "-g", "memory:program", program, "300", "1s")
- require.Error(t, p3.Run())
- assert.Equal(t, Event{Type: EventTypeProcessStart, Pid: uint32(p3.Process.Pid)}, *getEvent())
- assert.Equal(t, Event{Type: EventTypeProcessExit, Reason: EventReasonOOMKill, Pid: uint32(p3.Process.Pid)}, *getEvent())
- for {
- e := getEvent()
- if e == nil {
- break
- }
- t.Errorf("unexpected event %+v", e)
- }
- }
- func TestTcpEvents(t *testing.T) {
- skipIfNotRoot(t)
- l, err := net.Listen("tcp", "127.0.0.1:8080")
- require.NoError(t, err)
- listenAddr := l.Addr().String()
- remoteAddr := "127.0.0.1:8080"
- c, err := net.DialTimeout("tcp", remoteAddr, 100*time.Millisecond)
- require.NoError(t, err)
- localAddr := c.LocalAddr().String()
- time.Sleep(100 * time.Millisecond)
- getEvent, stop := runTracer(t, false)
- defer stop()
- pid := uint32(os.Getpid())
- is := func(e *Event, typ EventType, sAddr string, dAddr string, pid uint32) bool {
- if e == nil {
- return false
- }
- sa := e.SrcAddr.String()
- if strings.HasSuffix(sAddr, ":") {
- sa = fmt.Sprintf("%s:", e.SrcAddr.IP())
- }
- da := e.DstAddr.String()
- return e.Type == typ && e.Pid == pid && sa == sAddr && da == dAddr
- }
- listenFound := false
- connectFound := false
- for {
- e := getEvent()
- if e == nil {
- break
- }
- if is(e, EventTypeListenOpen, listenAddr, "0.0.0.0:0", pid) {
- listenFound = true
- }
- if is(e, EventTypeConnectionOpen, localAddr, remoteAddr, pid) {
- connectFound = true
- }
- }
- if !listenFound {
- t.Errorf("expected %s on %s", EventTypeListenOpen, l.Addr())
- }
- if !connectFound {
- t.Errorf("expected %s to %s", EventTypeConnectionOpen, l.Addr())
- }
- nextIs := func(typ EventType, sAddr string, dAddr string, pid uint32) {
- e := getEvent()
- if !is(e, typ, sAddr, dAddr, pid) {
- expected := fmt.Sprintf("%-20s %6d: %s -> %s", typ, pid, sAddr, dAddr)
- actual := "nil"
- if e != nil {
- actual = fmt.Sprintf("%-20s %6d: %s -> %s", e.Type, e.Pid, e.SrcAddr, e.DstAddr)
- }
- assert.Equal(t, expected, actual)
- }
- }
- require.NoError(t, c.Close())
- nextIs(EventTypeConnectionClose, localAddr, listenAddr, 0)
- nextIs(EventTypeConnectionClose, listenAddr, localAddr, 0)
- require.NoError(t, l.Close())
- nextIs(EventTypeListenClose, listenAddr, "0.0.0.0:0", pid)
- c, err = net.DialTimeout("tcp", listenAddr, 100*time.Millisecond)
- require.Error(t, err)
- nextIs(EventTypeConnectionError, "127.0.0.1:", listenAddr, pid)
- l, err = net.Listen("tcp4", ":8080")
- require.NoError(t, err)
- listenAddr = l.Addr().String()
- nextIs(EventTypeListenOpen, listenAddr, "0.0.0.0:0", pid)
- c, err = net.DialTimeout("tcp", remoteAddr, 100*time.Millisecond)
- require.NoError(t, err)
- localAddr = c.LocalAddr().String()
- nextIs(EventTypeConnectionOpen, localAddr, remoteAddr, pid)
- require.NoError(t, exec.Command("tc", "qdisc", "add", "dev", "lo", "root", "netem", "loss", "100%").Run())
- getEvent()
- getEvent()
- c.Write([]byte("hello"))
- nextIs(EventTypeTCPRetransmit, localAddr, remoteAddr, 0)
- require.NoError(t, exec.Command("tc", "qdisc", "del", "dev", "lo", "root", "netem").Run())
- getEvent()
- getEvent()
- func() {
- timer := time.NewTimer(time.Second)
- for {
- select {
- case <-timer.C:
- return
- default:
- e := getEvent()
- require.True(t, e == nil || e.Type == EventTypeTCPRetransmit)
- }
- }
- }()
- require.NoError(t, c.Close())
- nextIs(EventTypeConnectionClose, localAddr, remoteAddr, 0)
- nextIs(EventTypeConnectionClose, remoteAddr, localAddr, 0)
- require.NoError(t, l.Close())
- nextIs(EventTypeListenClose, listenAddr, "0.0.0.0:0", pid)
- for {
- e := getEvent()
- if e == nil {
- break
- }
- t.Errorf("unexpected event %+v", e)
- }
- }
- func TestFileEvents(t *testing.T) {
- skipIfNotRoot(t)
- src := `
- package main
-
- import (
- "os"
- "strconv"
- "syscall"
- "unsafe"
- "time"
- )
-
- func main() {
- call, _ := strconv.Atoi(os.Args[1])
- path := os.Args[2]
- flags, _ := strconv.Atoi(os.Args[3])
- filename, _ := syscall.BytePtrFromString(path)
- var err syscall.Errno
- switch call {
- case syscall.SYS_OPEN:
- _, _, err = syscall.Syscall6(syscall.SYS_OPEN, uintptr(unsafe.Pointer(filename)), uintptr(flags), 0, 0, 0, 0)
- case syscall.SYS_OPENAT:
- AT_FDCWD := -100
- _, _, err = syscall.Syscall6(syscall.SYS_OPENAT, uintptr(AT_FDCWD), uintptr(unsafe.Pointer(filename)), uintptr(flags), 0, 0, 0)
- }
- time.Sleep(100 * time.Millisecond)
- os.Exit(int(err))
- }
- `
- require.NoError(t, os.Chdir(t.TempDir()))
- require.NoError(t, os.WriteFile("program.go", []byte(src), 0644))
- out, err := exec.Command("go", "build", "-o", "program", "program.go").CombinedOutput()
- require.Equal(t, "", string(out))
- require.NoError(t, err)
- getEvent, stop := runTracer(t, false)
- defer stop()
- for {
- if e := getEvent(); e == nil {
- break
- }
- }
- for _, call := range []int{syscall.SYS_OPEN, syscall.SYS_OPENAT} {
- run := func(file string, flag int) (uint32, error) {
- p := exec.Command("./program", strconv.Itoa(call), file, strconv.Itoa(flag))
- err := p.Run()
- return uint32(p.Process.Pid), err
- }
- pid, err := run("program.go", os.O_RDONLY)
- assert.NoError(t, err)
- assert.Equal(t, Event{Type: EventTypeProcessStart, Pid: pid}, *getEvent())
- assert.Equal(t, Event{Type: EventTypeProcessExit, Pid: pid}, *getEvent())
- pid, err = run("program.go", os.O_WRONLY)
- assert.NoError(t, err)
- assert.Equal(t, Event{Type: EventTypeProcessStart, Pid: pid}, *getEvent())
- assert.Equal(t, Event{Type: EventTypeFileOpen, Pid: pid, Fd: 3}, *getEvent())
- assert.Equal(t, Event{Type: EventTypeProcessExit, Pid: pid}, *getEvent())
- pid, err = run("program.go", os.O_RDWR)
- assert.NoError(t, err)
- assert.Equal(t, Event{Type: EventTypeProcessStart, Pid: pid}, *getEvent())
- assert.Equal(t, Event{Type: EventTypeFileOpen, Pid: pid, Fd: 3}, *getEvent())
- assert.Equal(t, Event{Type: EventTypeProcessExit, Pid: pid}, *getEvent())
- // open error: text file busy
- pid, err = run("program", os.O_RDWR)
- assert.Error(t, err)
- assert.Equal(t, Event{Type: EventTypeProcessStart, Pid: pid}, *getEvent())
- assert.Equal(t, Event{Type: EventTypeProcessExit, Pid: pid}, *getEvent())
- // ignoring /proc/*, /dev/*, /sys/*
- for _, f := range []string{"/proc/sys/fs/file-max", "/dev/null", "/sys/kernel/profiling"} {
- pid, err = run(f, os.O_RDWR)
- assert.NoError(t, err)
- assert.Equal(t, Event{Type: EventTypeProcessStart, Pid: pid}, *getEvent())
- assert.Equal(t, Event{Type: EventTypeProcessExit, Pid: pid}, *getEvent())
- }
- for {
- e := getEvent()
- if e == nil {
- break
- }
- t.Errorf("unexpected event %+v", e)
- }
- }
- }
- func runTracer(t *testing.T, verbose bool) (func() *Event, func()) {
- events := make(chan Event, 1000)
- done := make(chan bool, 1)
- var uname unix.Utsname
- assert.NoError(t, unix.Uname(&uname))
- go func() {
- tt, err := NewTracer(events, string(bytes.Split(uname.Release[:], []byte{0})[0]))
- require.NoError(t, err)
- <-done
- tt.Close()
- }()
- stop := func() {
- done <- true
- }
- get := func() *Event {
- select {
- case e := <-events:
- if verbose {
- fmt.Printf("%+v\n", e)
- }
- return &e
- case <-time.NewTimer(time.Second).C:
- return nil
- }
- }
- return get, stop
- }
|