blob: c201930a038d46c3b8a23d21ae25b5682445e8c7 [file] [log] [blame]
#!/usr/bin/python3
#
# nflatency Trace netfilter hook latency.
#
# This attaches a kprobe and kretprobe to nf_hook_slow.
# 2020-04-03 Casey Callendrello / <cdc@redhat.com>
import argparse
import sys
import time
from bcc import BPF
BPF_SRC = """
#include <linux/ip.h>
#include <linux/ipv6.h>
#include <linux/tcp.h>
#include <linux/netfilter.h>
#include <net/ip.h>
#include <uapi/linux/bpf.h>
static struct tcphdr *skb_to_tcphdr(const struct sk_buff *skb)
{
// unstable API. verify logic in tcp_hdr() -> skb_transport_header().
return (struct tcphdr *)(skb->head + skb->transport_header);
}
static inline struct iphdr *skb_to_iphdr(const struct sk_buff *skb)
{
// unstable API. verify logic in ip_hdr() -> skb_network_header().
return (struct iphdr *)(skb->head + skb->network_header);
}
static inline struct ipv6hdr *skb_to_ip6hdr(const struct sk_buff *skb)
{
// unstable API. verify logic in ip_hdr() -> skb_network_header().
return (struct ipv6hdr *)(skb->head + skb->network_header);
}
// for correlating between kprobe and kretprobe
struct start_data {
u8 hook;
u8 pf; // netfilter protocol
u8 tcp_state;
u64 ts;
};
BPF_PERCPU_ARRAY(sts, struct start_data, 1);
// the histogram keys
typedef struct nf_lat_key {
u8 proto; // see netfilter.h
u8 hook;
u8 tcp_state;
} nf_lat_key_t;
typedef struct hist_key {
nf_lat_key_t key;
u64 slot;
} hist_key_t;
BPF_HISTOGRAM(dist, hist_key_t);
int kprobe__nf_hook_slow(struct pt_regs *ctx, struct sk_buff *skb, struct nf_hook_state *state) {
struct start_data data = {};
data.ts = bpf_ktime_get_ns();
data.hook = state->hook;
data.pf = state->pf;
COND
u8 ip_proto;
if (skb->protocol == htons(ETH_P_IP)) {
struct iphdr *ip = skb_to_iphdr(skb);
ip_proto = ip->protocol;
} else if (skb->protocol == htons(ETH_P_IPV6)) {
struct ipv6hdr *ip = skb_to_ip6hdr(skb);
ip_proto = ip->nexthdr;
}
data.tcp_state = 0;
if (ip_proto == 0x06) { //tcp
struct tcphdr *tcp = skb_to_tcphdr(skb);
u8 tcpflags = ((u_int8_t *)tcp)[13];
// FIN or RST
if (((tcpflags & 1) + (tcpflags & 4)) > 0) {
data.tcp_state = 3;
}
// SYN / SACK
else if ((tcpflags & 0x02) > 0) {
data.tcp_state = 1;
if ((tcpflags & 16) > 0) { // ACK
data.tcp_state = 2;
}
}
}
u32 idx = 0;
sts.update(&idx, &data);
return 0;
}
int kretprobe__nf_hook_slow(struct pt_regs *ctx) {
u32 idx = 0;
struct start_data *s;
s = sts.lookup(&idx);
if (!s || s->ts == 0) {
return 0;
}
s->ts = bpf_ktime_get_ns() - s->ts;
hist_key_t key = {};
key.key.hook = s->hook;
key.key.proto = s->pf;
key.key.tcp_state = s->tcp_state;
key.slot = bpf_log2l(s->ts / FACTOR );
dist.increment(key);
s->ts = 0;
sts.update(&idx, s);
return 0;
}
"""
# constants from netfilter.h
NF_HOOKS = {
0: "PRE_ROUTING",
1: "LOCAL_IN",
2: "FORWARD",
3: "LOCAL_OUT",
4: "POST_ROUTING",
}
NF_PROTOS = {
0: "UNSPEC",
1: "INET",
2: "IPV4",
3: "ARP",
5: "NETDEV",
7: "BRIDGE",
10: "IPV6",
12: "DECNET",
}
TCP_FLAGS = {
0: "other",
1: "SYN",
2: "SACK",
3: "FIN",
}
EXAMPLES = """examples:
nflatency # print netfilter latency histograms, 1 second refresh
nflatency -p IPV4 -p IPV6 # print only for ipv4 and ipv6
nflatency -k PRE_ROUTING # only record the PRE_ROUTING hook
nflatency -i 5 -d 10 # run for 10 seconds, printing every 5
"""
parser = argparse.ArgumentParser(
description="Track latency added by netfilter hooks. Where possible, interesting TCP flags are included",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=EXAMPLES)
parser.add_argument("-p", "--proto",
action='append',
help="record this protocol only (multiple parameters allowed)",
choices=NF_PROTOS.values())
parser.add_argument("-k", "--hook",
action='append',
help="record this netfilter hook only (multiple parameters allowed)",
choices=NF_HOOKS.values())
parser.add_argument("-i", "--interval", type=int,
help="summary interval, in seconds. Default is 10, unless --duration is supplied")
parser.add_argument("-d", "--duration", type=int,
help="total duration of trace, in seconds")
parser.add_argument("--nano", action="store_true",
help="bucket by nanoseconds instead of milliseconds")
def main():
args = parser.parse_args()
src = build_src(args)
b = BPF(text=src)
dist = b.get_table("dist")
seconds = 0
interval = 0
if not args.interval:
interval = 1
if args.duration:
interval = args.duration
else:
interval = args.interval
sys.stderr.write("Tracing netfilter hooks... Hit Ctrl-C to end.\n")
while 1:
try:
dist.print_log2_hist(
section_header="Bucket",
bucket_fn=lambda k: (k.proto, k.hook, k.tcp_state),
section_print_fn=bucket_desc)
if args.duration and seconds >= args.duration:
sys.exit(0)
seconds += interval
time.sleep(interval)
except KeyboardInterrupt:
sys.exit(1)
def build_src(args):
cond_src = ""
if args.proto:
predicate = " || ".join(map(lambda x: "data.pf == NFPROTO_%s" % x, args.proto))
cond_src = "if (!(%s)) { return 0; }\n" % predicate
if args.hook:
predicate = " || ".join(map(lambda x: "data.hook == NF_INET_%s" % x, args.hook))
cond_src = "%s if (!(%s)) { return 0;}\n" % (cond_src, predicate)
factor = "1000"
if args.nano:
factor = "1"
return BPF_SRC.replace('COND', cond_src).replace('FACTOR', factor)
def bucket_desc(bucket):
return "%s %s (tcp %s)" % (
NF_PROTOS[bucket[0]],
NF_HOOKS[bucket[1]],
TCP_FLAGS[bucket[2]])
if __name__ == "__main__":
main()