Skip to content

Commit dae388c

Browse files
committed
Initial BPF_MAP_LOOKUP_BATCH support
1 parent 26711f7 commit dae388c

File tree

5 files changed

+202
-58
lines changed

5 files changed

+202
-58
lines changed

go.mod

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
module github.com/dkorunic/pktstat-bpf
22

3-
go 1.23.0
4-
5-
toolchain go1.24.1
3+
go 1.25
64

75
require (
8-
github.com/avast/retry-go/v4 v4.6.1
96
github.com/cilium/ebpf v0.19.0
107
github.com/gdamore/tcell/v2 v2.8.1
118
github.com/goccy/go-json v0.10.5

go.sum

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk=
2-
github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA=
31
github.com/cilium/ebpf v0.19.0 h1:Ro/rE64RmFBeA9FGjcTc+KmCeY6jXmryu6FfnzPRIao=
42
github.com/cilium/ebpf v0.19.0/go.mod h1:fLCgMo3l8tZmAdM3B2XqdFzXBpwkcSTroaVqN08OWVY=
5-
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6-
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
73
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
84
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
95
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
@@ -36,8 +32,6 @@ github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N
3632
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
3733
github.com/peterbourgon/ff/v4 v4.0.0-beta.1 h1:hV8qRu3V7YfiSMsBSfPfdcznAvPQd3jI5zDddSrDoUc=
3834
github.com/peterbourgon/ff/v4 v4.0.0-beta.1/go.mod h1:onQJUKipvCyFmZ1rIYwFAh1BhPOvftb1uhvSI7krNLc=
39-
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
40-
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
4135
github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb h1:n7UJ8X9UnrTZBYXnd1kAIBc067SWyuPIrsocjketYW8=
4236
github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY=
4337
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -46,8 +40,6 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
4640
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
4741
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
4842
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
49-
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
50-
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
5143
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
5244
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
5345
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -122,5 +114,3 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
122114
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
123115
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
124116
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
125-
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
126-
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

main.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ package main
2323

2424
import (
2525
"context"
26+
"errors"
2627
"fmt"
2728
"log"
2829
"net"
@@ -31,6 +32,7 @@ import (
3132
"syscall"
3233
"time"
3334

35+
"github.com/cilium/ebpf"
3436
"github.com/cilium/ebpf/link"
3537
"github.com/cilium/ebpf/rlimit"
3638
"github.com/hako/durafmt"
@@ -162,7 +164,12 @@ func main() {
162164

163165
m, err = processMap(objsCounter.PktCount, startTime, bitrateSort)
164166
if err != nil {
165-
log.Fatalf("Error reading eBPF map: %v", err)
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+
}
166173
}
167174

168175
if *jsonOutput {

map.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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 5.7 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+
}

output.go

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import (
2828
"strings"
2929
"time"
3030

31-
"github.com/avast/retry-go/v4"
3231
"github.com/cilium/ebpf"
3332
"github.com/goccy/go-json"
3433
)
@@ -39,8 +38,6 @@ const (
3938
Mbps = 1000 * Kbps
4039
Gbps = 1000 * Mbps
4140
Tbps = 1000 * Gbps
42-
43-
mapReadRetries = 3
4441
)
4542

4643
// processMap generates statEntry objects from an ebpf.Map using the provided start time.
@@ -49,48 +46,10 @@ const (
4946
//
5047
// m *ebpf.Map - the eb
5148
func processMap(m *ebpf.Map, start time.Time, sortFunc func([]statEntry)) ([]statEntry, error) {
52-
var (
53-
key counterStatkey
54-
val counterStatvalue
55-
)
56-
57-
dur := time.Since(start).Seconds()
58-
stats := make([]statEntry, 0, m.MaxEntries())
59-
60-
err := retry.Do(func() error {
61-
// reset to zero length
62-
stats = stats[:0]
63-
64-
iter := m.Iterate()
65-
66-
// build statEntry slice converting data where needed
67-
for iter.Next(&key, &val) {
68-
stats = append(stats, statEntry{
69-
SrcIP: bytesToAddr(key.Srcip.In6U.U6Addr8),
70-
DstIP: bytesToAddr(key.Dstip.In6U.U6Addr8),
71-
Proto: protoToString(key.Proto),
72-
SrcPort: key.SrcPort,
73-
DstPort: key.DstPort,
74-
Bytes: val.Bytes,
75-
Packets: val.Packets,
76-
Bitrate: 8 * float64(val.Bytes) / dur,
77-
Pid: key.Pid,
78-
Comm: bsliceToString(key.Comm[:]),
79-
CGroup: cGroupToPath(key.Cgroupid),
80-
})
81-
}
82-
83-
return iter.Err()
84-
},
85-
retry.Attempts(mapReadRetries),
86-
)
87-
if err != nil {
88-
return nil, err
89-
}
90-
49+
stats, err := listMap(m, start)
9150
sortFunc(stats)
9251

93-
return stats, nil
52+
return stats, err
9453
}
9554

9655
// bitrateSort sorts a slice of statEntry objects by their Bitrate field in descending order.

0 commit comments

Comments
 (0)