Skip to content

Commit 644392b

Browse files
committed
dropwatch: consolidate BPF/Go changes
- Rename drop_source to drop_reason in BPF/Go binding - Add device filter via load-time constant rewrite - Wire --device/--device-exclude flags to BPF constants - Add DROP_REASON_UNSUPPORT and rename filter variables - Propagate writer IO errors and stop mutating events - Set Source=client in handleEvent instead of socketWriter - Unify stack formatting via symbol.FormatStack - Capture subprocess output in exit error - Clean up DropWatchTracing schema - Adopt pcapfilter.Load (final API) Change-Id: Iafbc745ca16c31fb1b52580816a39bdc41f259af (cherry picked from commit d77559401b3a65ed76e0eea7adeb20075d4e70e5)
1 parent 26a6a0f commit 644392b

9 files changed

Lines changed: 181 additions & 40 deletions

File tree

bpf/dropwatch.c

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "bpf_net_namespace.h"
88
#include "bpf_netdevice.h"
99
#include "bpf_pcap_stub.h"
10+
#include "bpf_skb_filter.h"
1011
#include "vmlinux_net.h"
1112

1213
#define TYPE_TCP_COMMON_DROP 1
@@ -19,8 +20,9 @@
1920
#define SK_FL_TYPE_SHIFT 16
2021
#define SK_FL_TYPE_MASK 0xffff0000
2122

22-
/* Drop source (who triggered the kfree_skb) */
23-
#define DROP_SOURCE_UNKNOWN 0
23+
/* Drop reason (kernel skb_drop_reason enum; UNKNOWN until plumbed). */
24+
#define DROP_REASON_UNKNOWN 0
25+
#define DROP_REASON_UNSUPPORT 1
2426
#define PKT_RAW_LEN 120
2527

2628
struct packet_meta {
@@ -32,7 +34,7 @@ struct packet_meta {
3234
u32 ifindex; /* 4 */
3335
u32 dev_flags; /* 4 */
3436
u32 queue_mapping; /* 4 */
35-
u32 drop_source; /* 4 */
37+
u32 drop_reason; /* 4 */
3638
u32 type; /* 4 */
3739
u32 net_inum; /* 4 */
3840
u8 dev_name[IFNAMSIZ]; /* 16 */
@@ -167,6 +169,13 @@ int bpf_kfree_skb_prog(struct trace_event_raw_kfree_skb *ctx)
167169
* Read directly from skb to avoid the ambiguity. */
168170
skb_protocol = bpf_ntohs(BPF_CORE_READ(skb, protocol));
169171

172+
/* device filter: rewritten at load time via filter_ifindex_included /
173+
* filter_ifindex_excluded. filter_ifindex_included == 0 (default) means all
174+
* devices pass; cheap check, runs before the pcap bytecode below.
175+
*/
176+
if (!skb_filter_pass_dev(skb))
177+
return 0;
178+
170179
/* pcap filter via bpf_pcap_stub.h: pass-through stub patched at load
171180
* time by internal/pcapinject with the compiled tcpdump expression.
172181
*/
@@ -183,7 +192,7 @@ int bpf_kfree_skb_prog(struct trace_event_raw_kfree_skb *ctx)
183192
bpf_get_current_comm(&data->meta.comm, sizeof(data->meta.comm));
184193
data->meta.kfree_skb_addr = (u64)(unsigned long)ctx->location;
185194
data->meta.queue_mapping = BPF_CORE_READ(skb, queue_mapping);
186-
data->meta.drop_source = DROP_SOURCE_UNKNOWN;
195+
data->meta.drop_reason = DROP_REASON_UNKNOWN;
187196
data->meta.type = 0;
188197

189198
data->pkt_hdr.pkt_len = BPF_CORE_READ(skb, len);

bpf/include/bpf_skb_filter.h

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
2+
#ifndef __BPF_SKB_FILTER_H__
3+
#define __BPF_SKB_FILTER_H__
4+
5+
#include <bpf/bpf_core_read.h>
6+
7+
#include "vmlinux_net.h"
8+
9+
/*
10+
* Device filter injected via RewriteConstants at load time.
11+
*
12+
* filter_ifindex_included == 0 : disabled — all devices pass (default)
13+
* filter_ifindex_included != 0 : compare skb->dev->ifindex against this value
14+
* filter_ifindex_excluded == 0 : whitelist — only matching ifindex passes
15+
* filter_ifindex_excluded == 1 : blacklist — matching ifindex is dropped
16+
*
17+
* Each .c file that includes this header gets its own private copies of
18+
* the variables (static linkage), rewritten independently per ELF load.
19+
*/
20+
static volatile const __u32 filter_ifindex_included = 0;
21+
static volatile const __u32 filter_ifindex_excluded = 0;
22+
23+
/*
24+
* skb_filter_pass_dev - device filter check for a kfree_skb-style program.
25+
*
26+
* Returns true if the SKB should be processed, false if it should be skipped.
27+
* When filter_ifindex_included == 0 the check is a no-op (returns true immediately).
28+
*/
29+
static __always_inline bool skb_filter_pass_dev(struct sk_buff *skb)
30+
{
31+
// Early return when filter is disabled.
32+
if (filter_ifindex_included == 0)
33+
return true;
34+
35+
struct net_device *dev = BPF_CORE_READ(skb, dev);
36+
__u32 idx = dev ? BPF_CORE_READ(dev, ifindex) : 0;
37+
bool match = (idx == filter_ifindex_included);
38+
39+
return filter_ifindex_excluded ? !match : match;
40+
}
41+
42+
#endif /* __BPF_SKB_FILTER_H__ */

cmd/dropwatch/format.go

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ import (
1919
"fmt"
2020
"io"
2121
"os"
22-
"strings"
2322

2423
"huatuo-bamai/internal/packet"
24+
"huatuo-bamai/internal/symbol"
2525
"huatuo-bamai/internal/toolstream"
2626
"huatuo-bamai/pkg/types"
2727
)
@@ -34,15 +34,15 @@ type writer interface {
3434
type textWriter struct{ w io.Writer }
3535

3636
func (s *textWriter) Write(ev *types.DropWatchTracing) error {
37-
fmt.Fprintf(s.w, "%s %s %s len=%d dev=%s pid=%d[%s] addr=%s\n",
37+
if _, err := fmt.Fprintf(s.w, "%s %s %s len=%d dev=%s pid=%d[%s] addr=%s\n",
3838
ev.ObservedTimestamp, ev.PacketType, formatDetail(ev.PacketInfo),
39-
ev.PacketLen, ev.NetdevName, ev.Pid, ev.Comm, ev.PacketSkbAddr)
39+
ev.PacketLen, ev.NetdevName, ev.Pid, ev.Comm, ev.PacketSkbAddr); err != nil {
40+
return err
41+
}
4042

4143
if ev.Stack != "" {
42-
for i, frame := range strings.Split(strings.TrimRight(ev.Stack, "\n"), "\n") {
43-
if frame != "" {
44-
fmt.Fprintf(s.w, "\t#%-2d %s\n", i, frame)
45-
}
44+
if err := symbol.FormatStackLines(s.w, ev.Stack); err != nil {
45+
return err
4646
}
4747
}
4848

@@ -52,15 +52,18 @@ func (s *textWriter) Write(ev *types.DropWatchTracing) error {
5252
type jsonWriter struct{ w io.Writer }
5353

5454
func (s *jsonWriter) Write(ev *types.DropWatchTracing) error {
55-
b, _ := json.Marshal(ev)
56-
_, _ = fmt.Fprintf(s.w, "%s\n", b)
57-
return nil
55+
b, err := json.Marshal(ev)
56+
if err != nil {
57+
return err
58+
}
59+
b = append(b, '\n')
60+
_, err = s.w.Write(b)
61+
return err
5862
}
5963

6064
type socketWriter struct{ client *toolstream.Client }
6165

6266
func (s *socketWriter) Write(ev *types.DropWatchTracing) error {
63-
ev.Source = "client"
6467
return s.client.Send(ev)
6568
}
6669

cmd/dropwatch/format_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2026 The HuaTuo Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"errors"
19+
"testing"
20+
21+
"huatuo-bamai/internal/packet"
22+
"huatuo-bamai/pkg/types"
23+
)
24+
25+
// errWriter always fails Write with the configured error. Used to verify that
26+
// the dropwatch writers propagate IO errors instead of swallowing them.
27+
type errWriter struct{ err error }
28+
29+
func (w errWriter) Write(_ []byte) (int, error) { return 0, w.err }
30+
31+
func TestTextWriterPropagatesIOError(t *testing.T) {
32+
boom := errors.New("boom")
33+
w := &textWriter{w: errWriter{err: boom}}
34+
35+
err := w.Write(&types.DropWatchTracing{
36+
ObservedTimestamp: "now",
37+
PacketType: packet.PacketTypeIPv4TCP,
38+
NetdevName: "eth0",
39+
})
40+
if !errors.Is(err, boom) {
41+
t.Fatalf("got %v, want %v", err, boom)
42+
}
43+
}
44+
45+
func TestJSONWriterPropagatesIOError(t *testing.T) {
46+
boom := errors.New("boom")
47+
w := &jsonWriter{w: errWriter{err: boom}}
48+
49+
err := w.Write(&types.DropWatchTracing{ObservedTimestamp: "now"})
50+
if !errors.Is(err, boom) {
51+
t.Fatalf("got %v, want %v", err, boom)
52+
}
53+
}

cmd/dropwatch/main.go

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package main
1717
import (
1818
"context"
1919
"fmt"
20+
"net"
2021
"os"
2122
"os/signal"
2223
"strings"
@@ -54,7 +55,7 @@ type packetMeta struct {
5455
NetdevIfindex uint32
5556
NetdevFlags uint32
5657
NetdevQueueMapping uint32
57-
DropSource uint32
58+
DropReason uint32
5859
Type uint32
5960
NetInode uint32
6061
NetdevName [bpf.NetdevNameLen]byte
@@ -85,16 +86,38 @@ var (
8586
_ = [1]struct{}{}[240-unsafe.Offsetof(dropPacketEvent{}.Stack)]
8687
)
8788

88-
// loadDropwatchBPF reads the BPF object at bpfPath, injects filterExpr into the
89-
// pcap_stub_l2/l3 stubs, and loads it. Each instance uses a unique BPF name to
90-
// allow multiple instances to coexist.
91-
func loadDropwatchBPF(bpfPath, filterExpr string) (bpf.BPF, error) {
89+
// loadBPFWithFilter reads the BPF object at bpfPath, injects filterExpr into the
90+
// pcap_stub_l2/l3 stubs, applies RewriteConstants for any non-nil consts, and
91+
// loads it. Each instance uses a unique BPF name to allow multiple instances
92+
// to coexist.
93+
func loadBPFWithFilter(bpfPath, filterExpr string, consts map[string]any) (bpf.BPF, error) {
9294
bpfBytes, err := os.ReadFile(bpfPath)
9395
if err != nil {
9496
return nil, fmt.Errorf("read bpf object: %w", err)
9597
}
9698
bpfName := fmt.Sprintf("dropwatch_%d.o", time.Now().UnixNano())
97-
return pcapfilter.Load(bpfName, bpfBytes, filterExpr, nil)
99+
return pcapfilter.Load(bpfName, bpfBytes, filterExpr, consts)
100+
}
101+
102+
// deviceFilterConsts resolves devName to an ifindex and returns the consts map
103+
// expected by bpf_skb_filter.h. Returns nil consts when devName is empty
104+
// (filter disabled at the BPF level by leaving filter_ifindex_included == 0).
105+
func deviceFilterConsts(devName string, exclude bool) (map[string]any, error) {
106+
if devName == "" {
107+
return nil, nil
108+
}
109+
iface, err := net.InterfaceByName(devName)
110+
if err != nil {
111+
return nil, fmt.Errorf("--device %q: %w", devName, err)
112+
}
113+
var excl uint32
114+
if exclude {
115+
excl = 1
116+
}
117+
return map[string]any{
118+
"filter_ifindex_included": uint32(iface.Index),
119+
"filter_ifindex_excluded": excl,
120+
}, nil
98121
}
99122

100123
func formatEvent(ev *dropPacketEvent) *types.DropWatchTracing {
@@ -158,7 +181,12 @@ func mainAction(c *cli.Context) error {
158181
defer sockClient.End()
159182
}
160183

161-
bpfObj, err := loadDropwatchBPF(c.String("bpf-path"), c.String("filter"))
184+
consts, err := deviceFilterConsts(c.String("device"), c.Bool("device-exclude"))
185+
if err != nil {
186+
return fmt.Errorf("dropwatch: %w", err)
187+
}
188+
189+
bpfObj, err := loadBPFWithFilter(c.String("bpf-path"), c.String("filter"), consts)
162190
if err != nil {
163191
return fmt.Errorf("dropwatch: load bpf: %w", err)
164192
}
@@ -234,6 +262,14 @@ func main() {
234262
Name: "filter",
235263
Usage: `tcpdump expression, e.g. "tcp and port 80"`,
236264
},
265+
&cli.StringFlag{
266+
Name: "device",
267+
Usage: "interface to filter on (e.g. eth0); empty = all devices",
268+
},
269+
&cli.BoolFlag{
270+
Name: "device-exclude",
271+
Usage: "treat --device as a blacklist (drop matching) instead of whitelist (pass matching)",
272+
},
237273
&cli.IntFlag{
238274
Name: "duration",
239275
Usage: "run for N seconds then exit (0=forever)",

core/events/config.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,8 @@ type Config struct {
3535
}
3636

3737
Dropwatch struct {
38-
ExcludedNeighInvalidate bool `default:"true"`
39-
Filter string `default:"tcp"`
40-
ExcludeContainers []string
38+
Filter string `default:"tcp"`
39+
ExcludeContainers []string
4140
}
4241

4342
Netdev struct {

core/events/dropwatch.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,8 @@ func (c *dropWatchTracing) ignore(data *types.DropWatchTracing) bool {
179179
// 4. neigh_timer_handler/ffffffff96d3a870
180180
// 5. ...
181181
// neigh_invalidate: ARP/neighbor table cleanup, filtered by config.
182-
if cfg != nil && cfg.Dropwatch.ExcludedNeighInvalidate {
183-
if len(stack) >= 3 && strings.HasPrefix(stack[2], "neigh_invalidate/") {
184-
return true
185-
}
182+
if len(stack) >= 3 && strings.HasPrefix(stack[2], "neigh_invalidate/") {
183+
return true
186184
}
187185

188186
// stack:

huatuo-bamai.conf

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -401,19 +401,21 @@ BlackList = ["netdev_hw", "metax_gpu"]
401401
#
402402
# monitor packets dropped events in the Linux kernel.
403403
#
404-
# - ExcludedNeighInvalidate
405-
# Skip neigh_invalidate drop events when true.
406-
# Default: true
407-
#
408-
# - Filter
404+
405+
# NOT supported (go-pcap limitation):
409406
# icmp, icmp6, igmp, vrrp, ah, esp, pim and other raw IP protocol keywords
410407
# CIDR prefix length notation (e.g. /24) is not supported for subnet masks;
411408
# use dotted-decimal notation instead (e.g. mask 255.255.255.0)
412-
# Default: tcp
413-
#
414409
[EventTracing.Dropwatch]
415-
# ExcludedNeighInvalidate = true
416-
# Filter = "tcp"
410+
Filter = "tcp"
411+
412+
# event socket
413+
#
414+
# Unified event socket for receiving events from tools.
415+
# Default: "" (disabled)
416+
#
417+
[EventTracing.EventSocket]
418+
SocketPath = "/var/run/huatuo-bamai-event.sock"
417419

418420
# ras
419421
#

pkg/types/dropwatch.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ import "huatuo-bamai/internal/packet"
2121
type DropWatchTracing struct {
2222
ObservedTimestamp string `json:"observed_timestamp"`
2323
Type string `json:"type"`
24-
Reason string `json:"reason,omitempty"`
25-
DropSource string `json:"drop_source"`
24+
DropReason string `json:"drop_reason"`
2625
Comm string `json:"comm"`
2726
Pid uint64 `json:"pid"`
2827
ContainerID string `json:"container_id,omitempty"`

0 commit comments

Comments
 (0)