├── .prettierignore ├── demo.gif ├── gopher.png ├── cgroup_x86_bpfel.o ├── cgroup_arm64_bpfel.o ├── counter_arm64_bpfel.o ├── counter_x86_bpfel.o ├── go.mod ├── .gitignore ├── .golangci.yml ├── LICENSE ├── init.go ├── .goreleaser.yml ├── gen.go ├── bpf ├── counter.h ├── cgroup.h ├── cgroup.bpf.c └── counter.bpf.c ├── types.go ├── Taskfile.yml ├── helpers.go ├── cgroup_arm64_bpfel.go ├── cgroup_x86_bpfel.go ├── flags.go ├── main.go ├── counter_arm64_bpfel.go ├── counter_x86_bpfel.go ├── map.go ├── go.sum ├── cgroup.go ├── output.go ├── .clang-format ├── tui.go ├── README.md └── probe.go /.prettierignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkorunic/pktstat-bpf/HEAD/demo.gif -------------------------------------------------------------------------------- /gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkorunic/pktstat-bpf/HEAD/gopher.png -------------------------------------------------------------------------------- /cgroup_x86_bpfel.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkorunic/pktstat-bpf/HEAD/cgroup_x86_bpfel.o -------------------------------------------------------------------------------- /cgroup_arm64_bpfel.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkorunic/pktstat-bpf/HEAD/cgroup_arm64_bpfel.o -------------------------------------------------------------------------------- /counter_arm64_bpfel.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkorunic/pktstat-bpf/HEAD/counter_arm64_bpfel.o -------------------------------------------------------------------------------- /counter_x86_bpfel.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkorunic/pktstat-bpf/HEAD/counter_x86_bpfel.o -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dkorunic/pktstat-bpf 2 | 3 | go 1.25 4 | 5 | require ( 6 | github.com/cilium/ebpf v0.20.0 7 | github.com/gdamore/tcell/v2 v2.13.2 8 | github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b 9 | github.com/peterbourgon/ff/v4 v4.0.0-beta.1 10 | github.com/rivo/tview v0.42.0 11 | ) 12 | 13 | require ( 14 | github.com/gdamore/encoding v1.0.1 // indirect 15 | github.com/lucasb-eyer/go-colorful v1.3.0 // indirect 16 | github.com/rivo/uniseg v0.4.7 // indirect 17 | golang.org/x/sys v0.39.0 // indirect 18 | golang.org/x/term v0.38.0 // indirect 19 | golang.org/x/text v0.32.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Go ### 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | pktstat-bpf 9 | dist/ 10 | vendor/ 11 | 12 | # Test binary, build with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | ### Intellij ### 19 | # User-specific stuff: 20 | .idea/ 21 | 22 | # CMake 23 | cmake-build-debug/ 24 | 25 | ## File-based project format: 26 | *.iws 27 | 28 | # IntelliJ 29 | /out/ 30 | 31 | ### VisualStudioCode ### 32 | .vscode/ 33 | 34 | ### Tokens ### 35 | credentials.json 36 | token.json 37 | 38 | ### Shell wrappers ### 39 | *.sh 40 | 41 | ## Usual macOS cruft ### 42 | .DS_Store 43 | 44 | ## Editor config 45 | .editorconfig 46 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: all 4 | disable: 5 | - cyclop 6 | - depguard 7 | - dupl 8 | - exhaustruct 9 | - forbidigo 10 | - funlen 11 | - gochecknoglobals 12 | - gocognit 13 | - lll 14 | - varnamelen 15 | - wrapcheck 16 | exclusions: 17 | generated: lax 18 | presets: 19 | - comments 20 | - common-false-positives 21 | - legacy 22 | - std-error-handling 23 | paths: 24 | - third_party$ 25 | - builtin$ 26 | - examples$ 27 | formatters: 28 | enable: 29 | - gci 30 | - gofmt 31 | - gofumpt 32 | - goimports 33 | exclusions: 34 | generated: lax 35 | paths: 36 | - third_party$ 37 | - builtin$ 38 | - examples$ 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2024 Dinko Korunic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2024 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package main 23 | 24 | import "strings" 25 | 26 | var ( 27 | GitTag = "" 28 | GitCommit = "" 29 | GitDirty = "" 30 | BuildTime = "" 31 | ) 32 | 33 | //nolint:gochecknoinits 34 | func init() { 35 | GitTag = strings.TrimSpace(GitTag) 36 | GitCommit = strings.TrimSpace(GitCommit) 37 | GitDirty = strings.TrimSpace(GitDirty) 38 | BuildTime = strings.TrimSpace(BuildTime) 39 | } 40 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | builds: 5 | - flags: 6 | - -trimpath 7 | env: 8 | - CGO_ENABLED=0 9 | ldflags: | 10 | -s -w -extldflags '-static' -X "main.GitTag={{.Tag}}" -X "main.GitCommit={{.ShortCommit}}" -X "main.GitDirty= " -X "main.BuildTime={{.Date}}" 11 | goos: 12 | - linux 13 | goarch: 14 | - amd64 15 | - arm64 16 | universal_binaries: 17 | - replace: true 18 | changelog: 19 | sort: asc 20 | archives: 21 | - name_template: >- 22 | {{ .ProjectName }}_ 23 | {{- title .Os }}_ 24 | {{- if eq .Arch "amd64" }}x86_64 25 | {{- else if eq .Arch "386" }}i386 26 | {{- else }}{{ .Arch }}{{ end }} 27 | {{- if .Arm }}v{{ .Arm }}{{ end }} 28 | format_overrides: 29 | - goos: windows 30 | format: zip 31 | files: 32 | - README.md 33 | - LICENSE 34 | - src: dist/CHANGELOG.md 35 | dst: "" 36 | strip_parent: true 37 | checksum: 38 | name_template: "checksums.txt" 39 | snapshot: 40 | name_template: "{{ .Tag }}-next" 41 | nfpms: 42 | - package_name: pktstat-bpf 43 | vendor: Dinko Korunic 44 | homepage: https://github.com/dkorunic/pktstat-bpf 45 | maintainer: Dinko Korunic 46 | description: eBPF-based Ethernet interface traffic monitor and reporting tool 47 | license: MIT 48 | formats: 49 | - apk 50 | - deb 51 | - rpm 52 | - termux.deb 53 | - archlinux 54 | bindir: /usr/bin 55 | section: net 56 | priority: optional 57 | deb: 58 | lintian_overrides: 59 | - statically-linked-binary 60 | - changelog-file-missing-in-native-package 61 | -------------------------------------------------------------------------------- /gen.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2024 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package main 23 | 24 | //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target amd64 counter ./bpf/counter.bpf.c -- -I./contrib/amd64 25 | //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target arm64 counter ./bpf/counter.bpf.c -- -I./contrib/arm64 26 | 27 | //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target amd64 cgroup ./bpf/cgroup.bpf.c -- -I./contrib/amd64 28 | //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target arm64 cgroup ./bpf/cgroup.bpf.c -- -I./contrib/arm64 29 | -------------------------------------------------------------------------------- /bpf/counter.h: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2024 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | //go:build ignore 23 | 24 | #define s6_addr in6_u.u6_addr8 25 | #define s6_addr16 in6_u.u6_addr16 26 | #define s6_addr32 in6_u.u6_addr32 27 | #define inet_num sk.__sk_common.skc_num 28 | 29 | #define ETH_P_IP 0x0800 30 | #define ETH_P_IPV6 0x86DD 31 | #define TC_ACT_UNSPEC -1 32 | #define AF_INET 2 33 | #define AF_INET6 10 34 | #define IPPROTO_ICMPV6 58 35 | 36 | #define OK 1 37 | #define NOK 0 38 | #define ALLOW_PKT 1 39 | #define ALLOW_SK 1 40 | 41 | #define TASK_COMM_LEN 16 42 | #define MAX_ENTRIES 131072 43 | -------------------------------------------------------------------------------- /bpf/cgroup.h: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2025 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | //go:build ignore 23 | 24 | #define MAX_ENTRIES 1024 25 | #define PATH_MAX 4096 26 | 27 | union kernfs_node_id { 28 | struct { 29 | u32 ino; 30 | u32 generation; 31 | }; 32 | u64 id; 33 | }; 34 | 35 | struct kernfs_node___older_v55 { 36 | const char *name; 37 | union kernfs_node_id id; 38 | }; 39 | 40 | struct kernfs_node___rh8 { 41 | const char *name; 42 | union { 43 | u64 id; 44 | struct { 45 | union kernfs_node_id id; 46 | } rh_kabi_hidden_172; 47 | union {}; 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2024 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "net/netip" 26 | 27 | "github.com/cilium/ebpf" 28 | ) 29 | 30 | type statEntry struct { 31 | SrcIP netip.Addr `json:"srcIp"` 32 | DstIP netip.Addr `json:"dstIp"` 33 | Proto string `json:"proto"` 34 | Comm string `json:"comm,omitempty"` 35 | CGroup string `json:"cgroup,omitempty"` 36 | Bytes uint64 `json:"bytes"` 37 | Packets uint64 `json:"packets"` 38 | Bitrate float64 `json:"bitrate"` 39 | Pid int32 `json:"pid,omitempty"` 40 | SrcPort uint16 `json:"srcPort"` 41 | DstPort uint16 `json:"dstPort"` 42 | } 43 | 44 | type kprobeHook struct { 45 | prog *ebpf.Program 46 | kprobe string 47 | } 48 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | vars: 4 | TARGET: pktstat-bpf 5 | GIT_LAST_TAG: 6 | sh: git describe --abbrev=0 --tags 2>/dev/null || echo latest 7 | GIT_HEAD_COMMIT: 8 | sh: git rev-parse --short HEAD 2>/dev/null || echo unknown 9 | GIT_TAG_COMMIT: 10 | sh: git rev-parse --short {{.GIT_LAST_TAG}} 2>/dev/null || echo unknown 11 | GIT_MODIFIED1: 12 | sh: git diff {{.GIT_HEAD_COMMIT}} {{.GIT_TAG_COMMIT}} --quiet 2>/dev/null || echo .dev 13 | GIT_MODIFIED2: 14 | sh: git diff --quiet 2>/dev/null || echo .dirty 15 | GIT_MODIFIED: 16 | sh: echo "{{.GIT_MODIFIED1}}{{.GIT_MODIFIED2}}" 17 | BUILD_DATE: 18 | sh: date -u '+%Y-%m-%dT%H:%M:%SZ' 19 | 20 | env: 21 | CGO_ENABLED: 0 22 | 23 | tasks: 24 | default: 25 | cmds: 26 | - task: update 27 | - task: build 28 | 29 | update: 30 | cmds: 31 | - go get -u 32 | - go mod tidy 33 | 34 | update-major: 35 | cmds: 36 | - gomajor list 37 | 38 | update-tools: 39 | cmds: 40 | - go install github.com/daixiang0/gci@latest 41 | - go install mvdan.cc/gofumpt@latest 42 | - go install github.com/dkorunic/betteralign/cmd/betteralign@latest 43 | 44 | fmt: 45 | cmds: 46 | - gci write . 47 | - gofumpt -l -w . 48 | - betteralign -apply ./... 49 | 50 | fmt-bpf: 51 | cmds: 52 | - clang-format -i ./bpf/* 53 | - gsed -i 's,[[:space:]]*go:build,go:build,g' ./bpf/* 54 | 55 | modernize: 56 | cmds: 57 | - go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -fix -test ./... 58 | 59 | generate: 60 | cmds: 61 | - go generate 62 | 63 | build: 64 | cmds: 65 | - task: fmt 66 | - go build -trimpath -pgo=auto -ldflags="-s -w -X main.GitTag={{.GIT_LAST_TAG}} -X main.GitCommit={{.GIT_HEAD_COMMIT}} -X main.GitDirty={{.GIT_MODIFIED}} -X main.BuildTime={{.BUILD_DATE}}" -o {{.TARGET}} 67 | 68 | build-debug: 69 | env: 70 | CGO_ENABLED: 1 71 | cmds: 72 | - task: update 73 | - task: fmt 74 | - go build -ldflags="-X main.GitTag={{.GIT_LAST_TAG}} -X main.GitCommit={{.GIT_HEAD_COMMIT}} -X main.GitDirty={{.GIT_MODIFIED}} -X main.BuildTime={{.BUILD_DATE}}" -race -o {{.TARGET}} 75 | 76 | lint: 77 | cmds: 78 | - task: fmt 79 | - golangci-lint run --timeout 5m 80 | 81 | lint-nil: 82 | cmds: 83 | - task: fmt 84 | - nilaway ./... 85 | 86 | tools: 87 | cmds: 88 | - task: gofumpt 89 | - task: gci 90 | - task: betteralign 91 | 92 | release: 93 | cmds: 94 | - goreleaser release --clean -p 4 95 | 96 | gci: 97 | internal: true 98 | status: 99 | - which gci 100 | cmds: 101 | - go install github.com/daixiang0/gci@latest 102 | 103 | gofumpt: 104 | internal: true 105 | status: 106 | - which gofumpt 107 | cmds: 108 | - go install mvdan.cc/gofumpt@latest 109 | 110 | betteralign: 111 | internal: true 112 | status: 113 | - which betteralign 114 | cmds: 115 | - go install github.com/dkorunic/betteralign/cmd/betteralign@latest 116 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2024 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "net" 26 | "net/netip" 27 | "strings" 28 | ) 29 | 30 | var protoNumbers = map[uint8]string{ 31 | 0: "IPv4", 32 | 1: "ICMPv4", 33 | 2: "IGMP", 34 | 3: "GGP", 35 | 4: "IP-ENCAP", 36 | 5: "ST", 37 | 6: "TCP", 38 | 8: "EGP", 39 | 9: "IGP", 40 | 12: "PUP", 41 | 17: "UDP", 42 | 20: "HMP", 43 | 22: "XNS-IDP", 44 | 27: "RDP", 45 | 29: "ISO-TP4", 46 | 33: "DCCP", 47 | 36: "XTP", 48 | 37: "DDP", 49 | 38: "IDPR-CMTP", 50 | 41: "IPv6", 51 | 43: "IPv6-Route", 52 | 44: "IPv6-Frag", 53 | 45: "IDRP", 54 | 46: "RSVP", 55 | 47: "GRE", 56 | 50: "IPSEC-ESP", 57 | 51: "IPSEC-AH", 58 | 57: "SKIP", 59 | 58: "IPv6-ICMP", 60 | 59: "IPv6-NoNxt", 61 | 60: "IPv6-Opts", 62 | 73: "RSPF", 63 | 81: "VMTP", 64 | 88: "EIGRP", 65 | 89: "OSPFIGP", 66 | 93: "AX.25", 67 | 94: "IPIP", 68 | 97: "ETHERIP", 69 | 98: "ENCAP", 70 | 99: "Tailscale", // TSMP 71 | 103: "PIM", 72 | 108: "IPCOMP", 73 | 112: "VRRP", 74 | 115: "L2TP", 75 | 124: "ISIS", 76 | 132: "SCTP", 77 | 133: "FC", 78 | 135: "Mobility-Header", 79 | 136: "UDPLite", 80 | 137: "MPLS-in-IP", 81 | 138: "MANET", 82 | 139: "HIP", 83 | 140: "Shim6", 84 | 141: "WESP", 85 | 142: "ROHC", 86 | 143: "Ethernet", 87 | 255: "Fragment", 88 | } 89 | 90 | // protoToString converts a protocol number to its corresponding name. 91 | // 92 | // p: the protocol number to convert. 93 | // string: the name of the protocol. 94 | func protoToString(p uint8) string { 95 | if v, ok := protoNumbers[p]; ok { 96 | return v 97 | } 98 | 99 | return "Unknown" 100 | } 101 | 102 | // bytesToAddr converts a 16-byte address to a netip.Addr. 103 | // 104 | // It takes an addr parameter of type [16]byte and returns a netip.Addr. 105 | func bytesToAddr(addr [16]byte) netip.Addr { 106 | return netip.AddrFrom16(addr).Unmap() 107 | } 108 | 109 | // findFirstEtherIface returns the name of the first non-loopback, up Ethernet interface. 110 | // 111 | // It iterates over all network interfaces and checks if each interface is up and not a loopback interface. 112 | // If an interface meets these criteria, its name is returned. If no suitable interface is found, the default 113 | // interface name is returned. 114 | // 115 | // Returns: 116 | // 117 | // string: The name of the first non-loopback, up Ethernet interface. 118 | func findFirstEtherIface() string { 119 | i, err := net.Interfaces() 120 | if err != nil { 121 | return defaultIface 122 | } 123 | 124 | for _, f := range i { 125 | if (f.Flags&net.FlagUp == 0) || (f.Flags&net.FlagLoopback) != 0 { 126 | continue 127 | } 128 | 129 | if strings.Contains(f.Name, "docker") { 130 | continue 131 | } 132 | 133 | return f.Name 134 | } 135 | 136 | return defaultIface 137 | } 138 | -------------------------------------------------------------------------------- /cgroup_arm64_bpfel.go: -------------------------------------------------------------------------------- 1 | // Code generated by bpf2go; DO NOT EDIT. 2 | //go:build arm64 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | _ "embed" 9 | "fmt" 10 | "io" 11 | "structs" 12 | 13 | "github.com/cilium/ebpf" 14 | ) 15 | 16 | type cgroupCgroupevent struct { 17 | _ structs.HostLayout 18 | Path [4096]int8 19 | Cgroupid uint64 20 | } 21 | 22 | // loadCgroup returns the embedded CollectionSpec for cgroup. 23 | func loadCgroup() (*ebpf.CollectionSpec, error) { 24 | reader := bytes.NewReader(_CgroupBytes) 25 | spec, err := ebpf.LoadCollectionSpecFromReader(reader) 26 | if err != nil { 27 | return nil, fmt.Errorf("can't load cgroup: %w", err) 28 | } 29 | 30 | return spec, err 31 | } 32 | 33 | // loadCgroupObjects loads cgroup and converts it into a struct. 34 | // 35 | // The following types are suitable as obj argument: 36 | // 37 | // *cgroupObjects 38 | // *cgroupPrograms 39 | // *cgroupMaps 40 | // 41 | // See ebpf.CollectionSpec.LoadAndAssign documentation for details. 42 | func loadCgroupObjects(obj interface{}, opts *ebpf.CollectionOptions) error { 43 | spec, err := loadCgroup() 44 | if err != nil { 45 | return err 46 | } 47 | 48 | return spec.LoadAndAssign(obj, opts) 49 | } 50 | 51 | // cgroupSpecs contains maps and programs before they are loaded into the kernel. 52 | // 53 | // It can be passed ebpf.CollectionSpec.Assign. 54 | type cgroupSpecs struct { 55 | cgroupProgramSpecs 56 | cgroupMapSpecs 57 | cgroupVariableSpecs 58 | } 59 | 60 | // cgroupProgramSpecs contains programs before they are loaded into the kernel. 61 | // 62 | // It can be passed ebpf.CollectionSpec.Assign. 63 | type cgroupProgramSpecs struct { 64 | TraceCgroupMkdir *ebpf.ProgramSpec `ebpf:"trace_cgroup_mkdir"` 65 | } 66 | 67 | // cgroupMapSpecs contains maps before they are loaded into the kernel. 68 | // 69 | // It can be passed ebpf.CollectionSpec.Assign. 70 | type cgroupMapSpecs struct { 71 | CgroupEvent *ebpf.MapSpec `ebpf:"cgroup_event"` 72 | PerfCgroupEvent *ebpf.MapSpec `ebpf:"perf_cgroup_event"` 73 | } 74 | 75 | // cgroupVariableSpecs contains global variables before they are loaded into the kernel. 76 | // 77 | // It can be passed ebpf.CollectionSpec.Assign. 78 | type cgroupVariableSpecs struct { 79 | } 80 | 81 | // cgroupObjects contains all objects after they have been loaded into the kernel. 82 | // 83 | // It can be passed to loadCgroupObjects or ebpf.CollectionSpec.LoadAndAssign. 84 | type cgroupObjects struct { 85 | cgroupPrograms 86 | cgroupMaps 87 | cgroupVariables 88 | } 89 | 90 | func (o *cgroupObjects) Close() error { 91 | return _CgroupClose( 92 | &o.cgroupPrograms, 93 | &o.cgroupMaps, 94 | ) 95 | } 96 | 97 | // cgroupMaps contains all maps after they have been loaded into the kernel. 98 | // 99 | // It can be passed to loadCgroupObjects or ebpf.CollectionSpec.LoadAndAssign. 100 | type cgroupMaps struct { 101 | CgroupEvent *ebpf.Map `ebpf:"cgroup_event"` 102 | PerfCgroupEvent *ebpf.Map `ebpf:"perf_cgroup_event"` 103 | } 104 | 105 | func (m *cgroupMaps) Close() error { 106 | return _CgroupClose( 107 | m.CgroupEvent, 108 | m.PerfCgroupEvent, 109 | ) 110 | } 111 | 112 | // cgroupVariables contains all global variables after they have been loaded into the kernel. 113 | // 114 | // It can be passed to loadCgroupObjects or ebpf.CollectionSpec.LoadAndAssign. 115 | type cgroupVariables struct { 116 | } 117 | 118 | // cgroupPrograms contains all programs after they have been loaded into the kernel. 119 | // 120 | // It can be passed to loadCgroupObjects or ebpf.CollectionSpec.LoadAndAssign. 121 | type cgroupPrograms struct { 122 | TraceCgroupMkdir *ebpf.Program `ebpf:"trace_cgroup_mkdir"` 123 | } 124 | 125 | func (p *cgroupPrograms) Close() error { 126 | return _CgroupClose( 127 | p.TraceCgroupMkdir, 128 | ) 129 | } 130 | 131 | func _CgroupClose(closers ...io.Closer) error { 132 | for _, closer := range closers { 133 | if err := closer.Close(); err != nil { 134 | return err 135 | } 136 | } 137 | return nil 138 | } 139 | 140 | // Do not access this directly. 141 | // 142 | //go:embed cgroup_arm64_bpfel.o 143 | var _CgroupBytes []byte 144 | -------------------------------------------------------------------------------- /cgroup_x86_bpfel.go: -------------------------------------------------------------------------------- 1 | // Code generated by bpf2go; DO NOT EDIT. 2 | //go:build 386 || amd64 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | _ "embed" 9 | "fmt" 10 | "io" 11 | "structs" 12 | 13 | "github.com/cilium/ebpf" 14 | ) 15 | 16 | type cgroupCgroupevent struct { 17 | _ structs.HostLayout 18 | Path [4096]int8 19 | Cgroupid uint64 20 | } 21 | 22 | // loadCgroup returns the embedded CollectionSpec for cgroup. 23 | func loadCgroup() (*ebpf.CollectionSpec, error) { 24 | reader := bytes.NewReader(_CgroupBytes) 25 | spec, err := ebpf.LoadCollectionSpecFromReader(reader) 26 | if err != nil { 27 | return nil, fmt.Errorf("can't load cgroup: %w", err) 28 | } 29 | 30 | return spec, err 31 | } 32 | 33 | // loadCgroupObjects loads cgroup and converts it into a struct. 34 | // 35 | // The following types are suitable as obj argument: 36 | // 37 | // *cgroupObjects 38 | // *cgroupPrograms 39 | // *cgroupMaps 40 | // 41 | // See ebpf.CollectionSpec.LoadAndAssign documentation for details. 42 | func loadCgroupObjects(obj interface{}, opts *ebpf.CollectionOptions) error { 43 | spec, err := loadCgroup() 44 | if err != nil { 45 | return err 46 | } 47 | 48 | return spec.LoadAndAssign(obj, opts) 49 | } 50 | 51 | // cgroupSpecs contains maps and programs before they are loaded into the kernel. 52 | // 53 | // It can be passed ebpf.CollectionSpec.Assign. 54 | type cgroupSpecs struct { 55 | cgroupProgramSpecs 56 | cgroupMapSpecs 57 | cgroupVariableSpecs 58 | } 59 | 60 | // cgroupProgramSpecs contains programs before they are loaded into the kernel. 61 | // 62 | // It can be passed ebpf.CollectionSpec.Assign. 63 | type cgroupProgramSpecs struct { 64 | TraceCgroupMkdir *ebpf.ProgramSpec `ebpf:"trace_cgroup_mkdir"` 65 | } 66 | 67 | // cgroupMapSpecs contains maps before they are loaded into the kernel. 68 | // 69 | // It can be passed ebpf.CollectionSpec.Assign. 70 | type cgroupMapSpecs struct { 71 | CgroupEvent *ebpf.MapSpec `ebpf:"cgroup_event"` 72 | PerfCgroupEvent *ebpf.MapSpec `ebpf:"perf_cgroup_event"` 73 | } 74 | 75 | // cgroupVariableSpecs contains global variables before they are loaded into the kernel. 76 | // 77 | // It can be passed ebpf.CollectionSpec.Assign. 78 | type cgroupVariableSpecs struct { 79 | } 80 | 81 | // cgroupObjects contains all objects after they have been loaded into the kernel. 82 | // 83 | // It can be passed to loadCgroupObjects or ebpf.CollectionSpec.LoadAndAssign. 84 | type cgroupObjects struct { 85 | cgroupPrograms 86 | cgroupMaps 87 | cgroupVariables 88 | } 89 | 90 | func (o *cgroupObjects) Close() error { 91 | return _CgroupClose( 92 | &o.cgroupPrograms, 93 | &o.cgroupMaps, 94 | ) 95 | } 96 | 97 | // cgroupMaps contains all maps after they have been loaded into the kernel. 98 | // 99 | // It can be passed to loadCgroupObjects or ebpf.CollectionSpec.LoadAndAssign. 100 | type cgroupMaps struct { 101 | CgroupEvent *ebpf.Map `ebpf:"cgroup_event"` 102 | PerfCgroupEvent *ebpf.Map `ebpf:"perf_cgroup_event"` 103 | } 104 | 105 | func (m *cgroupMaps) Close() error { 106 | return _CgroupClose( 107 | m.CgroupEvent, 108 | m.PerfCgroupEvent, 109 | ) 110 | } 111 | 112 | // cgroupVariables contains all global variables after they have been loaded into the kernel. 113 | // 114 | // It can be passed to loadCgroupObjects or ebpf.CollectionSpec.LoadAndAssign. 115 | type cgroupVariables struct { 116 | } 117 | 118 | // cgroupPrograms contains all programs after they have been loaded into the kernel. 119 | // 120 | // It can be passed to loadCgroupObjects or ebpf.CollectionSpec.LoadAndAssign. 121 | type cgroupPrograms struct { 122 | TraceCgroupMkdir *ebpf.Program `ebpf:"trace_cgroup_mkdir"` 123 | } 124 | 125 | func (p *cgroupPrograms) Close() error { 126 | return _CgroupClose( 127 | p.TraceCgroupMkdir, 128 | ) 129 | } 130 | 131 | func _CgroupClose(closers ...io.Closer) error { 132 | for _, closer := range closers { 133 | if err := closer.Close(); err != nil { 134 | return err 135 | } 136 | } 137 | return nil 138 | } 139 | 140 | // Do not access this directly. 141 | // 142 | //go:embed cgroup_x86_bpfel.o 143 | var _CgroupBytes []byte 144 | -------------------------------------------------------------------------------- /flags.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2024 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "fmt" 26 | "os" 27 | "time" 28 | 29 | "github.com/cilium/ebpf/link" 30 | "github.com/peterbourgon/ff/v4" 31 | "github.com/peterbourgon/ff/v4/ffhelp" 32 | ) 33 | 34 | const ( 35 | defaultIface = "eth0" 36 | defaultTimeout = 10 * time.Minute 37 | defaultRefresh = 1 * time.Second 38 | defaultXDPMode = "auto" 39 | XDPAttachModeNone link.XDPAttachFlags = 0 40 | ) 41 | 42 | var ( 43 | ifname, xdpMode, useCGroup *string 44 | jsonOutput, version, help, useXDP, useKProbes, enableTUI *bool 45 | timeout, refresh *time.Duration 46 | xdpAttachFlags link.XDPAttachFlags 47 | ) 48 | 49 | func parseFlags() { 50 | fs := ff.NewFlagSet("pktstat-bpf") 51 | 52 | help = fs.Bool('?', "help", "display help") 53 | jsonOutput = fs.Bool('j', "json", "if true, output in JSON format") 54 | useCGroup = fs.String('c', "cgroup", "", "the path to a CGroup V2 to measure statistics on") 55 | useXDP = fs.Bool('x', "xdp", "if true, use XDP instead of TC (this disables egress statistics)") 56 | useKProbes = fs.Bool('k', "kprobes", "if true, use KProbes for per-process TCP/UDP statistics") 57 | enableTUI = fs.Bool('g', "tui", "if true, enable TUI") 58 | 59 | version = fs.BoolLong("version", "display program version") 60 | 61 | ifname = fs.String('i', "iface", findFirstEtherIface(), "interface to read from") 62 | xdpMode = fs.StringLong("xdp_mode", defaultXDPMode, "XDP attach mode (auto, generic, native or offload; native and offload require NIC driver support)") 63 | 64 | refresh = fs.Duration('r', "refresh", defaultRefresh, "refresh interval in TUI") 65 | timeout = fs.Duration('t', "timeout", defaultTimeout, "timeout for packet capture in CLI") 66 | 67 | var err error 68 | 69 | if err = ff.Parse(fs, os.Args[1:]); err != nil { 70 | fmt.Printf("%s\n", ffhelp.Flags(fs)) 71 | fmt.Printf("Error: %v\n", err) 72 | 73 | os.Exit(1) 74 | } 75 | 76 | if *help { 77 | fmt.Printf("%s\n", ffhelp.Flags(fs)) 78 | 79 | os.Exit(0) 80 | } 81 | 82 | if *version { 83 | fmt.Printf("pktstat-bpf %v %v%v, built on: %v\n", GitTag, GitCommit, GitDirty, BuildTime) 84 | 85 | os.Exit(0) 86 | } 87 | 88 | switch *xdpMode { 89 | case "", "auto", "best": 90 | // kernel will select the best mode starting with Native and fallback to Generic 91 | xdpAttachFlags = XDPAttachModeNone 92 | case "generic": 93 | // SKB generic XDP mode 94 | xdpAttachFlags = link.XDPGenericMode 95 | case "native", "driver": 96 | // XDP support from NIC driver required 97 | xdpAttachFlags = link.XDPDriverMode 98 | case "offload", "hardware": 99 | // only for NICs with HW XDP support 100 | xdpAttachFlags = link.XDPOffloadMode 101 | default: 102 | fmt.Printf("Error invalid XDP mode: %v, pick from: auto, generic, native or offload\n", *xdpMode) 103 | 104 | os.Exit(1) 105 | } 106 | 107 | if *enableTUI { 108 | *timeout = 0 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /bpf/cgroup.bpf.c: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2025 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | //go:build ignore 23 | 24 | #include "vmlinux.h" 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #include "cgroup.h" 32 | 33 | typedef struct cgroup_event_t { 34 | char path[PATH_MAX]; // cgroup path 35 | __u64 cgroupid; // cgroup ID 36 | } cgroupevent; 37 | 38 | struct { 39 | __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); // per cpu array requires 4.6 kernel 40 | __uint(max_entries, 1); 41 | __type(key, __u32); 42 | __type(value, cgroupevent); 43 | } cgroup_event SEC(".maps"); 44 | 45 | struct { 46 | __uint(type, 47 | BPF_MAP_TYPE_PERF_EVENT_ARRAY); // perf event array requires 4.3 kernel 48 | __uint(max_entries, MAX_ENTRIES); 49 | __type(key, int); 50 | __type(value, __u32); 51 | } perf_cgroup_event SEC(".maps"); 52 | 53 | /** 54 | * get_cgroupid - reads the cgroup ID from the given struct cgroup 55 | * 56 | * This function reads the cgroup ID from the given struct cgroup and returns 57 | * it. The function works on kernels v4.10 and above. 58 | * 59 | * @cgrp: the struct cgroup to read the cgroup ID from 60 | * 61 | * Returns: the cgroup ID as an unsigned 64-bit integer 62 | * 63 | * get_cgroupid() comes from aquasecurity/tracee, license: Apache-2.0 64 | */ 65 | static inline __attribute__((always_inline)) __u64 66 | get_cgroupid(struct cgroup *cgrp) { 67 | struct kernfs_node *kn = BPF_CORE_READ(cgrp, kn); 68 | 69 | if (kn == NULL) 70 | return 0; 71 | 72 | __u64 id; // was union kernfs_node_id before 5.5, can read it as u64 in both 73 | // situations 74 | 75 | if (bpf_core_type_exists(union kernfs_node_id)) { 76 | struct kernfs_node___older_v55 *kn_old = (void *)kn; 77 | struct kernfs_node___rh8 *kn_rh8 = (void *)kn; 78 | 79 | if (bpf_core_field_exists(kn_rh8->id)) { 80 | // RHEL8 has both types declared: union and u64: 81 | // kn->id 82 | // rh->rh_kabi_hidden_172->id 83 | // pointing to the same data 84 | bpf_core_read(&id, sizeof(__u64), &kn_rh8->id); 85 | id = id & 0xffffffff; // XXX: u32 is required 86 | } else { 87 | // all other regular kernels below v5.5 88 | bpf_core_read(&id, sizeof(__u64), &kn_old->id); 89 | id = id & 0xffffffff; // XXX: u32 is required 90 | } 91 | } else { 92 | // kernel v5.5 and above 93 | bpf_core_read(&id, sizeof(__u64), &kn->id); 94 | } 95 | 96 | return id; 97 | } 98 | /** 99 | * trace_cgroup_mkdir traces the creation of a new cgroup directory. 100 | * 101 | * This function is attached to the raw tracepoint for cgroup_mkdir events. 102 | * It retrieves the cgroup ID and the path of the newly created cgroup 103 | * directory, then stores this information in a cgroupevent structure. 104 | * The function outputs the event data to a perf event map for further 105 | * processing. 106 | * 107 | * @param ctx A pointer to the bpf_raw_tracepoint_args structure containing 108 | * the arguments of the tracepoint, including the destination 109 | * cgroup and path. 110 | * 111 | * @return Always returns 0. 112 | */ 113 | SEC("raw_tracepoint/cgroup_mkdir") 114 | int trace_cgroup_mkdir(struct bpf_raw_tracepoint_args *ctx) { 115 | struct cgroup *dst_cgrp = (struct cgroup *)ctx->args[0]; 116 | char *path = (char *)ctx->args[1]; 117 | 118 | __u64 cgroupid = get_cgroupid(dst_cgrp); 119 | __u32 zero_key = 0; 120 | 121 | cgroupevent *val = 122 | (cgroupevent *)bpf_map_lookup_elem(&cgroup_event, &zero_key); 123 | if (val == NULL) { 124 | return 0; 125 | } 126 | 127 | bpf_probe_read_str(val->path, PATH_MAX, path); 128 | val->cgroupid = cgroupid; 129 | 130 | bpf_perf_event_output(ctx, &perf_cgroup_event, BPF_F_CURRENT_CPU, val, 131 | sizeof(cgroupevent)); 132 | 133 | return 0; 134 | } 135 | 136 | char __license[] SEC("license") = "Dual MIT/GPL"; 137 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2024 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "context" 26 | "errors" 27 | "fmt" 28 | "log" 29 | "net" 30 | "os" 31 | "os/signal" 32 | "syscall" 33 | "time" 34 | 35 | "github.com/cilium/ebpf" 36 | "github.com/cilium/ebpf/link" 37 | "github.com/cilium/ebpf/rlimit" 38 | "github.com/hako/durafmt" 39 | ) 40 | 41 | // main is the entry point of the program. 42 | // 43 | // It loads the eBPF object files, removes resource limits for kernels <5.11, 44 | // and starts the appropriate packet capture method based on the flags passed. 45 | // 46 | // The packet capture methods are: 47 | // 48 | // - XDP (if *useXDP is set) 49 | // - TC (if *useXDP is not set) 50 | // - KProbes w/ PID tracking (if *useKProbes is set) 51 | // - cgroup tracing (if *useCGroup is set) 52 | // 53 | // After starting the packet capture method, it waits for the context to be 54 | // canceled, and then closes all the links and prints the final statistics. 55 | func main() { 56 | parseFlags() 57 | 58 | // Remove resource limits for kernels <5.11 59 | if err := rlimit.RemoveMemlock(); err != nil { 60 | log.Fatalf("Error removing memlock: %v", err) 61 | } 62 | 63 | // Load the compiled counter eBPF ELF and load it into the kernel 64 | var objsCounter counterObjects 65 | if err := loadCounterObjects(&objsCounter, nil); err != nil { 66 | log.Fatalf("Error loading eBPF objects: %v", err) 67 | } 68 | 69 | defer func() { _ = objsCounter.Close() }() 70 | 71 | // Load the compiled cgroup eBPF ELF and load it into the kernel 72 | var objsCgroup cgroupObjects 73 | if err := loadCgroupObjects(&objsCgroup, nil); err != nil { 74 | log.Fatalf("Error loading eBPF objects: %v", err) //nolint:gocritic 75 | } 76 | 77 | defer func() { _ = objsCgroup.Close() }() 78 | 79 | iface, err := net.InterfaceByName(*ifname) 80 | if err != nil { 81 | log.Fatalf("Error getting interface %q: %v", *ifname, err) 82 | } 83 | 84 | var links []link.Link 85 | 86 | defer func() { 87 | for _, l := range links { 88 | _ = l.Close() 89 | } 90 | }() 91 | 92 | switch { 93 | case *useCGroup != "": 94 | cGroupCacheInit() 95 | 96 | links = startCgroup(objsCounter, *useCGroup, links) 97 | links = startCGroupTrace(objsCgroup, links) 98 | 99 | rd, err := cGroupWatcher(objsCgroup) 100 | if err != nil { 101 | defer func() { _ = rd.Close() }() 102 | } 103 | // KProbes w/ PID tracking 104 | case *useKProbes: 105 | cGroupCacheInit() 106 | 107 | hooks := []kprobeHook{ 108 | {kprobe: "tcp_sendmsg", prog: objsCounter.TcpSendmsg}, 109 | {kprobe: "tcp_cleanup_rbuf", prog: objsCounter.TcpCleanupRbuf}, 110 | {kprobe: "ip_send_skb", prog: objsCounter.IpSendSkb}, 111 | {kprobe: "skb_consume_udp", prog: objsCounter.SkbConsumeUdp}, 112 | {kprobe: "__icmp_send", prog: objsCounter.IcmpSend}, 113 | {kprobe: "icmp6_send", prog: objsCounter.Icmp6Send}, 114 | {kprobe: "icmp_rcv", prog: objsCounter.IcmpRcv}, 115 | {kprobe: "icmpv6_rcv", prog: objsCounter.Icmpv6Rcv}, 116 | } 117 | 118 | links = startKProbes(hooks, links) 119 | links = startCGroupTrace(objsCgroup, links) 120 | 121 | rd, err := cGroupWatcher(objsCgroup) 122 | if err != nil { 123 | defer func() { _ = rd.Close() }() 124 | } 125 | // XDP 126 | case *useXDP: 127 | links = startXDP(objsCounter, iface, links) 128 | // TC 129 | default: 130 | links = startTC(objsCounter, iface, links) 131 | } 132 | 133 | c1, cancel := context.WithCancel(context.Background()) 134 | defer cancel() 135 | 136 | startTime := time.Now() 137 | 138 | //nolint:nestif 139 | if *enableTUI { 140 | drawTUI(objsCounter, startTime) 141 | } else { 142 | signalCh := make(chan os.Signal, 1) 143 | signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) 144 | 145 | go func() { 146 | s := <-signalCh 147 | _, _ = fmt.Fprintf(os.Stderr, "Received %v signal, trying to exit...\n", s) 148 | 149 | cancel() 150 | }() 151 | 152 | if *timeout > 0 { 153 | log.Printf("Listening for %v before exiting", durafmt.Parse(*timeout)) 154 | 155 | go func() { 156 | time.Sleep(*timeout) 157 | cancel() 158 | }() 159 | } 160 | 161 | <-c1.Done() 162 | 163 | var m []statEntry 164 | 165 | m, err = processMap(objsCounter.PktCount, startTime, bitrateSort) 166 | if err != nil { 167 | // reads from BPF_MAP_TYPE_LRU_HASH maps might get interrupted 168 | if errors.Is(err, ebpf.ErrIterationAborted) { 169 | _, _ = fmt.Fprint(os.Stderr, "Iteration aborted while reading eBPF map, output may be incomplete\n") 170 | } else { 171 | log.Fatalf("Error reading eBPF map: %v", err) 172 | } 173 | } 174 | 175 | if *jsonOutput { 176 | fmt.Println(outputJSON(m)) 177 | } else { 178 | fmt.Print(outputPlain(m)) 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /counter_arm64_bpfel.go: -------------------------------------------------------------------------------- 1 | // Code generated by bpf2go; DO NOT EDIT. 2 | //go:build arm64 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | _ "embed" 9 | "fmt" 10 | "io" 11 | "structs" 12 | 13 | "github.com/cilium/ebpf" 14 | ) 15 | 16 | type counterSockinfo struct { 17 | _ structs.HostLayout 18 | Comm [16]uint8 19 | Pid int32 20 | } 21 | 22 | type counterStatkey struct { 23 | _ structs.HostLayout 24 | Srcip struct { 25 | _ structs.HostLayout 26 | In6U struct { 27 | _ structs.HostLayout 28 | U6Addr8 [16]uint8 29 | } 30 | } 31 | Dstip struct { 32 | _ structs.HostLayout 33 | In6U struct { 34 | _ structs.HostLayout 35 | U6Addr8 [16]uint8 36 | } 37 | } 38 | Cgroupid uint64 39 | Comm [16]int8 40 | Pid int32 41 | SrcPort uint16 42 | DstPort uint16 43 | Proto uint8 44 | _ [7]byte 45 | } 46 | 47 | type counterStatvalue struct { 48 | _ structs.HostLayout 49 | Packets uint64 50 | Bytes uint64 51 | } 52 | 53 | // loadCounter returns the embedded CollectionSpec for counter. 54 | func loadCounter() (*ebpf.CollectionSpec, error) { 55 | reader := bytes.NewReader(_CounterBytes) 56 | spec, err := ebpf.LoadCollectionSpecFromReader(reader) 57 | if err != nil { 58 | return nil, fmt.Errorf("can't load counter: %w", err) 59 | } 60 | 61 | return spec, err 62 | } 63 | 64 | // loadCounterObjects loads counter and converts it into a struct. 65 | // 66 | // The following types are suitable as obj argument: 67 | // 68 | // *counterObjects 69 | // *counterPrograms 70 | // *counterMaps 71 | // 72 | // See ebpf.CollectionSpec.LoadAndAssign documentation for details. 73 | func loadCounterObjects(obj interface{}, opts *ebpf.CollectionOptions) error { 74 | spec, err := loadCounter() 75 | if err != nil { 76 | return err 77 | } 78 | 79 | return spec.LoadAndAssign(obj, opts) 80 | } 81 | 82 | // counterSpecs contains maps and programs before they are loaded into the kernel. 83 | // 84 | // It can be passed ebpf.CollectionSpec.Assign. 85 | type counterSpecs struct { 86 | counterProgramSpecs 87 | counterMapSpecs 88 | counterVariableSpecs 89 | } 90 | 91 | // counterProgramSpecs contains programs before they are loaded into the kernel. 92 | // 93 | // It can be passed ebpf.CollectionSpec.Assign. 94 | type counterProgramSpecs struct { 95 | IcmpSend *ebpf.ProgramSpec `ebpf:"__icmp_send"` 96 | CgroupSkbEgress *ebpf.ProgramSpec `ebpf:"cgroup_skb_egress"` 97 | CgroupSkbIngress *ebpf.ProgramSpec `ebpf:"cgroup_skb_ingress"` 98 | CgroupSockCreate *ebpf.ProgramSpec `ebpf:"cgroup_sock_create"` 99 | Icmp6Send *ebpf.ProgramSpec `ebpf:"icmp6_send"` 100 | IcmpRcv *ebpf.ProgramSpec `ebpf:"icmp_rcv"` 101 | Icmpv6Rcv *ebpf.ProgramSpec `ebpf:"icmpv6_rcv"` 102 | IpSendSkb *ebpf.ProgramSpec `ebpf:"ip_send_skb"` 103 | SkbConsumeUdp *ebpf.ProgramSpec `ebpf:"skb_consume_udp"` 104 | TcCountPackets *ebpf.ProgramSpec `ebpf:"tc_count_packets"` 105 | TcpCleanupRbuf *ebpf.ProgramSpec `ebpf:"tcp_cleanup_rbuf"` 106 | TcpSendmsg *ebpf.ProgramSpec `ebpf:"tcp_sendmsg"` 107 | XdpCountPackets *ebpf.ProgramSpec `ebpf:"xdp_count_packets"` 108 | } 109 | 110 | // counterMapSpecs contains maps before they are loaded into the kernel. 111 | // 112 | // It can be passed ebpf.CollectionSpec.Assign. 113 | type counterMapSpecs struct { 114 | PktCount *ebpf.MapSpec `ebpf:"pkt_count"` 115 | SockInfo *ebpf.MapSpec `ebpf:"sock_info"` 116 | } 117 | 118 | // counterVariableSpecs contains global variables before they are loaded into the kernel. 119 | // 120 | // It can be passed ebpf.CollectionSpec.Assign. 121 | type counterVariableSpecs struct { 122 | } 123 | 124 | // counterObjects contains all objects after they have been loaded into the kernel. 125 | // 126 | // It can be passed to loadCounterObjects or ebpf.CollectionSpec.LoadAndAssign. 127 | type counterObjects struct { 128 | counterPrograms 129 | counterMaps 130 | counterVariables 131 | } 132 | 133 | func (o *counterObjects) Close() error { 134 | return _CounterClose( 135 | &o.counterPrograms, 136 | &o.counterMaps, 137 | ) 138 | } 139 | 140 | // counterMaps contains all maps after they have been loaded into the kernel. 141 | // 142 | // It can be passed to loadCounterObjects or ebpf.CollectionSpec.LoadAndAssign. 143 | type counterMaps struct { 144 | PktCount *ebpf.Map `ebpf:"pkt_count"` 145 | SockInfo *ebpf.Map `ebpf:"sock_info"` 146 | } 147 | 148 | func (m *counterMaps) Close() error { 149 | return _CounterClose( 150 | m.PktCount, 151 | m.SockInfo, 152 | ) 153 | } 154 | 155 | // counterVariables contains all global variables after they have been loaded into the kernel. 156 | // 157 | // It can be passed to loadCounterObjects or ebpf.CollectionSpec.LoadAndAssign. 158 | type counterVariables struct { 159 | } 160 | 161 | // counterPrograms contains all programs after they have been loaded into the kernel. 162 | // 163 | // It can be passed to loadCounterObjects or ebpf.CollectionSpec.LoadAndAssign. 164 | type counterPrograms struct { 165 | IcmpSend *ebpf.Program `ebpf:"__icmp_send"` 166 | CgroupSkbEgress *ebpf.Program `ebpf:"cgroup_skb_egress"` 167 | CgroupSkbIngress *ebpf.Program `ebpf:"cgroup_skb_ingress"` 168 | CgroupSockCreate *ebpf.Program `ebpf:"cgroup_sock_create"` 169 | Icmp6Send *ebpf.Program `ebpf:"icmp6_send"` 170 | IcmpRcv *ebpf.Program `ebpf:"icmp_rcv"` 171 | Icmpv6Rcv *ebpf.Program `ebpf:"icmpv6_rcv"` 172 | IpSendSkb *ebpf.Program `ebpf:"ip_send_skb"` 173 | SkbConsumeUdp *ebpf.Program `ebpf:"skb_consume_udp"` 174 | TcCountPackets *ebpf.Program `ebpf:"tc_count_packets"` 175 | TcpCleanupRbuf *ebpf.Program `ebpf:"tcp_cleanup_rbuf"` 176 | TcpSendmsg *ebpf.Program `ebpf:"tcp_sendmsg"` 177 | XdpCountPackets *ebpf.Program `ebpf:"xdp_count_packets"` 178 | } 179 | 180 | func (p *counterPrograms) Close() error { 181 | return _CounterClose( 182 | p.IcmpSend, 183 | p.CgroupSkbEgress, 184 | p.CgroupSkbIngress, 185 | p.CgroupSockCreate, 186 | p.Icmp6Send, 187 | p.IcmpRcv, 188 | p.Icmpv6Rcv, 189 | p.IpSendSkb, 190 | p.SkbConsumeUdp, 191 | p.TcCountPackets, 192 | p.TcpCleanupRbuf, 193 | p.TcpSendmsg, 194 | p.XdpCountPackets, 195 | ) 196 | } 197 | 198 | func _CounterClose(closers ...io.Closer) error { 199 | for _, closer := range closers { 200 | if err := closer.Close(); err != nil { 201 | return err 202 | } 203 | } 204 | return nil 205 | } 206 | 207 | // Do not access this directly. 208 | // 209 | //go:embed counter_arm64_bpfel.o 210 | var _CounterBytes []byte 211 | -------------------------------------------------------------------------------- /counter_x86_bpfel.go: -------------------------------------------------------------------------------- 1 | // Code generated by bpf2go; DO NOT EDIT. 2 | //go:build 386 || amd64 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | _ "embed" 9 | "fmt" 10 | "io" 11 | "structs" 12 | 13 | "github.com/cilium/ebpf" 14 | ) 15 | 16 | type counterSockinfo struct { 17 | _ structs.HostLayout 18 | Comm [16]uint8 19 | Pid int32 20 | } 21 | 22 | type counterStatkey struct { 23 | _ structs.HostLayout 24 | Srcip struct { 25 | _ structs.HostLayout 26 | In6U struct { 27 | _ structs.HostLayout 28 | U6Addr8 [16]uint8 29 | } 30 | } 31 | Dstip struct { 32 | _ structs.HostLayout 33 | In6U struct { 34 | _ structs.HostLayout 35 | U6Addr8 [16]uint8 36 | } 37 | } 38 | Cgroupid uint64 39 | Comm [16]int8 40 | Pid int32 41 | SrcPort uint16 42 | DstPort uint16 43 | Proto uint8 44 | _ [7]byte 45 | } 46 | 47 | type counterStatvalue struct { 48 | _ structs.HostLayout 49 | Packets uint64 50 | Bytes uint64 51 | } 52 | 53 | // loadCounter returns the embedded CollectionSpec for counter. 54 | func loadCounter() (*ebpf.CollectionSpec, error) { 55 | reader := bytes.NewReader(_CounterBytes) 56 | spec, err := ebpf.LoadCollectionSpecFromReader(reader) 57 | if err != nil { 58 | return nil, fmt.Errorf("can't load counter: %w", err) 59 | } 60 | 61 | return spec, err 62 | } 63 | 64 | // loadCounterObjects loads counter and converts it into a struct. 65 | // 66 | // The following types are suitable as obj argument: 67 | // 68 | // *counterObjects 69 | // *counterPrograms 70 | // *counterMaps 71 | // 72 | // See ebpf.CollectionSpec.LoadAndAssign documentation for details. 73 | func loadCounterObjects(obj interface{}, opts *ebpf.CollectionOptions) error { 74 | spec, err := loadCounter() 75 | if err != nil { 76 | return err 77 | } 78 | 79 | return spec.LoadAndAssign(obj, opts) 80 | } 81 | 82 | // counterSpecs contains maps and programs before they are loaded into the kernel. 83 | // 84 | // It can be passed ebpf.CollectionSpec.Assign. 85 | type counterSpecs struct { 86 | counterProgramSpecs 87 | counterMapSpecs 88 | counterVariableSpecs 89 | } 90 | 91 | // counterProgramSpecs contains programs before they are loaded into the kernel. 92 | // 93 | // It can be passed ebpf.CollectionSpec.Assign. 94 | type counterProgramSpecs struct { 95 | IcmpSend *ebpf.ProgramSpec `ebpf:"__icmp_send"` 96 | CgroupSkbEgress *ebpf.ProgramSpec `ebpf:"cgroup_skb_egress"` 97 | CgroupSkbIngress *ebpf.ProgramSpec `ebpf:"cgroup_skb_ingress"` 98 | CgroupSockCreate *ebpf.ProgramSpec `ebpf:"cgroup_sock_create"` 99 | Icmp6Send *ebpf.ProgramSpec `ebpf:"icmp6_send"` 100 | IcmpRcv *ebpf.ProgramSpec `ebpf:"icmp_rcv"` 101 | Icmpv6Rcv *ebpf.ProgramSpec `ebpf:"icmpv6_rcv"` 102 | IpSendSkb *ebpf.ProgramSpec `ebpf:"ip_send_skb"` 103 | SkbConsumeUdp *ebpf.ProgramSpec `ebpf:"skb_consume_udp"` 104 | TcCountPackets *ebpf.ProgramSpec `ebpf:"tc_count_packets"` 105 | TcpCleanupRbuf *ebpf.ProgramSpec `ebpf:"tcp_cleanup_rbuf"` 106 | TcpSendmsg *ebpf.ProgramSpec `ebpf:"tcp_sendmsg"` 107 | XdpCountPackets *ebpf.ProgramSpec `ebpf:"xdp_count_packets"` 108 | } 109 | 110 | // counterMapSpecs contains maps before they are loaded into the kernel. 111 | // 112 | // It can be passed ebpf.CollectionSpec.Assign. 113 | type counterMapSpecs struct { 114 | PktCount *ebpf.MapSpec `ebpf:"pkt_count"` 115 | SockInfo *ebpf.MapSpec `ebpf:"sock_info"` 116 | } 117 | 118 | // counterVariableSpecs contains global variables before they are loaded into the kernel. 119 | // 120 | // It can be passed ebpf.CollectionSpec.Assign. 121 | type counterVariableSpecs struct { 122 | } 123 | 124 | // counterObjects contains all objects after they have been loaded into the kernel. 125 | // 126 | // It can be passed to loadCounterObjects or ebpf.CollectionSpec.LoadAndAssign. 127 | type counterObjects struct { 128 | counterPrograms 129 | counterMaps 130 | counterVariables 131 | } 132 | 133 | func (o *counterObjects) Close() error { 134 | return _CounterClose( 135 | &o.counterPrograms, 136 | &o.counterMaps, 137 | ) 138 | } 139 | 140 | // counterMaps contains all maps after they have been loaded into the kernel. 141 | // 142 | // It can be passed to loadCounterObjects or ebpf.CollectionSpec.LoadAndAssign. 143 | type counterMaps struct { 144 | PktCount *ebpf.Map `ebpf:"pkt_count"` 145 | SockInfo *ebpf.Map `ebpf:"sock_info"` 146 | } 147 | 148 | func (m *counterMaps) Close() error { 149 | return _CounterClose( 150 | m.PktCount, 151 | m.SockInfo, 152 | ) 153 | } 154 | 155 | // counterVariables contains all global variables after they have been loaded into the kernel. 156 | // 157 | // It can be passed to loadCounterObjects or ebpf.CollectionSpec.LoadAndAssign. 158 | type counterVariables struct { 159 | } 160 | 161 | // counterPrograms contains all programs after they have been loaded into the kernel. 162 | // 163 | // It can be passed to loadCounterObjects or ebpf.CollectionSpec.LoadAndAssign. 164 | type counterPrograms struct { 165 | IcmpSend *ebpf.Program `ebpf:"__icmp_send"` 166 | CgroupSkbEgress *ebpf.Program `ebpf:"cgroup_skb_egress"` 167 | CgroupSkbIngress *ebpf.Program `ebpf:"cgroup_skb_ingress"` 168 | CgroupSockCreate *ebpf.Program `ebpf:"cgroup_sock_create"` 169 | Icmp6Send *ebpf.Program `ebpf:"icmp6_send"` 170 | IcmpRcv *ebpf.Program `ebpf:"icmp_rcv"` 171 | Icmpv6Rcv *ebpf.Program `ebpf:"icmpv6_rcv"` 172 | IpSendSkb *ebpf.Program `ebpf:"ip_send_skb"` 173 | SkbConsumeUdp *ebpf.Program `ebpf:"skb_consume_udp"` 174 | TcCountPackets *ebpf.Program `ebpf:"tc_count_packets"` 175 | TcpCleanupRbuf *ebpf.Program `ebpf:"tcp_cleanup_rbuf"` 176 | TcpSendmsg *ebpf.Program `ebpf:"tcp_sendmsg"` 177 | XdpCountPackets *ebpf.Program `ebpf:"xdp_count_packets"` 178 | } 179 | 180 | func (p *counterPrograms) Close() error { 181 | return _CounterClose( 182 | p.IcmpSend, 183 | p.CgroupSkbEgress, 184 | p.CgroupSkbIngress, 185 | p.CgroupSockCreate, 186 | p.Icmp6Send, 187 | p.IcmpRcv, 188 | p.Icmpv6Rcv, 189 | p.IpSendSkb, 190 | p.SkbConsumeUdp, 191 | p.TcCountPackets, 192 | p.TcpCleanupRbuf, 193 | p.TcpSendmsg, 194 | p.XdpCountPackets, 195 | ) 196 | } 197 | 198 | func _CounterClose(closers ...io.Closer) error { 199 | for _, closer := range closers { 200 | if err := closer.Close(); err != nil { 201 | return err 202 | } 203 | } 204 | return nil 205 | } 206 | 207 | // Do not access this directly. 208 | // 209 | //go:embed counter_x86_bpfel.o 210 | var _CounterBytes []byte 211 | -------------------------------------------------------------------------------- /map.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2025 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "errors" 26 | "sync" 27 | "time" 28 | 29 | "github.com/cilium/ebpf" 30 | ) 31 | 32 | var ( 33 | haveBatchMapSupport bool 34 | checkBatchMapSupportOnce sync.Once 35 | ) 36 | 37 | // checkBatchMapSupport checks whether the given ebpf.Map supports batch lookups. 38 | // 39 | // A batch lookup is supported if the map supports the BPF_MAP_LOOKUP_BATCH 40 | // flag. This flag is only supported on Linux v5.6 and above. 41 | // 42 | // The function performs a batch lookup on the map with a single dummy key and 43 | // value to test whether the operation is supported. If the map does not 44 | // support batch lookups, the function returns false. Otherwise, it returns true. 45 | func checkBatchMapSupport(m *ebpf.Map) bool { 46 | keys := make([]counterStatkey, 1) 47 | values := make([]counterStatvalue, 1) 48 | 49 | var cursor ebpf.MapBatchCursor 50 | 51 | // BPF_MAP_LOOKUP_BATCH support requires v5.6 kernel 52 | _, err := m.BatchLookup(&cursor, keys, values, nil) 53 | 54 | if err != nil && errors.Is(err, ebpf.ErrNotSupported) { 55 | return false 56 | } 57 | 58 | return true 59 | } 60 | 61 | // listMap lists all the entries in the given ebpf.Map, converting the counter 62 | // values into a statEntry slice. 63 | // 64 | // The function uses the start time to calculate the duration of each entry. 65 | // 66 | // The function checks whether the map supports batch lookups and uses the 67 | // optimized listMapBatch or listMapIterate functions accordingly. 68 | // 69 | // listMap is safe to call concurrently. 70 | func listMap(m *ebpf.Map, start time.Time) ([]statEntry, error) { 71 | checkBatchMapSupportOnce.Do(func() { 72 | haveBatchMapSupport = checkBatchMapSupport(m) 73 | }) 74 | 75 | if haveBatchMapSupport { 76 | return listMapBatch(m, start) 77 | } 78 | 79 | // fallback to regular eBPF map iteration which might get interrupted for BPF_MAP_TYPE_LRU_HASH 80 | return listMapIterate(m, start) 81 | } 82 | 83 | // listMapBatch lists all the entries in the given ebpf.Map, converting the 84 | // counter values into a statEntry slice using batch lookups. 85 | // 86 | // The function uses the start time to calculate the duration of each entry. 87 | // 88 | // The function is safe to call concurrently. 89 | // 90 | // listMapBatch is used by listMap when the map supports batch lookups. 91 | func listMapBatch(m *ebpf.Map, start time.Time) ([]statEntry, error) { 92 | keys := make([]counterStatkey, m.MaxEntries()) 93 | values := make([]counterStatvalue, m.MaxEntries()) 94 | 95 | dur := time.Since(start).Seconds() 96 | stats := make([]statEntry, 0, m.MaxEntries()) 97 | 98 | var cursor ebpf.MapBatchCursor 99 | var ( 100 | count int 101 | c int 102 | err error 103 | ) 104 | 105 | // BPF_MAP_LOOKUP_BATCH support requires v5.6 kernel 106 | for { 107 | c, err = m.BatchLookup(&cursor, keys, values, nil) 108 | count += c 109 | 110 | if err != nil { 111 | if errors.Is(err, ebpf.ErrKeyNotExist) { 112 | break 113 | } 114 | 115 | return nil, err 116 | } 117 | } 118 | 119 | for i := 0; i < len(keys) && i < count; i++ { 120 | stats = addStats(stats, keys[i], values[i], dur) 121 | } 122 | 123 | return stats, nil 124 | } 125 | 126 | // listMapIterate iterates over all the entries in the given ebpf.Map, 127 | // converting the counter values into a statEntry slice. 128 | // 129 | // The function uses the start time to calculate the duration of each entry, 130 | // which is used to compute the bitrate. 131 | // 132 | // Parameters: 133 | // - m *ebpf.Map: the eBPF map to iterate over 134 | // - start time.Time: the start time for calculating entry duration 135 | // 136 | // Returns: 137 | // - []statEntry: a slice of statEntry objects containing the converted map entries 138 | // - error: an error if any occurred during map iteration, otherwise nil 139 | func listMapIterate(m *ebpf.Map, start time.Time) ([]statEntry, error) { 140 | var ( 141 | key counterStatkey 142 | val counterStatvalue 143 | ) 144 | 145 | dur := time.Since(start).Seconds() 146 | stats := make([]statEntry, 0, m.MaxEntries()) 147 | 148 | iter := m.Iterate() 149 | 150 | // build statEntry slice converting data where needed 151 | for iter.Next(&key, &val) { 152 | stats = addStats(stats, key, val, dur) 153 | } 154 | 155 | return stats, iter.Err() 156 | } 157 | 158 | // addStats takes a slice of statEntry, a counterStatkey, a counterStatvalue, 159 | // and a duration in seconds, and appends a new statEntry to the slice using 160 | // the provided data. The function converts the key SrcIP and DstIP fields to 161 | // netip.Addr objects, and the Comm field to a string. It also calculates the 162 | // bitrate by dividing the number of bytes by the duration. 163 | // 164 | // Parameters: 165 | // - stats []statEntry: the slice of statEntry objects to which the new entry is appended 166 | // - key counterStatkey: the counterStatkey object containing the source and 167 | // destination IP addresses, protocol, and ports, as well as the PID, Comm, 168 | // and CGroup information 169 | // - val counterStatvalue: the counterStatvalue object containing the packet 170 | // and byte counters 171 | // - dur float64: the duration in seconds 172 | // 173 | // Returns: 174 | // - []statEntry: the updated slice of statEntry objects 175 | func addStats(stats []statEntry, key counterStatkey, val counterStatvalue, dur float64) []statEntry { 176 | stats = append(stats, statEntry{ 177 | SrcIP: bytesToAddr(key.Srcip.In6U.U6Addr8), 178 | DstIP: bytesToAddr(key.Dstip.In6U.U6Addr8), 179 | Proto: protoToString(key.Proto), 180 | SrcPort: key.SrcPort, 181 | DstPort: key.DstPort, 182 | Bytes: val.Bytes, 183 | Packets: val.Packets, 184 | Bitrate: 8 * float64(val.Bytes) / dur, 185 | Pid: key.Pid, 186 | Comm: bsliceToString(key.Comm[:]), 187 | CGroup: cGroupToPath(key.Cgroupid), 188 | }) 189 | 190 | return stats 191 | } 192 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cilium/ebpf v0.20.0 h1:atwWj9d3NffHyPZzVlx3hmw1on5CLe9eljR8VuHTwhM= 2 | github.com/cilium/ebpf v0.20.0/go.mod h1:pzLjFymM+uZPLk/IXZUL63xdx5VXEo+enTzxkZXdycw= 3 | github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= 4 | github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= 5 | github.com/gdamore/tcell/v2 v2.13.2 h1:5j4srfF8ow3HICOv/61/sOhQtA25qxEB2XR3Q/Bhx2g= 6 | github.com/gdamore/tcell/v2 v2.13.2/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo= 7 | github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s= 8 | github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= 9 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 10 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 11 | github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4= 12 | github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0= 13 | github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= 14 | github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 15 | github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= 16 | github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE= 17 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 18 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 19 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 20 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 21 | github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= 22 | github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 23 | github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= 24 | github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= 25 | github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= 26 | github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= 27 | github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= 28 | github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 29 | github.com/peterbourgon/ff/v4 v4.0.0-beta.1 h1:hV8qRu3V7YfiSMsBSfPfdcznAvPQd3jI5zDddSrDoUc= 30 | github.com/peterbourgon/ff/v4 v4.0.0-beta.1/go.mod h1:onQJUKipvCyFmZ1rIYwFAh1BhPOvftb1uhvSI7krNLc= 31 | github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c= 32 | github.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY= 33 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 34 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 35 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 36 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 37 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 38 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 39 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 40 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 41 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 42 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 43 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 44 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 45 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 46 | golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= 47 | golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= 48 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 49 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 50 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 51 | golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= 52 | golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 53 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 54 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 55 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 57 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 59 | golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= 60 | golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 61 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 62 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 63 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 64 | golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= 65 | golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 66 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 67 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 68 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 69 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 70 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 71 | golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= 72 | golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 73 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 74 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 75 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 76 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 77 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 78 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 79 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 80 | -------------------------------------------------------------------------------- /cgroup.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2025 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "bytes" 26 | "encoding/binary" 27 | "errors" 28 | "fmt" 29 | "io/fs" 30 | "maps" 31 | "os" 32 | "path/filepath" 33 | "sync" 34 | "syscall" 35 | 36 | "github.com/cilium/ebpf/perf" 37 | ) 38 | 39 | const ( 40 | CGroupRootPath = "/sys/fs/cgroup" 41 | PerfBufferPages = 16 42 | ) 43 | 44 | var ( 45 | cGroupCache map[uint64]string 46 | cGroupCacheLock sync.RWMutex 47 | cGroupInitOnce sync.Once 48 | 49 | ErrNotStatT = errors.New("not a syscall.Stat_t") // not a syscall.Stat_t for path %s 50 | ) 51 | 52 | // cGroupToPath takes a cgroup ID and returns the corresponding path in the cgroup filesystem. 53 | // The function will cache cgroup paths for better performance, but it will also invalidate the cache 54 | // if it detects a change in the cgroup filesystem. If the cgroup ID is not found in the cache, it will 55 | // refresh the cache and return an empty string. 56 | // 57 | // The function is safe to call concurrently. 58 | func cGroupToPath(id uint64) string { 59 | // ID 0 is not a valid Cgroup ID 60 | if id == 0 { 61 | return "" 62 | } 63 | 64 | // fetch from cache first 65 | cGroupCacheLock.RLock() 66 | if p, ok := cGroupCache[id]; ok { 67 | cGroupCacheLock.RUnlock() 68 | 69 | return p 70 | } 71 | cGroupCacheLock.RUnlock() 72 | 73 | // force the cache refresh if missing 74 | cgroupCacheRefresh(CGroupRootPath) 75 | 76 | cGroupCacheLock.Lock() 77 | defer cGroupCacheLock.Unlock() 78 | 79 | // create negative cache entry if still missing 80 | if _, ok := cGroupCache[id]; !ok { 81 | cGroupCache[id] = fmt.Sprintf("cgroup-id: %v", id) 82 | } 83 | 84 | // return the result, positive or negative 85 | return cGroupCache[id] 86 | } 87 | 88 | // cGroupCacheInit initializes the cgroup cache and starts a goroutine to watch the cgroup filesystem for create events. 89 | // 90 | // The function creates an empty map to store the cgroup IDs and their corresponding paths and then starts a goroutine to watch the cgroup filesystem for create events. When a create event is received, the goroutine refreshes the cache. 91 | // 92 | // The function is safe to call concurrently. 93 | func cGroupCacheInit() { 94 | cGroupInitOnce.Do(func() { 95 | cGroupCache = make(map[uint64]string) 96 | 97 | // initial cache refresh 98 | cgroupCacheRefresh(CGroupRootPath) 99 | }) 100 | } 101 | 102 | // cgroupCacheRefresh refreshes the cache with the current cgroup paths. 103 | // 104 | // It walks the cgroup filesystem from the given directory and updates the cache 105 | // with the cgroup IDs and their corresponding paths. If the cache is already up 106 | // to date, the function does nothing. 107 | // 108 | // The function is safe to call concurrently. 109 | func cgroupCacheRefresh(dir string) { 110 | cGroupCacheLock.Lock() 111 | defer cGroupCacheLock.Unlock() 112 | 113 | if mapping, err := cGroupWalk(dir); err == nil { 114 | maps.Copy(cGroupCache, mapping) 115 | } 116 | } 117 | 118 | // cGroupWalk walks the cgroup filesystem and returns a mapping of cgroup IDs to their corresponding paths. 119 | // 120 | // The function takes a directory as an argument, which is the root of the cgroup filesystem. It walks the directory and its subdirectories, and for each subdirectory, it extracts the cgroup ID from the subdirectory's inode using `getInodeID`. 121 | // The function returns a mapping of cgroup IDs to their corresponding paths. If an error occurs during the walk, it is returned as the second argument. 122 | // 123 | // The function is safe to call concurrently. 124 | func cGroupWalk(dir string) (map[uint64]string, error) { 125 | mapping := map[uint64]string{} 126 | 127 | err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { 128 | if err != nil { 129 | // ignore disappearing files/directories 130 | if errors.Is(err, fs.ErrNotExist) { 131 | return nil 132 | } 133 | } 134 | 135 | if !d.IsDir() { 136 | return nil 137 | } 138 | 139 | i, err := getInodeID(path) 140 | if err != nil { 141 | // ignore disappearing files/directories 142 | if errors.Is(err, fs.ErrNotExist) { 143 | return nil 144 | } 145 | 146 | return err 147 | } 148 | 149 | mapping[i] = path 150 | 151 | return nil 152 | }) 153 | 154 | return mapping, err 155 | } 156 | 157 | // getInodeID returns the inode number of the file at the given path. 158 | // 159 | // The function takes a path as an argument, and returns the inode number of the file at that path. If an error occurs during the retrieval of the inode number, it is returned as the second argument. 160 | // 161 | // The function is safe to call concurrently. 162 | func getInodeID(path string) (uint64, error) { 163 | i, err := os.Stat(path) 164 | if err != nil { 165 | return 0, err 166 | } 167 | 168 | s, ok := i.Sys().(*syscall.Stat_t) 169 | if !ok { 170 | return 0, fmt.Errorf("%w: %s", ErrNotStatT, path) 171 | } 172 | 173 | return s.Ino, nil 174 | } 175 | 176 | // cGroupWatcher watches the cgroup filesystem for create events and updates the cgroup cache with the new cgroup IDs and their corresponding paths. 177 | // 178 | // The function takes a cgroupObjects structure as an argument, which contains the file descriptors of the cgroup filesystem and the perf event map. The function returns a perf.Reader object which can be used to read the events from the perf event map, and an error if any occurred during the creation of the perf.Reader object. 179 | // 180 | // The function is safe to call concurrently. 181 | // 182 | // The returned perf.Reader object will be closed when the returned error is ErrClosed. 183 | func cGroupWatcher(objs cgroupObjects) (*perf.Reader, error) { 184 | rd, err := perf.NewReader(objs.PerfCgroupEvent, PerfBufferPages*os.Getpagesize()) 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | go func() { 190 | var event cgroupCgroupevent 191 | 192 | var r perf.Record 193 | 194 | for { 195 | r, err = rd.Read() 196 | if err != nil { 197 | // reader has been closed 198 | if errors.Is(err, perf.ErrClosed) { 199 | return 200 | } 201 | 202 | continue 203 | } 204 | 205 | if err = binary.Read(bytes.NewBuffer(r.RawSample), binary.LittleEndian, &event); err != nil { 206 | continue 207 | } 208 | 209 | cGroupCacheLock.Lock() 210 | cGroupCache[event.Cgroupid] = bsliceToString(event.Path[:]) 211 | cGroupCacheLock.Unlock() 212 | } 213 | }() 214 | 215 | return rd, nil 216 | } 217 | -------------------------------------------------------------------------------- /output.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2024 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "bytes" 26 | "encoding/json" 27 | "fmt" 28 | "sort" 29 | "strings" 30 | "time" 31 | 32 | "github.com/cilium/ebpf" 33 | ) 34 | 35 | const ( 36 | Bps float64 = 1.0 37 | Kbps = 1000 * Bps 38 | Mbps = 1000 * Kbps 39 | Gbps = 1000 * Mbps 40 | Tbps = 1000 * Gbps 41 | ) 42 | 43 | // processMap processes a given ebpf.Map object by iterating over all its entries, 44 | // converting the counter values into a statEntry slice, and sorting the slice 45 | // using the given sortFunc. 46 | // 47 | // Parameters: 48 | // - m *ebpf.Map: the eBPF map to process 49 | // - start time.Time: the start time for calculating entry duration 50 | // - sortFunc func([]statEntry): a function to sort the statEntry slice 51 | // 52 | // Returns: 53 | // - []statEntry: the sorted statEntry slice 54 | // - error: an error if any occurred during map iteration, otherwise nil 55 | func processMap(m *ebpf.Map, start time.Time, sortFunc func([]statEntry)) ([]statEntry, error) { 56 | stats, err := listMap(m, start) 57 | sortFunc(stats) 58 | 59 | return stats, err 60 | } 61 | 62 | // bitrateSort sorts a slice of statEntry objects by their Bitrate field in descending order. 63 | // 64 | // Parameters: 65 | // 66 | // stats []statEntry - the slice of statEntry objects to be sorted 67 | func bitrateSort(stats []statEntry) { 68 | sort.Slice(stats, func(i, j int) bool { 69 | return stats[i].Bitrate > stats[j].Bitrate 70 | }) 71 | } 72 | 73 | // packetSort sorts a slice of statEntry objects by their Packets field in descending order. 74 | // 75 | // Parameters: 76 | // 77 | // stats []statEntry - the slice of statEntry objects to be sorted 78 | func packetSort(stats []statEntry) { 79 | sort.Slice(stats, func(i, j int) bool { 80 | return stats[i].Packets > stats[j].Packets 81 | }) 82 | } 83 | 84 | // bytesSort sorts a slice of statEntry objects by their Bytes field in descending order. 85 | // 86 | // Parameters: 87 | // 88 | // stats []statEntry - the slice of statEntry objects to be sorted 89 | func bytesSort(stats []statEntry) { 90 | sort.Slice(stats, func(i, j int) bool { 91 | return stats[i].Bytes > stats[j].Bytes 92 | }) 93 | } 94 | 95 | // srcIPSort sorts a slice of statEntry objects by their SrcIP field in descending order. 96 | // 97 | // Parameters: 98 | // 99 | // stats []statEntry - the slice of statEntry objects to be sorted 100 | func srcIPSort(stats []statEntry) { 101 | sort.Slice(stats, func(i, j int) bool { 102 | return stats[i].SrcIP.Compare(stats[j].SrcIP) < 0 103 | }) 104 | } 105 | 106 | // dstIPSort sorts a slice of statEntry objects by their DstIP field in descending order. 107 | // 108 | // Parameters: 109 | // 110 | // stats []statEntry - the slice of statEntry objects to be sorted 111 | func dstIPSort(stats []statEntry) { 112 | sort.Slice(stats, func(i, j int) bool { 113 | return stats[i].DstIP.Compare(stats[j].DstIP) < 0 114 | }) 115 | } 116 | 117 | // formatBitrate formats the bitrate value into a human-readable string. 118 | // 119 | // It takes a float64 parameter representing the bitrate and returns a string. 120 | func formatBitrate(b float64) string { 121 | switch { 122 | case b < Kbps: 123 | return fmt.Sprintf("%.2f bps", b) 124 | case b < 10*Kbps: 125 | return fmt.Sprintf("%.2f Kbps", b/Kbps) 126 | case b < 10*Mbps: 127 | return fmt.Sprintf("%.2f Mbps", b/Mbps) 128 | case b < 10*Gbps: 129 | return fmt.Sprintf("%.2f Gbps", b/Gbps) 130 | case b < 10*Tbps: 131 | return fmt.Sprintf("%.2f Tbps", b/Tbps) 132 | } 133 | 134 | return fmt.Sprintf("%.2fTbps", b/Tbps) 135 | } 136 | 137 | // outputPlain formats the provided statEntry slice into a plain text string. 138 | // 139 | // Each line contains information about a single protocol flow, including bitrate, 140 | // packets, bytes, protocol, source IP:port, destination IP:port, and ICMP type and 141 | // code if applicable. If kprobes are being used, the PID and comm fields are also 142 | // included. The output is sorted by bitrate in descending order. 143 | // 144 | // Parameters: 145 | // 146 | // m []statEntry - the statEntry slice to be formatted 147 | // 148 | // Returns: 149 | // 150 | // string - the formatted string 151 | func outputPlain(m []statEntry) string { 152 | var sb strings.Builder 153 | 154 | for _, v := range m { 155 | switch v.Proto { 156 | case "ICMPv4", "IPv6-ICMP": 157 | sb.WriteString(fmt.Sprintf("bitrate: %v, packets: %d, bytes: %d, proto: %v, src: %v, dst: %v, type: %d, code: %d", 158 | formatBitrate(v.Bitrate), v.Packets, v.Bytes, v.Proto, v.SrcIP, v.DstIP, v.SrcPort, v.DstPort)) 159 | default: 160 | sb.WriteString(fmt.Sprintf("bitrate: %v, packets: %d, bytes: %d, proto: %v, src: %v:%d, dst: %v:%d", 161 | formatBitrate(v.Bitrate), v.Packets, v.Bytes, v.Proto, v.SrcIP, v.SrcPort, v.DstIP, v.DstPort)) 162 | } 163 | 164 | if *useKProbes || *useCGroup != "" { 165 | if v.Pid > 0 { 166 | sb.WriteString(fmt.Sprintf(", pid: %d", v.Pid)) 167 | } 168 | 169 | if v.Comm != "" { 170 | sb.WriteString(fmt.Sprintf(", comm: %v", v.Comm)) 171 | } 172 | 173 | if v.CGroup != "" { 174 | sb.WriteString(fmt.Sprintf(", cgroup: %v", v.CGroup)) 175 | } 176 | } 177 | 178 | sb.WriteString("\n") 179 | } 180 | 181 | return sb.String() 182 | } 183 | 184 | // outputJSON formats the provided statEntry slice into a JSON string. 185 | // 186 | // The JSON is created using the encoding/json package, marshaling the statEntry 187 | // slice into a JSON array. The output is a string. 188 | // 189 | // Parameters: 190 | // 191 | // m []statEntry - the statEntry slice to be formatted 192 | // 193 | // Returns: 194 | // 195 | // string - the JSON string representation of m 196 | func outputJSON(m []statEntry) string { 197 | out, _ := json.Marshal(m) 198 | 199 | return string(out) 200 | } 201 | 202 | // bsliceToString converts a slice of int8 values to a string by first 203 | // transforming each int8 element to a byte. It then trims any NULL 204 | // characters from the resulting byte slice before converting it to 205 | // a string. 206 | // 207 | // Parameters: 208 | // - bs []int8: The slice of int8 values to be converted. 209 | // 210 | // Returns: 211 | // - string: The resulting string after conversion and trimming. 212 | func bsliceToString(bs []int8) string { 213 | b := make([]byte, len(bs)) 214 | for i, v := range bs { 215 | b[i] = byte(v) 216 | } 217 | 218 | // trim excess NULLs 219 | b = bytes.Trim(b, "\x00") 220 | 221 | return string(b) 222 | } 223 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | AccessModifierOffset: -2 4 | AlignAfterOpenBracket: Align 5 | AlignArrayOfStructures: None 6 | AlignConsecutiveAssignments: 7 | Enabled: false 8 | AcrossEmptyLines: false 9 | AcrossComments: false 10 | AlignCompound: false 11 | AlignFunctionPointers: false 12 | PadOperators: true 13 | AlignConsecutiveBitFields: 14 | Enabled: false 15 | AcrossEmptyLines: false 16 | AcrossComments: false 17 | AlignCompound: false 18 | AlignFunctionPointers: false 19 | PadOperators: false 20 | AlignConsecutiveDeclarations: 21 | Enabled: false 22 | AcrossEmptyLines: false 23 | AcrossComments: false 24 | AlignCompound: false 25 | AlignFunctionPointers: false 26 | PadOperators: false 27 | AlignConsecutiveMacros: 28 | Enabled: false 29 | AcrossEmptyLines: false 30 | AcrossComments: false 31 | AlignCompound: false 32 | AlignFunctionPointers: false 33 | PadOperators: false 34 | AlignConsecutiveShortCaseStatements: 35 | Enabled: false 36 | AcrossEmptyLines: false 37 | AcrossComments: false 38 | AlignCaseArrows: false 39 | AlignCaseColons: false 40 | AlignConsecutiveTableGenBreakingDAGArgColons: 41 | Enabled: false 42 | AcrossEmptyLines: false 43 | AcrossComments: false 44 | AlignCompound: false 45 | AlignFunctionPointers: false 46 | PadOperators: false 47 | AlignConsecutiveTableGenCondOperatorColons: 48 | Enabled: false 49 | AcrossEmptyLines: false 50 | AcrossComments: false 51 | AlignCompound: false 52 | AlignFunctionPointers: false 53 | PadOperators: false 54 | AlignConsecutiveTableGenDefinitionColons: 55 | Enabled: false 56 | AcrossEmptyLines: false 57 | AcrossComments: false 58 | AlignCompound: false 59 | AlignFunctionPointers: false 60 | PadOperators: false 61 | AlignEscapedNewlines: Right 62 | AlignOperands: Align 63 | AlignTrailingComments: 64 | Kind: Always 65 | OverEmptyLines: 0 66 | AllowAllArgumentsOnNextLine: true 67 | AllowAllParametersOfDeclarationOnNextLine: true 68 | AllowBreakBeforeNoexceptSpecifier: Never 69 | AllowShortBlocksOnASingleLine: Never 70 | AllowShortCaseExpressionOnASingleLine: true 71 | AllowShortCaseLabelsOnASingleLine: false 72 | AllowShortCompoundRequirementOnASingleLine: true 73 | AllowShortEnumsOnASingleLine: true 74 | AllowShortFunctionsOnASingleLine: All 75 | AllowShortIfStatementsOnASingleLine: Never 76 | AllowShortLambdasOnASingleLine: All 77 | AllowShortLoopsOnASingleLine: false 78 | AlwaysBreakAfterDefinitionReturnType: None 79 | AlwaysBreakBeforeMultilineStrings: false 80 | AttributeMacros: 81 | - __capability 82 | BinPackArguments: true 83 | BinPackParameters: true 84 | BitFieldColonSpacing: Both 85 | BraceWrapping: 86 | AfterCaseLabel: false 87 | AfterClass: false 88 | AfterControlStatement: Never 89 | AfterEnum: false 90 | AfterExternBlock: false 91 | AfterFunction: false 92 | AfterNamespace: false 93 | AfterObjCDeclaration: false 94 | AfterStruct: false 95 | AfterUnion: false 96 | BeforeCatch: false 97 | BeforeElse: false 98 | BeforeLambdaBody: false 99 | BeforeWhile: false 100 | IndentBraces: false 101 | SplitEmptyFunction: true 102 | SplitEmptyRecord: true 103 | SplitEmptyNamespace: true 104 | BreakAdjacentStringLiterals: true 105 | BreakAfterAttributes: Leave 106 | BreakAfterJavaFieldAnnotations: false 107 | BreakAfterReturnType: None 108 | BreakArrays: true 109 | BreakBeforeBinaryOperators: None 110 | BreakBeforeConceptDeclarations: Always 111 | BreakBeforeBraces: Attach 112 | BreakBeforeInlineASMColon: OnlyMultiline 113 | BreakBeforeTernaryOperators: true 114 | BreakConstructorInitializers: BeforeColon 115 | BreakFunctionDefinitionParameters: false 116 | BreakInheritanceList: BeforeColon 117 | BreakStringLiterals: true 118 | BreakTemplateDeclarations: MultiLine 119 | ColumnLimit: 80 120 | CommentPragmas: '^ IWYU pragma:' 121 | CompactNamespaces: false 122 | ConstructorInitializerIndentWidth: 4 123 | ContinuationIndentWidth: 4 124 | Cpp11BracedListStyle: true 125 | DerivePointerAlignment: false 126 | DisableFormat: false 127 | EmptyLineAfterAccessModifier: Never 128 | EmptyLineBeforeAccessModifier: LogicalBlock 129 | ExperimentalAutoDetectBinPacking: false 130 | FixNamespaceComments: true 131 | ForEachMacros: 132 | - foreach 133 | - Q_FOREACH 134 | - BOOST_FOREACH 135 | IfMacros: 136 | - KJ_IF_MAYBE 137 | IncludeBlocks: Preserve 138 | IncludeCategories: 139 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 140 | Priority: 2 141 | SortPriority: 0 142 | CaseSensitive: false 143 | - Regex: '^(<|"(gtest|gmock|isl|json)/)' 144 | Priority: 3 145 | SortPriority: 0 146 | CaseSensitive: false 147 | - Regex: '.*' 148 | Priority: 1 149 | SortPriority: 0 150 | CaseSensitive: false 151 | IncludeIsMainRegex: '(Test)?$' 152 | IncludeIsMainSourceRegex: '' 153 | IndentAccessModifiers: false 154 | IndentCaseBlocks: false 155 | IndentCaseLabels: false 156 | IndentExternBlock: AfterExternBlock 157 | IndentGotoLabels: true 158 | IndentPPDirectives: None 159 | IndentRequiresClause: true 160 | IndentWidth: 2 161 | IndentWrappedFunctionNames: false 162 | InsertBraces: false 163 | InsertNewlineAtEOF: false 164 | InsertTrailingCommas: None 165 | IntegerLiteralSeparator: 166 | Binary: 0 167 | BinaryMinDigits: 0 168 | Decimal: 0 169 | DecimalMinDigits: 0 170 | Hex: 0 171 | HexMinDigits: 0 172 | JavaScriptQuotes: Leave 173 | JavaScriptWrapImports: true 174 | KeepEmptyLines: 175 | AtEndOfFile: false 176 | AtStartOfBlock: true 177 | AtStartOfFile: true 178 | LambdaBodyIndentation: Signature 179 | LineEnding: DeriveLF 180 | MacroBlockBegin: '' 181 | MacroBlockEnd: '' 182 | MainIncludeChar: Quote 183 | MaxEmptyLinesToKeep: 1 184 | NamespaceIndentation: None 185 | ObjCBinPackProtocolList: Auto 186 | ObjCBlockIndentWidth: 2 187 | ObjCBreakBeforeNestedBlockParam: true 188 | ObjCSpaceAfterProperty: false 189 | ObjCSpaceBeforeProtocolList: true 190 | PackConstructorInitializers: BinPack 191 | PenaltyBreakAssignment: 2 192 | PenaltyBreakBeforeFirstCallParameter: 19 193 | PenaltyBreakComment: 300 194 | PenaltyBreakFirstLessLess: 120 195 | PenaltyBreakOpenParenthesis: 0 196 | PenaltyBreakScopeResolution: 500 197 | PenaltyBreakString: 1000 198 | PenaltyBreakTemplateDeclaration: 10 199 | PenaltyExcessCharacter: 1000000 200 | PenaltyIndentedWhitespace: 0 201 | PenaltyReturnTypeOnItsOwnLine: 60 202 | PointerAlignment: Right 203 | PPIndentWidth: -1 204 | QualifierAlignment: Leave 205 | ReferenceAlignment: Pointer 206 | ReflowComments: true 207 | RemoveBracesLLVM: false 208 | RemoveParentheses: Leave 209 | RemoveSemicolon: false 210 | RequiresClausePosition: OwnLine 211 | RequiresExpressionIndentation: OuterScope 212 | SeparateDefinitionBlocks: Leave 213 | ShortNamespaceLines: 1 214 | SkipMacroDefinitionBody: false 215 | SortIncludes: CaseSensitive 216 | SortJavaStaticImport: Before 217 | SortUsingDeclarations: LexicographicNumeric 218 | SpaceAfterCStyleCast: false 219 | SpaceAfterLogicalNot: false 220 | SpaceAfterTemplateKeyword: true 221 | SpaceAroundPointerQualifiers: Default 222 | SpaceBeforeAssignmentOperators: true 223 | SpaceBeforeCaseColon: false 224 | SpaceBeforeCpp11BracedList: false 225 | SpaceBeforeCtorInitializerColon: true 226 | SpaceBeforeInheritanceColon: true 227 | SpaceBeforeJsonColon: false 228 | SpaceBeforeParens: ControlStatements 229 | SpaceBeforeParensOptions: 230 | AfterControlStatements: true 231 | AfterForeachMacros: true 232 | AfterFunctionDefinitionName: false 233 | AfterFunctionDeclarationName: false 234 | AfterIfMacros: true 235 | AfterOverloadedOperator: false 236 | AfterPlacementOperator: true 237 | AfterRequiresInClause: false 238 | AfterRequiresInExpression: false 239 | BeforeNonEmptyParentheses: false 240 | SpaceBeforeRangeBasedForLoopColon: true 241 | SpaceBeforeSquareBrackets: false 242 | SpaceInEmptyBlock: false 243 | SpacesBeforeTrailingComments: 1 244 | SpacesInAngles: Never 245 | SpacesInContainerLiterals: true 246 | SpacesInLineCommentPrefix: 247 | Minimum: 1 248 | Maximum: -1 249 | SpacesInParens: Never 250 | SpacesInParensOptions: 251 | ExceptDoubleParentheses: false 252 | InCStyleCasts: false 253 | InConditionalStatements: false 254 | InEmptyParentheses: false 255 | Other: false 256 | SpacesInSquareBrackets: false 257 | Standard: Latest 258 | StatementAttributeLikeMacros: 259 | - Q_EMIT 260 | StatementMacros: 261 | - Q_UNUSED 262 | - QT_REQUIRE_VERSION 263 | TableGenBreakInsideDAGArg: DontBreak 264 | TabWidth: 8 265 | UseTab: Never 266 | VerilogBreakBetweenInstancePorts: true 267 | WhitespaceSensitiveMacros: 268 | - BOOST_PP_STRINGIZE 269 | - CF_SWIFT_NAME 270 | - NS_SWIFT_NAME 271 | - PP_STRINGIZE 272 | - STRINGIZE 273 | ... 274 | 275 | -------------------------------------------------------------------------------- /tui.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2025 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | //nolint:mnd 23 | package main 24 | 25 | import ( 26 | "fmt" 27 | "strconv" 28 | "strings" 29 | "time" 30 | 31 | "github.com/gdamore/tcell/v2" 32 | "github.com/rivo/tview" 33 | ) 34 | 35 | const ( 36 | naviText = `[white]↑/k[-] up • [white]↓/j[-] down • [white]q/x[-] exit • [white]r[-] redraw & jump on top • [white]0[-] sort by bitrate • [white]1[-] sort by packets • [white]2[-] sort by bytes • [white]3[-] sort by source IP • [white]4[-] sort by dest IP` 37 | ) 38 | 39 | // drawTUI displays a TUI (text-based user interface) with a table displaying 40 | // packet statistics. The TUI is updated in real time with the latest packet 41 | // statistics. The table has the following columns: 42 | // 43 | // - Bitrate (in Mbps) 44 | // - Number of packets 45 | // - Number of bytes 46 | // - Protocol (TCP, UDP, ICMP, or Other) 47 | // - Source IP 48 | // - Destination IP 49 | // - Type (ICMP echo request, ICMP echo reply, TCP SYN, TCP FIN, UDP, or Other) 50 | // - Code (ICMP code, or 0 for non-ICMP packets) 51 | // - PID (process ID) 52 | // - Comm (process name) 53 | // - CGroup (cgroup name) 54 | // 55 | // The TUI is interactive: pressing 'q' or 'x' will exit the program, 56 | // pressing 'r' or 'l' will redraw the TUI, and pressing any other key will 57 | // do nothing. 58 | func drawTUI(objs counterObjects, startTime time.Time) { 59 | app := tview.NewApplication() 60 | tableSort := bitrateSort 61 | 62 | // packet statistics 63 | statsTable := tview.NewTable(). 64 | SetBorders(true). 65 | SetSelectable(true, false). 66 | Select(0, 0). 67 | SetFixed(1, 1) 68 | 69 | statsTable.SetTitle("Network traffic monitor"). 70 | SetTitleAlign(tview.AlignLeft). 71 | SetBorder(true) 72 | 73 | statsTable.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 74 | switch event.Rune() { 75 | case '0': 76 | tableSort = bitrateSort 77 | 78 | statsTable.Select(0, 0) 79 | case '1': 80 | tableSort = packetSort 81 | 82 | statsTable.Select(0, 0) 83 | case '2': 84 | tableSort = bytesSort 85 | 86 | statsTable.Select(0, 0) 87 | case '3': 88 | tableSort = srcIPSort 89 | 90 | statsTable.Select(0, 0) 91 | case '4': 92 | tableSort = dstIPSort 93 | 94 | statsTable.Select(0, 0) 95 | case 'q', 'x', 'Q', 'X': 96 | app.Stop() 97 | case 'r', 'R': 98 | statsTable.Select(0, 0) 99 | app.ForceDraw() 100 | } 101 | 102 | return event 103 | }) 104 | 105 | // info view 106 | infoView := tview.NewTextView(). 107 | SetTextColor(tcell.ColorYellow) 108 | 109 | switch { 110 | case *useCGroup != "": 111 | infoView.SetText("CGroup eBPF mode w/ partial PID and comm tracking") 112 | case *useKProbes: 113 | infoView.SetText("KProbes eBPF mode w/ PID and comm tracking") 114 | case *useXDP: 115 | infoView.SetText(fmt.Sprintf("XDP (eXpress Data Path) eBPF mode %v on interface %v, only ingress stats", 116 | *xdpMode, *ifname)) 117 | default: 118 | infoView.SetText(fmt.Sprintf("TC (Traffic Control) eBPF mode on interface %v", *ifname)) 119 | } 120 | 121 | // navigation 122 | naviView := tview.NewTextView(). 123 | SetTextColor(tcell.ColorDimGray). 124 | SetWrap(true). 125 | SetWordWrap(true). 126 | SetDynamicColors(true) 127 | naviView.SetText(naviText) 128 | 129 | // grid layout 130 | grid := tview.NewGrid().SetRows(2, 0, 3). 131 | AddItem(infoView, 0, 0, 1, 1, 0, 0, false). 132 | AddItem(statsTable, 1, 0, 1, 1, 0, 0, true). 133 | AddItem(naviView, 2, 0, 1, 1, 0, 0, false) 134 | 135 | // start the update loop 136 | go updateStatsTable(app, statsTable, &tableSort, objs, startTime) 137 | 138 | _ = app.SetRoot(grid, true). 139 | SetFocus(statsTable). 140 | Run() 141 | } 142 | 143 | // updateStatsTable starts an infinite loop that updates the given table with 144 | // packet statistics at regular intervals. The loop is stopped when the 145 | // application is stopped. 146 | // 147 | // The table is populated with the following columns: 148 | // 149 | // - Bitrate (in Mbps) 150 | // - Number of packets 151 | // - Number of bytes 152 | // - Protocol (TCP, UDP, ICMP, or Other) 153 | // - Source IP 154 | // - Destination IP 155 | // - Type (ICMP echo request, ICMP echo reply, TCP SYN, TCP FIN, UDP, or Other) 156 | // - Code (ICMP code, or 0 for non-ICMP packets) 157 | // - PID (process ID) 158 | // - Comm (process name) 159 | // - CGroup (cgroup name) 160 | // 161 | // Note that the table is cleared and recreated on each iteration, so any cell 162 | // attributes are lost on each iteration. 163 | func updateStatsTable(app *tview.Application, table *tview.Table, tableSort *func(stats []statEntry), 164 | objs counterObjects, startTime time.Time, 165 | ) { 166 | headers := []string{ 167 | "bitrate", // column 0 168 | "packets", // column 1 169 | "bytes", // column 2 170 | "proto", // column 3 171 | "src", // column 4 172 | "dst", // column 5 173 | "type", // column 6 174 | "code", // column 7 175 | "pid", // column 8, only kprobes and cgroup 176 | "comm", // column 9, only kprobes and cgroup 177 | "cgroup", // column 10, only kprobes and cgroup 178 | } 179 | 180 | // remove pid, comm and cgroup columns if not in use 181 | if !*useKProbes && *useCGroup == "" { 182 | headers = headers[:8] 183 | } 184 | 185 | var m []statEntry 186 | 187 | for { 188 | app.QueueUpdateDraw(func() { 189 | table.Clear() 190 | 191 | for i, v := range headers { 192 | table.SetCell(0, i, &tview.TableCell{ 193 | Text: v, 194 | NotSelectable: true, 195 | Align: tview.AlignLeft, 196 | Color: tcell.ColorLightYellow, 197 | BackgroundColor: tcell.ColorDefault, 198 | Attributes: tcell.AttrBold, 199 | }) 200 | } 201 | 202 | m, _ = processMap(objs.PktCount, startTime, *tableSort) 203 | 204 | for i, v := range m { 205 | // populate bitrate, packets, bytes and proto 206 | table.SetCell(i+1, 0, tview.NewTableCell(formatBitrate(v.Bitrate)). 207 | SetTextColor(tcell.ColorWhite). 208 | SetExpansion(1)) 209 | 210 | table.SetCell(i+1, 1, tview.NewTableCell(strconv.FormatUint(v.Packets, 10)). 211 | SetTextColor(tcell.ColorWhite). 212 | SetExpansion(1)) 213 | 214 | table.SetCell(i+1, 2, tview.NewTableCell(strconv.FormatUint(v.Bytes, 10)). 215 | SetTextColor(tcell.ColorWhite). 216 | SetExpansion(1)) 217 | 218 | table.SetCell(i+1, 3, tview.NewTableCell(v.Proto). 219 | SetTextColor(tcell.ColorWhite). 220 | SetExpansion(1)) 221 | 222 | // populate src, dst, src port, dst port, type and code 223 | switch v.Proto { 224 | case "ICMPv4", "IPv6-ICMP": 225 | table.SetCell(i+1, 4, tview.NewTableCell(v.SrcIP.String()). 226 | SetTextColor(tcell.ColorWhite). 227 | SetExpansion(1)) 228 | 229 | table.SetCell(i+1, 5, tview.NewTableCell(v.DstIP.String()). 230 | SetTextColor(tcell.ColorWhite). 231 | SetExpansion(1)) 232 | 233 | table.SetCell(i+1, 6, tview.NewTableCell(strconv.Itoa(int(v.SrcPort))). 234 | SetTextColor(tcell.ColorWhite). 235 | SetExpansion(1)) 236 | 237 | table.SetCell(i+1, 7, tview.NewTableCell(strconv.Itoa(int(v.DstPort))). 238 | SetTextColor(tcell.ColorWhite). 239 | SetExpansion(1)) 240 | default: 241 | table.SetCell(i+1, 4, tview.NewTableCell(fmt.Sprintf("%v:%d", v.SrcIP, v.SrcPort)). 242 | SetTextColor(tcell.ColorWhite). 243 | SetExpansion(1)) 244 | 245 | table.SetCell(i+1, 5, tview.NewTableCell(fmt.Sprintf("%v:%d", v.DstIP, v.DstPort)). 246 | SetTextColor(tcell.ColorWhite). 247 | SetExpansion(1)) 248 | 249 | table.SetCell(i+1, 6, tview.NewTableCell(""). 250 | SetTextColor(tcell.ColorWhite). 251 | SetExpansion(1)) 252 | 253 | table.SetCell(i+1, 7, tview.NewTableCell(""). 254 | SetTextColor(tcell.ColorWhite). 255 | SetExpansion(1)) 256 | } 257 | 258 | // populate pid, comm and cgroup 259 | if *useKProbes || *useCGroup != "" { 260 | table.SetCell(i+1, 8, tview.NewTableCell(strconv.FormatInt(int64(v.Pid), 10)). 261 | SetTextColor(tcell.ColorWhite). 262 | SetExpansion(1)) 263 | 264 | table.SetCell(i+1, 9, tview.NewTableCell(v.Comm). 265 | SetTextColor(tcell.ColorWhite). 266 | SetExpansion(1)) 267 | 268 | // trim system CgroupRootPath from the Cgroup path 269 | table.SetCell(i+1, 10, tview.NewTableCell(strings.TrimPrefix(v.CGroup, CGroupRootPath)). 270 | SetTextColor(tcell.ColorWhite). 271 | SetExpansion(1)) 272 | } 273 | } 274 | }) 275 | 276 | time.Sleep(*refresh) 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pktstat-bpf 2 | 3 | [![GitHub license](https://img.shields.io/github/license/dkorunic/pktstat-bpf)](https://github.com/dkorunic/pktstat-bpf/blob/master/LICENSE) 4 | [![GitHub release](https://img.shields.io/github/release/dkorunic/pktstat-bpf)](https://github.com/dkorunic/pktstat-bpf/releases/latest) 5 | 6 | ![](gopher.png) 7 | 8 | ## About 9 | 10 | pktstat-bpf is a simple replacement for ncurses/libpcap-based [pktstat](https://github.com/dleonard0/pktstat), using Linux eBPF ([extended Berkeley Packet Filter](https://prototype-kernel.readthedocs.io/en/latest/bpf/)) program, allowing packet statistics gathering even under **very high traffic volume** conditions, typically several million packets per second even on an average server. In this scenario (high volume, DoS attacks etc.) typically regular packet capture solutions start being unreliable due to increasing packet loss. 11 | 12 | At the end of the execution program will display per-IP and per-protocol statistics sorted by per-connection bps, packets and (source-IP:port, destination-IP:port) tuples. 13 | 14 | Program consists of the [eBPF code in C](bpf/counter.bpf.c) and the pure-Go userland Golang program that parses and outputs final IP/port/protocol/bitrate statistics. Go part of the program uses wonderful [cillium/ebpf](https://github.com/cilium/ebpf) library to load and run eBPF program, interfacing with eBPF map. 15 | 16 | By default eBPF component uses **TC** (Traffic Control) eBPF hooks with TCX attaching requiring at minimum Linux kernel **v6.6** for both ingress and egress traffic statistics for TCP, UDP, ICMPv4 and ICMPv6. It can also switch to even faster [XDP](https://github.com/xdp-project/xdp-tutorial) (eXpress Data Path) hook but with a consequence of **losing egress statistics** since **XDP** works only in ingress path. XDP mode due to XDP program to network interface attaching calls requires at minimum Linux kernel **v5.9**. Some distributions might have backported XDP/TC patches (notable example is Red Hat Enterprise Linux kernel) and eBPF program might work on older kernels too (see requirements for more info). 17 | 18 | Alternatively it can use **KProbes** to monitor TCP, UDP, ICMPv4 and ICMPv6 communication throughout all containers, K8s pods, translations and forwards and display process ID as well as process name and CGroup path, if the traffic was being sent or delivered to userspace application. KProbes traditionally work the slowest, being closest to the userspace -- but they bring sometimes useful process information. KProbes work also with much older Linux kernels as well, but the hard-dependancy is a [BTF-enabled](https://docs.ebpf.io/concepts/btf/) kernel. Program will make sure to resolve kernel-level CGroup IDs to CGroup paths (below `/sys/fs/cgroup`) by both processing `/sys/fs/cgroup` and getting kernel CGroup mkdir events from [custom eBPF code](bpf/cgroup.bpf.c). 19 | 20 | In case that you need to monitor just a specific **CGroup**, it is possible as well and monitoring both ingress and egress traffic is supported. You can also monitor all traffic by attaching to the root CGroup (e.g. `/sys/fs/cgroup`). Process tracking is possible when using CGroups, but only for traffic where pktstat-bpf observed the socket creation. 21 | ![Demo](demo.gif) 22 | 23 | ## Talks 24 | 25 | The author of this tool has given a few eBPF talks which can be seen below, together with [slides](https://dkorunic.net/pdf/Korunic_eBPF.pdf): 26 | 27 | - Shorter eBPF features/capabilities/implementation overview (35 minutes): 28 | 29 | [![DORS/CLUC 2025: eBPF](https://img.youtube.com/vi/m8dbesXHOU4/0.jpg)](https://youtu.be/m8dbesXHOU4) 30 | 31 | - Longer dive into eBPF features/capabilities/implementation and security (~45 minutes, in Croatian language): 32 | 33 | [![DEEP 2024: eBPF: Features, capabilities and implementation](https://img.youtube.com/vi/9mQ03Cpfq_g/0.jpg)](https://youtu.be/9mQ03Cpfq_g) 34 | 35 | ## Requirements 36 | 37 | Hard requirement for eBPF program is Linux kernel **4.10** with BTF enabled and in such older kernels most likely only supported mode will be KProbes (RHEL/CentOS 8, Debian 10). From **5.9** kernel onwards (RHEL/CentOS 9, Debian 11, Ubuntu 20.04) XDP mode is supported. TC might even work on **5.14** (RHEL/CentOS 9) depending if the distribution has backported TC eBPF patches. In all recent distributions (RHEL/CentOS 9, Debian 12, Ubuntu 24.04), all eBPF modes are fully supported. 38 | 39 | Loading eBPF program typically requires root privileges and in our specific case pointer arithmetics in eBPF code causes [eBPF verifier](https://docs.kernel.org/bpf/verifier.html) to explicitly deny non-root use. Kernel has to be with BTF enabled and some features require more recent kernels, as shown in the table below. 40 | 41 | Typically BPF JIT (Just in Time compiler) should be enabled for best performance (and most Linux distributions have this enabled by default): 42 | 43 | ```shell 44 | sysctl -w net.core.bpf_jit_enable=1 45 | ``` 46 | 47 | In case of XDP, not all NIC drivers support **Native XDP** (XDP program is loaded by NIC driver with XDP support as part of initial receive path and most common 10G drivers already support this) or even **Offloaded XDP** (XDP program loads directly on NIC with hardware XDP support and executes without using CPU), causing kernel to fallback on **Generic XDP** with reduced performance. Generic XDP does not require any special support from NIC drivers, but such XDP happens much later in the networking stack and in such case performance is more or less equivalent to TC hooks. 48 | 49 | The following table maps features, requirements and expected performance for described modes: 50 | 51 | | Capture type | Ingress | Egress | Performance | Process tracking | Kernel required | SmartNIC required | 52 | | --------------------------------------------------- | ------- | ------ | -------------- | ------------------------------------------ | --------------- | ----------------- | 53 | | Generic [PCAP](https://github.com/dkorunic/pktstat) | Yes | Yes | Low | No | Any | No | 54 | | [AF_PACKET](https://github.com/dkorunic/pktstat) | Yes | Yes | Medium | No | v2.2 | No | 55 | | KProbes | Yes | Yes | Medium+ | **Yes** (command, process ID, CGroup path) | v4.10 | No | 56 | | CGroup (SKB) | Yes | Yes | Medium+ | Partial (command, process ID) | v4.10 | No | 57 | | TC (SchedACT) | Yes | Yes | **High** | No | v6.6 | No | 58 | | XDP Generic | Yes | **No** | **High** | No | v5.9 | No | 59 | | XDP Native | Yes | **No** | **Very high** | No | v5.9 | No | 60 | | XDP Offloaded | Yes | **No** | **Wire speed** | No | v5.9 | **Yes** | 61 | 62 | A list of XDP compatible drivers follows (and it is not necessarily up-to-date): 63 | 64 | - [xdp-project XDP driver list](https://github.com/xdp-project/xdp-project/blob/master/areas/drivers/README.org) 65 | - [IO Visor XDP driver list](https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md#xdp) 66 | 67 | ## Usage 68 | 69 | ```shell 70 | NAME 71 | pktstat-bpf 72 | 73 | FLAGS 74 | -?, --help display help 75 | -j, --json if true, output in JSON format 76 | -c, --cgroup STRING the path to a CGroup V2 to measure statistics on 77 | -x, --xdp if true, use XDP instead of TC (this disables egress statistics) 78 | -k, --kprobes if true, use KProbes for per-proces TCP/UDP statistics 79 | -g, --tui if true, enable TUI 80 | --version display program version 81 | -i, --iface STRING interface to read from (default: eth0) 82 | --xdp_mode STRING XDP attach mode (auto, generic, native or offload; native and offload require NIC driver support) (default: auto) 83 | -r, --refresh DURATION refresh interval in TUI (default: 1s) 84 | -t, --timeout DURATION timeout for packet capture in CLI (default: 10m0s) 85 | ``` 86 | 87 | It is possible to specify interface with `--iface`. 88 | 89 | Timeout `--timeout` will stop execution after a specified time, but it is also possible to interrupt program with Ctrl C, SIGTERM or SIGINT. 90 | 91 | With `--tui` program will switch to a very simple TUI primarily for continous monitoring purpose. Use arrow keys to browse statistics table and keys 'q' or 'x' to exit. 92 | 93 | With `--json` it is possible to get traffic statistics in JSON format. 94 | 95 | With `--xdp` program will switch from TC eBPF mode to XDP eBPF mode, working in even more high-performance mode however this will disable all egress statistics. On program exit it is also possible to get an interface reset, so it is best to use this program inside of [screen](https://www.gnu.org/software/screen/) or [tmux](https://github.com/tmux/tmux). 96 | 97 | Additionally it is possible to change XDP attach mode with `--xdp_mode` from `auto` (best-effort between native and generic) to `native` or `offload`, for NIC drivers that support XDP or even NICs that have hardware XDP support. 98 | 99 | With `--kprobes` program will switch to Kprobe mode and track TCP and UDP traffic per process. Performance will be even more degraded compared to TC and XDP mode, but all per-process traffic will be visible, inside of all Cgroups, containers, K8s pods etc. Additional details such as process command name, process ID and Control Group will be shown as well. 100 | 101 | With `--cgroup` parameter with path to the CGroup it is possible to measure ingress and egress traffic for a specific Control Group. Additional details such as process command name and process ID will be shown if available. 102 | 103 | ## Star History 104 | 105 | [![Star History Chart](https://api.star-history.com/svg?repos=dkorunic/pktstat,dkorunic/pktstat-bpf&type=Date)](https://star-history.com/#dkorunic/pktstat&dkorunic/pktstat-bpf&Date) 106 | -------------------------------------------------------------------------------- /probe.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2025 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "errors" 26 | "log" 27 | "net" 28 | 29 | "github.com/cilium/ebpf" 30 | "github.com/cilium/ebpf/features" 31 | "github.com/cilium/ebpf/link" 32 | ) 33 | 34 | // startKProbes attaches a series of eBPF programs to kernel functions using KProbes. 35 | // 36 | // This function iterates over a slice of kprobeHook structs, each containing a kernel function 37 | // name (kprobe) and an associated eBPF program. It attempts to attach each eBPF program to its 38 | // respective kernel function using KProbes. If a Kprobe cannot be attached, an error message 39 | // is logged, but the function continues with the next Kprobe. 40 | // 41 | // The function first checks if KProbes are supported by the current kernel. If not supported, 42 | // it logs a fatal error and terminates the program. 43 | // 44 | // Parameters: 45 | // 46 | // hooks []kprobeHook: A slice of kprobeHook structs, where each struct contains a kernel 47 | // function name and an associated eBPF program. 48 | // 49 | // links []link.Link: A slice of link.Link objects to which successfully attached KProbes 50 | // are appended. 51 | // 52 | // Returns: 53 | // 54 | // []link.Link: The updated slice of link.Link objects, including any newly attached KProbes. 55 | func startKProbes(hooks []kprobeHook, links []link.Link) []link.Link { 56 | var l link.Link 57 | 58 | err := features.HaveProgramType(ebpf.Kprobe) 59 | if errors.Is(err, ebpf.ErrNotSupported) { 60 | log.Fatalf("KProbes are not supported on this kernel") 61 | } 62 | 63 | if err != nil { 64 | log.Fatalf("Error checking KProbes support: %v", err) 65 | } 66 | 67 | for _, kp := range hooks { 68 | l, err = link.Kprobe(kp.kprobe, kp.prog, nil) 69 | if err != nil { 70 | log.Printf("Unable to attach %q KProbe: %v", kp.kprobe, err) 71 | 72 | continue 73 | } 74 | 75 | links = append(links, l) 76 | } 77 | 78 | log.Printf("Using KProbes mode w/ PID/comm tracking") 79 | 80 | return links 81 | } 82 | 83 | // startXDP attaches an eBPF XDP program to a network interface for packet counting. 84 | // 85 | // This function checks if the XDP program type is supported by the kernel. If supported, 86 | // it attaches the XDP program to the specified network interface's ingress path. Note that 87 | // egress support is not available for XDP, and the function requires at least a v5.9 kernel 88 | // for BPF_LINK_CREATE, though it might work with older RHEL kernels. 89 | // 90 | // Parameters: 91 | // 92 | // objs counterObjects: Contains the eBPF programs, including the XDP program to be attached. 93 | // iface *net.Interface: The network interface to which the XDP program should be attached. 94 | // links []link.Link: A slice of existing link.Link objects to which the newly attached XDP link 95 | // will be appended. 96 | // 97 | // Returns: 98 | // 99 | // []link.Link: The updated slice of link.Link objects, now including the newly attached XDP link. 100 | func startXDP(objs counterObjects, iface *net.Interface, links []link.Link) []link.Link { 101 | var l link.Link 102 | 103 | err := features.HaveProgramType(ebpf.XDP) 104 | if errors.Is(err, ebpf.ErrNotSupported) { 105 | log.Fatalf("XDP not supported on this kernel") 106 | } 107 | 108 | if err != nil { 109 | log.Fatalf("Error checking XDP support: %v", err) 110 | } 111 | 112 | // Attach count_packets to the network interface ingress, uses BPF_XDP 113 | // NOTE: no egress support yet for BPF_XDP path 114 | // NOTE: BPF_LINK_CREATE for XDP requires v5.9 kernel, but might work with older RHEL kernels 115 | l, err = link.AttachXDP(link.XDPOptions{ 116 | Program: objs.XdpCountPackets, 117 | Interface: iface.Index, 118 | Flags: xdpAttachFlags, 119 | }) 120 | if err != nil { 121 | log.Fatalf("Error attaching %q XDP ingress: %v", *ifname, err) 122 | } 123 | 124 | links = append(links, l) 125 | 126 | log.Printf("Starting on interface %q using XDP (eXpress Data Path) eBPF mode", *ifname) 127 | log.Printf("Due to XDP mode, egress statistics are not available. Upon program exit, interface reset may happen on some cards.") 128 | 129 | return links 130 | } 131 | 132 | // startTC attaches an eBPF program to a network interface for packet counting using 133 | // the Traffic Control (TC) eBPF mode. The function checks if the TC eBPF mode is 134 | // supported by the kernel. If supported, it attaches the program to both the ingress 135 | // and egress paths of the specified network interface. Note that TC eBPF mode requires 136 | // at least a v6.6 kernel. 137 | // 138 | // Parameters: 139 | // 140 | // objs counterObjects: Contains the eBPF programs, including the TC program to be 141 | // attached. 142 | // iface *net.Interface: The network interface to which the TC program should be 143 | // attached. 144 | // links []link.Link: A slice of existing link.Link objects to which the newly 145 | // attached TC links will be appended. 146 | // 147 | // Returns: 148 | // 149 | // []link.Link: The updated slice of link.Link objects, now including the newly 150 | // attached TC links. 151 | func startTC(objs counterObjects, iface *net.Interface, links []link.Link) []link.Link { 152 | var l link.Link 153 | 154 | err := features.HaveProgramType(ebpf.SchedACT) 155 | if errors.Is(err, ebpf.ErrNotSupported) { 156 | log.Fatalf("SchedACT not supported on this kernel") 157 | } 158 | 159 | if err != nil { 160 | log.Fatalf("Error checking SchedACT support: %v", err) 161 | } 162 | 163 | // NOTE: BPF_TCX_INGRESS and BPF_TCX_EGRESS require v6.6 kernel 164 | // Attach count_packets to the network interface ingress, uses BPF_TCX_INGRESS 165 | l, err = link.AttachTCX(link.TCXOptions{ 166 | Program: objs.TcCountPackets, 167 | Attach: ebpf.AttachTCXIngress, 168 | Interface: iface.Index, 169 | }) 170 | if err != nil { 171 | log.Fatalf("Error attaching %q TCX ingress: %v", *ifname, err) 172 | } 173 | 174 | links = append(links, l) 175 | 176 | // Attach count_packets to the network interface egresss, uses BPF_TCX_EGRESS 177 | l, err = link.AttachTCX(link.TCXOptions{ 178 | Program: objs.TcCountPackets, 179 | Attach: ebpf.AttachTCXEgress, 180 | Interface: iface.Index, 181 | }) 182 | if err != nil { 183 | log.Fatalf("Error attaching %q TCX egress: %v", *ifname, err) 184 | } 185 | 186 | links = append(links, l) 187 | 188 | log.Printf("Starting on interface %q using TC (Traffic Control) eBPF mode", *ifname) 189 | 190 | return links 191 | } 192 | 193 | // startCgroup attaches eBPF programs to a specified cgroup for monitoring and control of socket 194 | // creation and packet ingress/egress. 195 | // 196 | // This function checks if the CGroupSKB program type is supported by the kernel. If supported, 197 | // it attaches the provided eBPF programs to the specified cgroup path, facilitating monitoring 198 | // of socket creation and ingress/egress traffic within the cgroup. 199 | // 200 | // Parameters: 201 | // 202 | // objs counterObjects: Contains the eBPF programs, including those for socket creation and 203 | // ingress/egress packet handling. 204 | // cgroupPath string: The filesystem path to the target cgroup where the eBPF programs will be 205 | // attached. 206 | // links []link.Link: A slice of existing link.Link objects, to which the newly attached cgroup 207 | // links will be appended. 208 | // 209 | // Returns: 210 | // 211 | // []link.Link: The updated slice of link.Link objects, now including the newly attached cgroup 212 | // links. 213 | func startCgroup(objs counterObjects, cgroupPath string, links []link.Link) []link.Link { 214 | var l link.Link 215 | 216 | err := features.HaveProgramType(ebpf.CGroupSKB) 217 | if errors.Is(err, ebpf.ErrNotSupported) { 218 | log.Fatalf("CgroupSKB not supported on this kernel") 219 | } 220 | 221 | if err != nil { 222 | log.Fatalf("Error checking CGroupSKB support: %v", err) 223 | } 224 | 225 | l, err = link.AttachCgroup(link.CgroupOptions{ 226 | Program: objs.CgroupSockCreate, 227 | Attach: ebpf.AttachCGroupInetSockCreate, 228 | Path: cgroupPath, 229 | }) 230 | if err != nil { 231 | log.Fatalf("Error attaching CgroupSockCreate to %s: %v", cgroupPath, err) 232 | } 233 | 234 | links = append(links, l) 235 | 236 | l, err = link.AttachCgroup(link.CgroupOptions{ 237 | Program: objs.CgroupSkbIngress, 238 | Attach: ebpf.AttachCGroupInetIngress, 239 | Path: cgroupPath, 240 | }) 241 | if err != nil { 242 | log.Fatalf("Error attaching CgroupSkbIngress to %s: %v", cgroupPath, err) 243 | } 244 | 245 | links = append(links, l) 246 | 247 | l, err = link.AttachCgroup(link.CgroupOptions{ 248 | Program: objs.CgroupSkbEgress, 249 | Attach: ebpf.AttachCGroupInetEgress, 250 | Path: cgroupPath, 251 | }) 252 | if err != nil { 253 | log.Fatalf("Error attaching CgroupSkbEgress to %s: %v", cgroupPath, err) 254 | } 255 | 256 | links = append(links, l) 257 | 258 | log.Printf("Starting on CGroup %s", cgroupPath) 259 | 260 | return links 261 | } 262 | 263 | // startCGroupTrace attaches a raw tracepoint eBPF program to a kernel tracepoint. 264 | // 265 | // This function checks if the RawTracepoint program type is supported by the kernel. 266 | // If supported, it attaches the TraceCgroupMkdir eBPF program to the "cgroup_mkdir" tracepoint. 267 | // 268 | // Parameters: 269 | // 270 | // objs cgroupObjects: Contains the eBPF programs, including the TraceCgroupMkdir program to be attached. 271 | // links []link.Link: A slice of existing link.Link objects to which the newly attached tracepoint link will be appended. 272 | // 273 | // Returns: 274 | // 275 | // []link.Link: The updated slice of link.Link objects, now including the newly attached tracepoint link. 276 | func startCGroupTrace(objs cgroupObjects, links []link.Link) []link.Link { 277 | var l link.Link 278 | 279 | err := features.HaveProgramType(ebpf.RawTracepoint) 280 | if errors.Is(err, ebpf.ErrNotSupported) { 281 | log.Printf("RawTracepoint not supported on this kernel") 282 | 283 | return links 284 | } 285 | 286 | l, err = link.AttachRawTracepoint(link.RawTracepointOptions{ 287 | Program: objs.TraceCgroupMkdir, 288 | Name: "cgroup_mkdir", 289 | }) 290 | if err != nil { 291 | log.Fatalf("Error attaching TraceCgroupMkdirSignal: %v", err) 292 | } 293 | 294 | links = append(links, l) 295 | 296 | return links 297 | } 298 | -------------------------------------------------------------------------------- /bpf/counter.bpf.c: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2024 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | //go:build ignore 23 | 24 | #include "vmlinux.h" 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #include "counter.h" 32 | 33 | // Map key struct for IP traffic 34 | typedef struct statkey_t { 35 | struct in6_addr srcip; // source IPv6 address 36 | struct in6_addr dstip; // destination IPv6 address 37 | __u64 cgroupid; // cgroup ID 38 | char comm[TASK_COMM_LEN]; // process command 39 | pid_t pid; // process ID 40 | __u16 src_port; // source port 41 | __u16 dst_port; // destination port 42 | __u8 proto; // transport protocol 43 | } statkey; 44 | 45 | // Map value struct with counters 46 | typedef struct statvalue_t { 47 | __u64 packets; // packets ingress + egress 48 | __u64 bytes; // bytes ingress + egress 49 | } statvalue; 50 | 51 | // Map definition 52 | struct { 53 | __uint(type, BPF_MAP_TYPE_LRU_HASH); // LRU hash requires 4.10 kernel 54 | __uint(max_entries, MAX_ENTRIES); 55 | __type(key, statkey); 56 | __type(value, statvalue); 57 | } pkt_count SEC(".maps"); 58 | 59 | typedef struct sockinfo_t { 60 | __u8 comm[TASK_COMM_LEN]; 61 | pid_t pid; 62 | } sockinfo; 63 | 64 | struct { 65 | __uint(type, BPF_MAP_TYPE_LRU_HASH); // LRU hash requires 4.10 kernel 66 | __uint(max_entries, MAX_ENTRIES); 67 | __type(key, __u64); 68 | __type(value, sockinfo); 69 | } sock_info SEC(".maps"); 70 | 71 | // IPv4-mapped IPv6 address prefix (for V4MAPPED conversion) 72 | static const __u8 ip4in6[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}; 73 | 74 | /** 75 | * Process an IPv4 packet and populate the key with the relevant information. 76 | * 77 | * @param ip4 pointer to the start of the IPv4 header 78 | * @param data_end pointer to the end of the packet data 79 | * @param key pointer to the statkey structure to be populated 80 | * 81 | * @return OK if the packet was processed successfully, NOK otherwise 82 | * 83 | * @throws none 84 | */ 85 | static inline __attribute__((always_inline)) int 86 | process_ip4(struct iphdr *ip4, void *data_end, statkey *key) { 87 | // validate IPv4 size 88 | if ((void *)ip4 + sizeof(*ip4) > data_end) { 89 | return NOK; 90 | } 91 | 92 | // convert to V4MAPPED address 93 | __builtin_memcpy(key->srcip.s6_addr, ip4in6, sizeof(ip4in6)); 94 | __builtin_memcpy(key->srcip.s6_addr + sizeof(ip4in6), &ip4->saddr, 95 | sizeof(ip4->saddr)); 96 | 97 | // convert to V4MAPPED address 98 | __builtin_memcpy(key->dstip.s6_addr, ip4in6, sizeof(ip4in6)); 99 | __builtin_memcpy(key->dstip.s6_addr + sizeof(ip4in6), &ip4->daddr, 100 | sizeof(ip4->daddr)); 101 | 102 | key->proto = ip4->protocol; 103 | 104 | switch (ip4->protocol) { 105 | case IPPROTO_TCP: { 106 | struct tcphdr *tcp = (void *)ip4 + sizeof(*ip4); 107 | 108 | // validate TCP size 109 | if ((void *)tcp + sizeof(*tcp) > data_end) { 110 | return NOK; 111 | } 112 | 113 | key->src_port = bpf_ntohs(tcp->source); 114 | key->dst_port = bpf_ntohs(tcp->dest); 115 | 116 | break; 117 | } 118 | case IPPROTO_UDP: { 119 | struct udphdr *udp = (void *)ip4 + sizeof(*ip4); 120 | 121 | // validate UDP size 122 | if ((void *)udp + sizeof(*udp) > data_end) { 123 | return NOK; 124 | } 125 | 126 | key->src_port = bpf_ntohs(udp->source); 127 | key->dst_port = bpf_ntohs(udp->dest); 128 | 129 | break; 130 | } 131 | case IPPROTO_ICMP: { 132 | struct icmphdr *icmp = (void *)ip4 + sizeof(*ip4); 133 | 134 | // validate ICMP size 135 | if ((void *)icmp + sizeof(*icmp) > data_end) { 136 | return NOK; 137 | } 138 | 139 | // store ICMP type in src port 140 | key->src_port = icmp->type; 141 | // store ICMP code in dst port 142 | key->dst_port = icmp->code; 143 | 144 | break; 145 | } 146 | } 147 | 148 | return OK; 149 | } 150 | 151 | /** 152 | * Process an IPv6 packet and extract relevant information to populate the key. 153 | * 154 | * @param ip6 pointer to the start of the IPv6 header 155 | * @param data_end pointer to the end of the packet data 156 | * @param key pointer to the statkey structure to be populated 157 | * 158 | * @return OK if the packet was successfully processed, NOK otherwise 159 | * 160 | * @throws none 161 | */ 162 | static inline __attribute__((always_inline)) int 163 | process_ip6(struct ipv6hdr *ip6, void *data_end, statkey *key) { 164 | // validate IPv6 size 165 | if ((void *)ip6 + sizeof(*ip6) > data_end) { 166 | return NOK; 167 | } 168 | 169 | // IPv6 copy of source IP, destination IP and transport protocol 170 | key->srcip = ip6->saddr; 171 | key->dstip = ip6->daddr; 172 | key->proto = ip6->nexthdr; 173 | 174 | switch (ip6->nexthdr) { 175 | case IPPROTO_TCP: { 176 | struct tcphdr *tcp = (void *)ip6 + sizeof(*ip6); 177 | 178 | // validate TCP size 179 | if ((void *)tcp + sizeof(*tcp) > data_end) { 180 | return NOK; 181 | } 182 | 183 | key->src_port = bpf_ntohs(tcp->source); 184 | key->dst_port = bpf_ntohs(tcp->dest); 185 | 186 | break; 187 | } 188 | case IPPROTO_UDP: { 189 | struct udphdr *udp = (void *)ip6 + sizeof(*ip6); 190 | 191 | // validate UDP size 192 | if ((void *)udp + sizeof(*udp) > data_end) { 193 | return NOK; 194 | } 195 | 196 | key->src_port = bpf_ntohs(udp->source); 197 | key->dst_port = bpf_ntohs(udp->dest); 198 | 199 | break; 200 | } 201 | case IPPROTO_ICMPV6: { 202 | struct icmp6hdr *icmp = (void *)ip6 + sizeof(*ip6); 203 | 204 | // validate ICMPv6 size 205 | if ((void *)icmp + sizeof(*icmp) > data_end) { 206 | return NOK; 207 | } 208 | 209 | // store ICMP type in src port 210 | key->src_port = icmp->icmp6_type; 211 | // store ICMP code in dst port 212 | key->dst_port = icmp->icmp6_code; 213 | 214 | break; 215 | } 216 | } 217 | 218 | return OK; 219 | } 220 | 221 | /** 222 | * Process an Ethernet packet and populate the key with the relevant 223 | * information. 224 | * 225 | * @param data pointer to the start of the packet data 226 | * @param data_end pointer to the end of the packet data 227 | * @param pkt_len length of the packet 228 | * 229 | * @return void 230 | * 231 | * @throws none 232 | */ 233 | static inline __attribute__((always_inline)) void 234 | process_eth(void *data, void *data_end, __u64 pkt_len) { 235 | struct ethhdr *eth = data; 236 | 237 | // validate Ethernet size 238 | if ((void *)eth + sizeof(*eth) > data_end) { 239 | // bpf_printk("size validation failure"); 240 | return; 241 | } 242 | 243 | // initialize key 244 | statkey key; 245 | __builtin_memset(&key, 0, sizeof(key)); 246 | 247 | // process only IPv4 and IPv6 248 | switch (bpf_ntohs(eth->h_proto)) { 249 | case ETH_P_IP: { 250 | struct iphdr *ip4 = (void *)eth + sizeof(*eth); 251 | 252 | if (process_ip4(ip4, data_end, &key) == NOK) { 253 | return; 254 | } 255 | 256 | break; 257 | } 258 | case ETH_P_IPV6: { 259 | struct ipv6hdr *ip6 = (void *)eth + sizeof(*eth); 260 | 261 | if (process_ip6(ip6, data_end, &key) == NOK) { 262 | return; 263 | } 264 | 265 | break; 266 | } 267 | default: 268 | // bpf_printk("wrong packet type: %d", eth->h_proto); 269 | return; 270 | } 271 | 272 | // lookup value in hash 273 | statvalue *val = (statvalue *)bpf_map_lookup_elem(&pkt_count, &key); 274 | if (val) { 275 | // atomic XADD, doesn't need bpf_spin_lock() 276 | __sync_fetch_and_add(&val->packets, 1); 277 | __sync_fetch_and_add(&val->bytes, pkt_len); 278 | } else { 279 | statvalue initval = {.packets = 1, .bytes = pkt_len}; 280 | 281 | bpf_map_update_elem(&pkt_count, &key, &initval, BPF_NOEXIST); 282 | } 283 | } 284 | 285 | /** 286 | * Process a socket buffer and extract relevant information to populate the key. 287 | * 288 | * @param skb pointer to the socket buffer 289 | * 290 | * @return none 291 | * 292 | * @throws none 293 | * 294 | * This function is called by the BPF program for each socket buffer received. 295 | * It extracts relevant information from the socket buffer (PID, command name, 296 | * src/dst IP, src/dst port, protocol) and stores it in the key. It then looks 297 | * up the value in the packet count hash and increments the packet count and 298 | * byte count if the key is found. If the key is not found, it creates a new 299 | * entry with the packet count and byte count set to 1. 300 | */ 301 | static inline __attribute__((always_inline)) void 302 | process_cgroup_skb(struct __sk_buff *skb) { 303 | void *data = (void *)(long)skb->data; 304 | void *data_end = (void *)(long)skb->data_end; 305 | __u64 pkt_len = skb->len; 306 | 307 | // initialize key 308 | statkey key; 309 | __builtin_memset(&key, 0, sizeof(key)); 310 | 311 | switch (bpf_ntohs(skb->protocol)) { 312 | case ETH_P_IP: { 313 | struct iphdr *ip4 = data; 314 | 315 | if (process_ip4(ip4, data_end, &key) == NOK) { 316 | return; 317 | } 318 | 319 | break; 320 | } 321 | case ETH_P_IPV6: { 322 | struct ipv6hdr *ip6 = data; 323 | 324 | if (process_ip6(ip6, data_end, &key) == NOK) { 325 | return; 326 | } 327 | 328 | break; 329 | } 330 | default: 331 | // bpf_printk("wrong packet type: %d", skb->protocol); 332 | return; 333 | } 334 | 335 | __u64 cookie = bpf_get_socket_cookie(skb); 336 | sockinfo *ski = bpf_map_lookup_elem(&sock_info, &cookie); 337 | if (ski) { 338 | key.pid = ski->pid; 339 | __builtin_memcpy(key.comm, ski->comm, sizeof(key.comm)); 340 | } 341 | 342 | statvalue *val = (statvalue *)bpf_map_lookup_elem(&pkt_count, &key); 343 | if (val) { 344 | // atomic XADD, doesn't need bpf_spin_lock() 345 | __sync_fetch_and_add(&val->packets, 1); 346 | __sync_fetch_and_add(&val->bytes, pkt_len); 347 | } else { 348 | statvalue initval = {.packets = 1, .bytes = pkt_len}; 349 | 350 | bpf_map_update_elem(&pkt_count, &key, &initval, BPF_NOEXIST); 351 | } 352 | } 353 | 354 | /** 355 | * Process the packet for traffic control and take necessary actions. 356 | * 357 | * @param skb pointer to the packet buffer 358 | * 359 | * @return TC_ACT_UNSPEC 360 | * 361 | * @throws none 362 | */ 363 | static inline __attribute__((always_inline)) void 364 | tc_process_packet(struct __sk_buff *skb) { 365 | void *data = (void *)(long)skb->data; 366 | void *data_end = (void *)(long)skb->data_end; 367 | 368 | process_eth(data, data_end, skb->len); 369 | } 370 | 371 | /** 372 | * Process the packet for XDP (eXpress Data Path) and take necessary actions. 373 | * 374 | * @param ctx pointer to the XDP context 375 | * 376 | * @return XDP_PASS 377 | * 378 | * @throws none 379 | */ 380 | static inline __attribute__((always_inline)) void 381 | xdp_process_packet(struct xdp_md *xdp) { 382 | void *data = (void *)(long)xdp->data; 383 | void *data_end = (void *)(long)xdp->data_end; 384 | 385 | process_eth(data, data_end, data_end - data); 386 | } 387 | 388 | /** 389 | * This function is a BPF program entry point for processing packets using 390 | * XDP (eXpress Data Path). It invokes the xdp_process_packet function to 391 | * handle the packet specified by the xdp parameter. 392 | * 393 | * @param xdp pointer to the XDP context 394 | * 395 | * @return XDP_PASS to indicate that the packet should be passed to the 396 | * next processing stage in the network stack 397 | * 398 | * @throws none 399 | */ 400 | SEC("xdp") 401 | int xdp_count_packets(struct xdp_md *xdp) { 402 | xdp_process_packet(xdp); 403 | 404 | return XDP_PASS; 405 | } 406 | 407 | /** 408 | * Process a packet for Traffic Control and update statistics. 409 | * 410 | * This function is a BPF program entry point for packet processing using 411 | * Traffic Control (TC) hooks. It invokes the tc_process_packet function 412 | * to handle the packet specified by the skb parameter. 413 | * 414 | * @param skb pointer to the packet buffer 415 | * 416 | * @return TC_ACT_UNSPEC to indicate no specific TC action is taken 417 | * 418 | * @throws none 419 | */ 420 | SEC("tc") 421 | int tc_count_packets(struct __sk_buff *skb) { 422 | tc_process_packet(skb); 423 | 424 | return TC_ACT_UNSPEC; 425 | } 426 | 427 | /** 428 | * BPF program entry point for tracking socket creations in a CGroup. 429 | * 430 | * This program is attached to the sock_create hook in the CGroup 431 | * hierarchy. It records the PID and command name of the process 432 | * creating the socket in the sock_info map. 433 | * 434 | * @param sk pointer to the newly created socket 435 | * 436 | * @return ALLOW_SK to allow the socket creation 437 | * 438 | * @throws none 439 | */ 440 | SEC("cgroup/sock_create") 441 | int cgroup_sock_create(struct bpf_sock *sk) { 442 | __u64 cookie = bpf_get_socket_cookie(sk); 443 | sockinfo ski = { 444 | .pid = bpf_get_current_pid_tgid(), 445 | .comm = {0}, 446 | }; 447 | 448 | bpf_get_current_comm(ski.comm, sizeof(ski.comm)); 449 | 450 | bpf_map_update_elem(&sock_info, &cookie, &ski, BPF_ANY); 451 | 452 | return ALLOW_SK; 453 | } 454 | 455 | /** 456 | * BPF program entry point for tracking ingress traffic in a CGroup. 457 | * 458 | * This program is attached to the ingress hook in the CGroup hierarchy. 459 | * It records the packet and byte counters for the process associated with 460 | * the socket in the pkt_count map. 461 | * 462 | * @param skb pointer to the packet buffer 463 | * 464 | * @return ALLOW_PKT to allow the packet to be processed 465 | * 466 | * @throws none 467 | */ 468 | SEC("cgroup_skb/ingress") 469 | int cgroup_skb_ingress(struct __sk_buff *skb) { 470 | process_cgroup_skb(skb); 471 | 472 | return ALLOW_PKT; 473 | } 474 | 475 | /** 476 | * BPF program entry point for tracking egress traffic in a CGroup. 477 | * 478 | * This program is attached to the egress hook in the CGroup hierarchy. 479 | * It records the packet and byte counters for the process associated with 480 | * the socket in the pkt_count map. 481 | * 482 | * @param skb pointer to the packet buffer 483 | * 484 | * @return ALLOW_PKT to allow the packet to be processed 485 | * 486 | * @throws none 487 | */ 488 | SEC("cgroup_skb/egress") 489 | int cgroup_skb_egress(struct __sk_buff *skb) { 490 | process_cgroup_skb(skb); 491 | 492 | return ALLOW_PKT; 493 | } 494 | 495 | /** 496 | * Process TCP socket information and populate the key structure with 497 | * extracted data. 498 | * 499 | * @param receive boolean flag indicating whether the socket is a receiver 500 | * @param sk pointer to the socket structure 501 | * @param key pointer to the statkey structure to be populated 502 | * @param pid process ID associated with the socket 503 | * 504 | * This function reads the socket's address family and based on whether it is 505 | * IPv4 or IPv6, it extracts the source and destination IP addresses and 506 | * ports. It also sets the protocol to TCP and assigns the provided process ID 507 | * to the key. 508 | * 509 | * The function handles both IPv4 and IPv6 addresses by converting them to an 510 | * IPv6-mapped format for uniformity. 511 | * 512 | * @throws none 513 | */ 514 | static inline __attribute__((always_inline)) void 515 | process_tcp(bool receive, struct sock *sk, statkey *key, pid_t pid) { 516 | __u16 family = BPF_CORE_READ(sk, __sk_common.skc_family); 517 | 518 | switch (family) { 519 | case AF_INET: { 520 | // convert to V4MAPPED address 521 | __be32 ip4_src = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr); 522 | key->srcip.s6_addr16[5] = bpf_htons(0xffff); 523 | __builtin_memcpy(&key->srcip.s6_addr32[3], &ip4_src, sizeof(ip4_src)); 524 | 525 | // convert to V4MAPPED address 526 | __be32 ip4_dst = BPF_CORE_READ(sk, __sk_common.skc_daddr); 527 | key->dstip.s6_addr16[5] = bpf_htons(0xffff); 528 | __builtin_memcpy(&key->dstip.s6_addr32[3], &ip4_dst, sizeof(ip4_dst)); 529 | 530 | break; 531 | } 532 | case AF_INET6: { 533 | BPF_CORE_READ_INTO(&key->srcip, sk, __sk_common.skc_v6_rcv_saddr); 534 | BPF_CORE_READ_INTO(&key->dstip, sk, __sk_common.skc_v6_daddr); 535 | 536 | break; 537 | } 538 | default: { 539 | return; 540 | } 541 | } 542 | 543 | __u16 sport = BPF_CORE_READ(sk, __sk_common.skc_num); 544 | if (sport == 0) { 545 | struct inet_sock *isk = (struct inet_sock *)sk; 546 | BPF_CORE_READ_INTO(&sport, isk, inet_sport); 547 | } 548 | key->src_port = sport; 549 | key->dst_port = bpf_ntohs(BPF_CORE_READ(sk, __sk_common.skc_dport)); 550 | 551 | key->proto = IPPROTO_TCP; 552 | key->pid = pid; 553 | 554 | /* we need to swap the source and destination IP addresses and ports */ 555 | if (receive) { 556 | struct in6_addr tmp_ip = key->srcip; 557 | key->srcip = key->dstip; 558 | key->dstip = tmp_ip; 559 | 560 | __u16 tmp_port = key->src_port; 561 | key->src_port = key->dst_port; 562 | key->dst_port = tmp_port; 563 | } 564 | } 565 | 566 | /** 567 | * Process UDP socket information from a sk_buff and populate the key 568 | * structure. 569 | * 570 | * @param receive boolean flag indicating whether the socket is a receiver 571 | * @param skb pointer to the socket buffer containing the UDP packet 572 | * @param key pointer to the statkey structure to be populated 573 | * @param pid process ID associated with the packet 574 | * 575 | * This function extracts source and destination IP addresses and ports from 576 | * the UDP packet, taking into account both IPv4 and IPv6 headers. It stores 577 | * these details in the provided statkey structure, along with the protocol 578 | * type set to UDP and the associated process ID. 579 | * 580 | * @throws none 581 | */ 582 | static inline __attribute__((always_inline)) void 583 | process_udp_recv(bool receive, struct sk_buff *skb, statkey *key, pid_t pid) { 584 | struct udphdr *udphdr = 585 | (struct udphdr *)(BPF_CORE_READ(skb, head) + 586 | BPF_CORE_READ(skb, transport_header)); 587 | 588 | __u16 proto = BPF_CORE_READ(skb, protocol); 589 | 590 | switch (bpf_ntohs(proto)) { 591 | case ETH_P_IP: { 592 | struct iphdr *iphdr = (struct iphdr *)(BPF_CORE_READ(skb, head) + 593 | BPF_CORE_READ(skb, network_header)); 594 | 595 | // convert to V4MAPPED address 596 | __be32 ip4_src = BPF_CORE_READ(iphdr, saddr); 597 | key->srcip.s6_addr16[5] = bpf_htons(0xffff); 598 | __builtin_memcpy(&key->srcip.s6_addr32[3], &ip4_src, sizeof(ip4_src)); 599 | 600 | // convert to V4MAPPED address 601 | __be32 ip4_dst = BPF_CORE_READ(iphdr, daddr); 602 | key->dstip.s6_addr16[5] = bpf_htons(0xffff); 603 | __builtin_memcpy(&key->dstip.s6_addr32[3], &ip4_dst, sizeof(ip4_dst)); 604 | break; 605 | } 606 | case ETH_P_IPV6: { 607 | struct ipv6hdr *iphdr = 608 | (struct ipv6hdr *)(BPF_CORE_READ(skb, head) + 609 | BPF_CORE_READ(skb, network_header)); 610 | 611 | BPF_CORE_READ_INTO(&key->srcip, iphdr, saddr); 612 | BPF_CORE_READ_INTO(&key->dstip, iphdr, daddr); 613 | 614 | break; 615 | } 616 | default: 617 | return; 618 | } 619 | 620 | key->src_port = bpf_ntohs(BPF_CORE_READ(udphdr, source)); 621 | key->dst_port = bpf_ntohs(BPF_CORE_READ(udphdr, dest)); 622 | 623 | key->proto = IPPROTO_UDP; 624 | key->pid = pid; 625 | } 626 | 627 | /** 628 | * Process an ICMPv4 packet and populate the key with the relevant information. 629 | * 630 | * @param skb pointer to the socket buffer containing the ICMPv4 packet 631 | * @param key pointer to the statkey structure to be populated 632 | * @param pid process ID associated with the packet 633 | * 634 | * This function extracts source and destination IP addresses and ICMP type 635 | * and code from the ICMPv4 packet, taking into account the IPv4 header. It 636 | * stores these details in the provided statkey structure, along with the 637 | * protocol type set to ICMPv4 and the associated process ID. 638 | * 639 | * @throws none 640 | */ 641 | static inline __attribute__((always_inline)) size_t 642 | process_icmp4(struct sk_buff *skb, statkey *key, pid_t pid) { 643 | struct icmphdr *icmphdr = 644 | (struct icmphdr *)(BPF_CORE_READ(skb, head) + 645 | BPF_CORE_READ(skb, transport_header)); 646 | struct iphdr *iphdr = (struct iphdr *)(BPF_CORE_READ(skb, head) + 647 | BPF_CORE_READ(skb, network_header)); 648 | 649 | // convert to V4MAPPED address 650 | __be32 ip4_src = BPF_CORE_READ(iphdr, saddr); 651 | key->srcip.s6_addr16[5] = bpf_htons(0xffff); 652 | __builtin_memcpy(&key->srcip.s6_addr32[3], &ip4_src, sizeof(ip4_src)); 653 | 654 | // convert to V4MAPPED address 655 | __be32 ip4_dst = BPF_CORE_READ(iphdr, daddr); 656 | key->dstip.s6_addr16[5] = bpf_htons(0xffff); 657 | __builtin_memcpy(&key->dstip.s6_addr32[3], &ip4_dst, sizeof(ip4_dst)); 658 | 659 | // store ICMP type in src port 660 | key->src_port = BPF_CORE_READ(icmphdr, type); 661 | // store ICMP code in dst port 662 | key->dst_port = BPF_CORE_READ(icmphdr, code); 663 | 664 | key->proto = IPPROTO_ICMP; 665 | key->pid = pid; 666 | 667 | size_t msglen = bpf_ntohs(BPF_CORE_READ(iphdr, tot_len)) - 668 | BPF_CORE_READ_BITFIELD_PROBED(iphdr, ihl) * 4; 669 | 670 | return msglen; 671 | } 672 | 673 | /** 674 | * Process an ICMPv6 packet and populate the key with the relevant information. 675 | * 676 | * @param skb pointer to the socket buffer containing the ICMPv6 packet 677 | * @param key pointer to the statkey structure to be populated 678 | * @param pid process ID associated with the packet 679 | * 680 | * This function extracts source and destination IP addresses and ICMPv6 type 681 | * and code from the ICMPv6 packet, taking into account the IPv6 header. It 682 | * stores these details in the provided statkey structure, along with the 683 | * protocol type set to ICMPv6 and the associated process ID. It also returns 684 | * the length of the ICMPv6 message payload. 685 | * 686 | * @return the length of the ICMPv6 message payload 687 | * @throws none 688 | */ 689 | 690 | static inline __attribute__((always_inline)) size_t 691 | process_icmp6(struct sk_buff *skb, statkey *key, pid_t pid) { 692 | struct icmp6hdr *icmphdr = 693 | (struct icmp6hdr *)(BPF_CORE_READ(skb, head) + 694 | BPF_CORE_READ(skb, transport_header)); 695 | 696 | struct ipv6hdr *iphdr = 697 | (struct ipv6hdr *)(BPF_CORE_READ(skb, head) + 698 | BPF_CORE_READ(skb, network_header)); 699 | 700 | BPF_CORE_READ_INTO(&key->srcip, iphdr, saddr); 701 | BPF_CORE_READ_INTO(&key->dstip, iphdr, daddr); 702 | 703 | // store ICMP type in src port 704 | key->src_port = BPF_CORE_READ(icmphdr, icmp6_type); 705 | // store ICMP code in dst port 706 | key->dst_port = BPF_CORE_READ(icmphdr, icmp6_code); 707 | 708 | key->proto = IPPROTO_ICMPV6; 709 | key->pid = pid; 710 | 711 | size_t msglen = bpf_ntohs(BPF_CORE_READ(iphdr, payload_len)); 712 | 713 | return msglen; 714 | } 715 | 716 | /** 717 | * Process UDP socket information from a sk_buff and populate the key 718 | * structure. 719 | * 720 | * @param skb pointer to the socket buffer containing the UDP packet 721 | * @param key pointer to the statkey structure to be populated 722 | * @param pid process ID associated with the packet 723 | * 724 | * This function extracts source and destination IP addresses and ports from 725 | * the UDP packet, taking into account both IPv4 and IPv6 headers. It stores 726 | * these details in the provided statkey structure, along with the protocol 727 | * type set to UDP and the associated process ID. It also returns the length 728 | * of the UDP message. 729 | * 730 | * @throws none 731 | */ 732 | static inline __attribute__((always_inline)) size_t 733 | process_udp_send(struct sk_buff *skb, statkey *key, pid_t pid) { 734 | struct udphdr *udphdr = 735 | (struct udphdr *)(BPF_CORE_READ(skb, head) + 736 | BPF_CORE_READ(skb, transport_header)); 737 | 738 | process_udp_recv(false, skb, key, pid); 739 | size_t msglen = BPF_CORE_READ(udphdr, len); 740 | 741 | return msglen; 742 | } 743 | 744 | #if 0 745 | /** 746 | * Process raw ICMP socket information for IPv4 and populate the key structure. 747 | * 748 | * @param sk pointer to the socket structure 749 | * @param msg pointer to the message header structure containing the packet 750 | * @param key pointer to the statkey structure to be populated 751 | * @param pid process ID associated with the packet 752 | * 753 | * This function extracts source and destination IPv4 addresses and ICMP type 754 | * and code from the raw socket message. It populates the provided statkey 755 | * structure with these details, converting IPv4 addresses to IPv6-mapped 756 | * format. The function only processes messages with the ICMP protocol. 757 | * 758 | * @throws none 759 | */ 760 | 761 | static inline __attribute__((always_inline)) void process_raw_sendmsg4(struct sock *sk, struct msghdr *msg, 762 | statkey *key, pid_t pid) { 763 | struct inet_sock *isk = (struct inet_sock *)sk; 764 | struct sockaddr_in *sin = (struct sockaddr_in *)BPF_CORE_READ(msg, msg_name); 765 | 766 | // raw sockets have the protocol number in inet_num 767 | __u16 proto = BPF_CORE_READ(isk, inet_num); 768 | if (proto != IPPROTO_ICMP) { 769 | return; 770 | } 771 | 772 | // convert to V4MAPPED address 773 | __be32 ip4_src = BPF_CORE_READ(isk, inet_saddr); 774 | key->srcip.s6_addr16[5] = bpf_htons(0xffff); 775 | __builtin_memcpy(&key->srcip.s6_addr32[3], &ip4_src, sizeof(ip4_src)); 776 | 777 | // convert to V4MAPPED address 778 | __be32 ip4_dst = BPF_CORE_READ(sin, sin_addr.s_addr); 779 | key->dstip.s6_addr16[5] = bpf_htons(0xffff); 780 | __builtin_memcpy(&key->dstip.s6_addr32[3], &ip4_dst, sizeof(ip4_dst)); 781 | 782 | struct iovec *iov = (struct iovec *)BPF_CORE_READ(msg, msg_iter.__iov); 783 | struct icmphdr *icmphdr = (struct icmphdr *)BPF_CORE_READ(iov, iov_base); 784 | 785 | // store ICMP type in src port 786 | key->src_port = BPF_CORE_READ(icmphdr, type); 787 | // store ICMP code in dst port 788 | key->dst_port = BPF_CORE_READ(icmphdr, code); 789 | 790 | key->proto = IPPROTO_ICMP; 791 | key->pid = pid; 792 | } 793 | #endif 794 | 795 | #if 0 796 | /** 797 | * Process raw ICMP socket information for IPv6 and populate the key structure. 798 | * 799 | * @param sk pointer to the socket structure 800 | * @param msg pointer to the message header structure containing the packet 801 | * @param key pointer to the statkey structure to be populated 802 | * @param pid process ID associated with the packet 803 | * 804 | * This function extracts source and destination IPv6 addresses and ICMPv6 type 805 | * and code from the raw socket message. It populates the provided statkey 806 | * structure with these details. The function only processes messages with the 807 | * ICMPv6 protocol. 808 | * 809 | * @throws none 810 | */ 811 | 812 | static inline __attribute__((always_inline)) void process_raw_sendmsg6(struct sock *sk, struct msghdr *msg, 813 | statkey *key, pid_t pid) { 814 | struct inet_sock *isk = (struct inet_sock *)sk; 815 | struct sockaddr_in6 *sin = 816 | (struct sockaddr_in6 *)BPF_CORE_READ(msg, msg_name); 817 | 818 | // raw sockets have the protocol number in inet_num 819 | __u16 proto = BPF_CORE_READ(isk, inet_num); 820 | if (proto != IPPROTO_ICMPV6) { 821 | return; 822 | } 823 | 824 | BPF_CORE_READ_INTO(&key->srcip, isk, inet_saddr); 825 | BPF_CORE_READ_INTO(&key->dstip, sin, sin6_addr); 826 | 827 | struct iovec *iov = (struct iovec *)BPF_CORE_READ(msg, msg_iter.__iov); 828 | struct icmp6hdr *icmphdr = (struct icmp6hdr *)BPF_CORE_READ(iov, iov_base); 829 | 830 | // store ICMP type in src port 831 | key->src_port = BPF_CORE_READ(icmphdr, icmp6_type); 832 | // store ICMP code in dst port 833 | key->dst_port = BPF_CORE_READ(icmphdr, icmp6_code); 834 | 835 | key->proto = IPPROTO_ICMPV6; 836 | key->pid = pid; 837 | } 838 | #endif 839 | 840 | /** 841 | * Update the packet and byte counters for the given key in the packet count 842 | * map. If the key is not present, it is inserted with an initial value of 1 843 | * packet and the given size in bytes. If the key is already present, the 844 | * packet and byte counters are atomically incremented. 845 | * 846 | * @param key pointer to the statkey structure containing the key to be 847 | * updated 848 | * @param size size of the packet to be counted 849 | * 850 | * @throws none 851 | */ 852 | static inline __attribute__((always_inline)) void update_val(statkey *key, 853 | size_t size) { 854 | // lookup value in hash 855 | statvalue *val = (statvalue *)bpf_map_lookup_elem(&pkt_count, key); 856 | if (val) { 857 | // atomic XADD, doesn't need bpf_spin_lock() 858 | __sync_fetch_and_add(&val->packets, 1); 859 | __sync_fetch_and_add(&val->bytes, size); 860 | } else { 861 | statvalue initval = {.packets = 1, .bytes = size}; 862 | 863 | bpf_map_update_elem(&pkt_count, key, &initval, BPF_NOEXIST); 864 | } 865 | } 866 | 867 | /** 868 | * Hook function for kprobe on tcp_sendmsg function. 869 | * 870 | * Populates the statkey structure with information from the UDP packet and 871 | * the process ID associated with the packet, and updates the packet and byte 872 | * counters in the packet count map. 873 | * 874 | * @param sk pointer to the socket structure 875 | * @param msg pointer to the msghdr structure 876 | * @param size size of the packet to be counted 877 | * 878 | * @return 0 879 | * 880 | * @throws none 881 | */ 882 | SEC("kprobe/tcp_sendmsg") 883 | int BPF_KPROBE(tcp_sendmsg, struct sock *sk, struct msghdr *msg, size_t size) { 884 | statkey key; 885 | __builtin_memset(&key, 0, sizeof(key)); 886 | 887 | pid_t pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF; 888 | if (pid > 0) { 889 | bpf_get_current_comm(&key.comm, sizeof(key.comm)); 890 | } 891 | 892 | key.cgroupid = bpf_get_current_cgroup_id(); 893 | 894 | process_tcp(false, sk, &key, pid); 895 | update_val(&key, size); 896 | 897 | return 0; 898 | } 899 | 900 | /** 901 | * Hook function for kprobe on tcp_cleanup_rbuf function. 902 | * 903 | * Populates the statkey structure with information from the socket and the 904 | * process ID associated with the socket, and updates the packet and byte 905 | * counters in the packet count map. 906 | * 907 | * @param sk pointer to the socket structure 908 | * @param copied size of the packet to be counted 909 | * 910 | * @return 0 911 | * 912 | * @throws none 913 | */ 914 | SEC("kprobe/tcp_cleanup_rbuf") 915 | int BPF_KPROBE(tcp_cleanup_rbuf, struct sock *sk, int copied) { 916 | if (copied <= 0) { 917 | return 0; 918 | } 919 | 920 | statkey key; 921 | __builtin_memset(&key, 0, sizeof(key)); 922 | 923 | pid_t pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF; 924 | if (pid > 0) { 925 | bpf_get_current_comm(&key.comm, sizeof(key.comm)); 926 | } 927 | 928 | key.cgroupid = bpf_get_current_cgroup_id(); 929 | 930 | process_tcp(true, sk, &key, pid); 931 | update_val(&key, copied); 932 | 933 | return 0; 934 | } 935 | 936 | /** 937 | * Hook function for kprobe on ip_send_skb function. 938 | * 939 | * Populates the statkey structure with information from the socket and the 940 | * process ID associated with the socket, and updates the packet and byte 941 | * counters in the packet count map. 942 | * 943 | * @param net pointer to the network namespace structure 944 | * @param skb pointer to the socket buffer 945 | * 946 | * @return 0 947 | * 948 | * @throws none 949 | */ 950 | SEC("kprobe/ip_send_skb") 951 | int BPF_KPROBE(ip_send_skb, struct net *net, struct sk_buff *skb) { 952 | __u16 protocol = BPF_CORE_READ(skb, protocol); 953 | if (protocol != IPPROTO_UDP) { 954 | return 0; 955 | } 956 | 957 | statkey key; 958 | __builtin_memset(&key, 0, sizeof(key)); 959 | 960 | pid_t pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF; 961 | if (pid > 0) { 962 | bpf_get_current_comm(&key.comm, sizeof(key.comm)); 963 | } 964 | 965 | key.cgroupid = bpf_get_current_cgroup_id(); 966 | 967 | size_t msglen = process_udp_send(skb, &key, pid); 968 | update_val(&key, msglen); 969 | 970 | return 0; 971 | } 972 | 973 | /** 974 | * Hook function for kprobe on skb_consume_udp function. 975 | * 976 | * Populates the statkey structure with information from the UDP packet and 977 | * the process ID associated with the packet, and updates the packet and byte 978 | * counters in the packet count map. 979 | * 980 | * @param sk pointer to the socket structure 981 | * @param skb pointer to the socket buffer containing the UDP packet 982 | * @param len length of the UDP message 983 | * 984 | * @return 0 985 | * 986 | * @throws none 987 | */ 988 | SEC("kprobe/skb_consume_udp") 989 | int BPF_KPROBE(skb_consume_udp, struct sock *sk, struct sk_buff *skb, int len) { 990 | statkey key; 991 | __builtin_memset(&key, 0, sizeof(key)); 992 | 993 | pid_t pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF; 994 | if (pid > 0) { 995 | bpf_get_current_comm(&key.comm, sizeof(key.comm)); 996 | } 997 | 998 | key.cgroupid = bpf_get_current_cgroup_id(); 999 | 1000 | process_udp_recv(true, skb, &key, pid); 1001 | update_val(&key, len); 1002 | 1003 | return 0; 1004 | } 1005 | 1006 | /** 1007 | * Hook function for kprobe on __icmp_send function. 1008 | * 1009 | * Populates the statkey structure with information from the ICMPv4 packet and 1010 | * the process ID associated with the packet, and updates the packet and byte 1011 | * counters in the packet count map. 1012 | * 1013 | * @param skb pointer to the socket buffer containing the ICMPv4 packet 1014 | * @param type type of ICMPv4 packet 1015 | * @param code code of ICMPv4 packet 1016 | * @param info additional information for the ICMPv4 packet 1017 | * @param opt pointer to the ip_options structure 1018 | * 1019 | * @return 0 1020 | * 1021 | * @throws none 1022 | */ 1023 | SEC("kprobe/__icmp_send") 1024 | int BPF_KPROBE(__icmp_send, struct sk_buff *skb, __u8 type, __u8 code, 1025 | __be32 info, const struct ip_options *opt) { 1026 | statkey key; 1027 | __builtin_memset(&key, 0, sizeof(key)); 1028 | 1029 | pid_t pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF; 1030 | if (pid > 0) { 1031 | bpf_get_current_comm(&key.comm, sizeof(key.comm)); 1032 | } 1033 | 1034 | key.cgroupid = bpf_get_current_cgroup_id(); 1035 | 1036 | size_t msglen = process_icmp4(skb, &key, pid); 1037 | update_val(&key, msglen); 1038 | 1039 | return 0; 1040 | } 1041 | 1042 | /** 1043 | * Hook function for kprobe on icmp6_send function. 1044 | * 1045 | * Populates the statkey structure with information from the ICMPv6 packet and 1046 | * the process ID associated with the packet, and updates the packet and byte 1047 | * counters in the packet count map. 1048 | * 1049 | * @param skb pointer to the socket buffer containing the ICMPv6 packet 1050 | * @param type type of ICMPv6 packet 1051 | * @param code code of ICMPv6 packet 1052 | * @param info additional information for the ICMPv6 packet 1053 | * 1054 | * @return 0 1055 | * 1056 | * @throws none 1057 | */ 1058 | SEC("kprobe/icmp6_send") 1059 | int BPF_KPROBE(icmp6_send, struct sk_buff *skb, __u8 type, __u8 code, 1060 | __u32 info) { 1061 | statkey key; 1062 | __builtin_memset(&key, 0, sizeof(key)); 1063 | 1064 | pid_t pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF; 1065 | if (pid > 0) { 1066 | bpf_get_current_comm(&key.comm, sizeof(key.comm)); 1067 | } 1068 | 1069 | key.cgroupid = bpf_get_current_cgroup_id(); 1070 | 1071 | size_t msglen = process_icmp6(skb, &key, pid); 1072 | update_val(&key, msglen); 1073 | 1074 | return 0; 1075 | } 1076 | 1077 | /** 1078 | * Hook function for kprobe on icmp_rcv function. 1079 | * 1080 | * Populates the statkey structure with information from the ICMP packet and 1081 | * the process ID associated with the packet, and updates the packet and byte 1082 | * counters in the packet count map. 1083 | * 1084 | * @param skb pointer to the socket buffer containing the ICMP packet 1085 | * 1086 | * @return 0 1087 | * 1088 | * @throws none 1089 | */ 1090 | SEC("kprobe/icmp_rcv") 1091 | int BPF_KPROBE(icmp_rcv, struct sk_buff *skb) { 1092 | statkey key; 1093 | __builtin_memset(&key, 0, sizeof(key)); 1094 | 1095 | pid_t pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF; 1096 | if (pid > 0) { 1097 | bpf_get_current_comm(&key.comm, sizeof(key.comm)); 1098 | } 1099 | 1100 | key.cgroupid = bpf_get_current_cgroup_id(); 1101 | 1102 | size_t msglen = process_icmp4(skb, &key, pid); 1103 | update_val(&key, msglen); 1104 | 1105 | return 0; 1106 | } 1107 | 1108 | /** 1109 | * Hook function for kprobe on icmpv6_rcv function. 1110 | * 1111 | * Populates the statkey structure with information from the ICMPv6 packet and 1112 | * the process ID associated with the packet, and updates the packet and byte 1113 | * counters in the packet count map. 1114 | * 1115 | * @param skb pointer to the socket buffer containing the ICMPv6 packet 1116 | * 1117 | * @return 0 1118 | * 1119 | * @throws none 1120 | */ 1121 | SEC("kprobe/icmpv6_rcv") 1122 | int BPF_KPROBE(icmpv6_rcv, struct sk_buff *skb) { 1123 | statkey key; 1124 | __builtin_memset(&key, 0, sizeof(key)); 1125 | 1126 | pid_t pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF; 1127 | if (pid > 0) { 1128 | bpf_get_current_comm(&key.comm, sizeof(key.comm)); 1129 | } 1130 | 1131 | key.cgroupid = bpf_get_current_cgroup_id(); 1132 | 1133 | size_t msglen = process_icmp6(skb, &key, pid); 1134 | update_val(&key, msglen); 1135 | 1136 | return 0; 1137 | } 1138 | 1139 | #if 0 1140 | /** 1141 | * Hook function for kprobe on raw_sendmsg function. 1142 | * 1143 | * Populates the statkey structure with information from the raw IPv4 packet and 1144 | * the process ID associated with the packet, and updates the packet and byte 1145 | * counters in the packet count map. 1146 | * 1147 | * @param sk pointer to the socket structure 1148 | * @param msg pointer to the msghdr structure 1149 | * @param len size of the message 1150 | * 1151 | * @return 0 1152 | * 1153 | * @throws none 1154 | */ 1155 | SEC("kprobe/raw_sendmsg") 1156 | int BPF_KPROBE(raw_sendmsg, struct sock *sk, struct msghdr *msg, size_t len) { 1157 | statkey key; 1158 | __builtin_memset(&key, 0, sizeof(key)); 1159 | 1160 | pid_t pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF; 1161 | if (pid > 0) { 1162 | bpf_get_current_comm(&key.comm, sizeof(key.comm)); 1163 | } 1164 | 1165 | key.cgroupid = bpf_get_current_cgroup_id(); 1166 | 1167 | process_raw_sendmsg4(sk, msg, &key, pid); 1168 | update_val(&key, len); 1169 | 1170 | return 0; 1171 | } 1172 | #endif 1173 | 1174 | #if 0 1175 | /** 1176 | * Hook function for kprobe on rawv6_sendmsg function. 1177 | * 1178 | * Populates the statkey structure with information from the raw IPv6 packet and 1179 | * the process ID associated with the packet, and updates the packet and byte 1180 | * counters in the packet count map. 1181 | * 1182 | * @param sk pointer to the socket structure 1183 | * @param msg pointer to the msghdr structure 1184 | * @param len size of the message 1185 | * 1186 | * @return 0 1187 | * 1188 | * @throws none 1189 | */ 1190 | SEC("kprobe/rawv6_sendmsg") 1191 | int BPF_KPROBE(rawv6_sendmsg, struct sock *sk, struct msghdr *msg, size_t len) { 1192 | statkey key; 1193 | __builtin_memset(&key, 0, sizeof(key)); 1194 | 1195 | pid_t pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF; 1196 | if (pid > 0) { 1197 | bpf_get_current_comm(&key.comm, sizeof(key.comm)); 1198 | } 1199 | 1200 | key.cgroupid = bpf_get_current_cgroup_id(); 1201 | 1202 | process_raw_sendmsg6(sk, msg, &key, pid); 1203 | update_val(&key, len); 1204 | 1205 | return 0; 1206 | } 1207 | #endif 1208 | 1209 | char __license[] SEC("license") = "Dual MIT/GPL"; 1210 | --------------------------------------------------------------------------------