blob: df6845331f016cb1078bd123f1d8e57c1e224b8e [file] [log] [blame]
Brendan Gregg797c3ec2016-10-19 18:55:10 -07001#!/usr/bin/python
2# @lint-avoid-python-3-compatibility-imports
3#
4# tcplife Trace the lifespan of TCP sessions and summarize.
5# For Linux, uses BCC, BPF. Embedded C.
6#
7# USAGE: tcplife [-h] [-C] [-S] [-p PID] [interval [count]]
8#
Brendan Gregge023bc82017-12-29 22:46:27 -08009# This uses the tcp:tcp_set_state tracepoint if it exists (added to
10# Linux 4.15), else it uses kernel dynamic tracing of tcp_set_state().
Brendan Gregg797c3ec2016-10-19 18:55:10 -070011#
12# While throughput counters are emitted, they are fetched in a low-overhead
13# manner: reading members of the tcp_info struct on TCP close. ie, we do not
14# trace send/receive.
15#
16# Copyright 2016 Netflix, Inc.
17# Licensed under the Apache License, Version 2.0 (the "License")
18#
19# IDEA: Julia Evans
20#
21# 18-Oct-2016 Brendan Gregg Created this.
Brendan Gregge023bc82017-12-29 22:46:27 -080022# 29-Dec-2017 " " Added tracepoint support.
Brendan Gregg797c3ec2016-10-19 18:55:10 -070023
24from __future__ import print_function
25from bcc import BPF
26import argparse
27from socket import inet_ntop, ntohs, AF_INET, AF_INET6
28from struct import pack
29import ctypes as ct
30from time import strftime
31
32# arguments
33examples = """examples:
34 ./tcplife # trace all TCP connect()s
35 ./tcplife -t # include time column (HH:MM:SS)
36 ./tcplife -w # wider colums (fit IPv6)
37 ./tcplife -stT # csv output, with times & timestamps
38 ./tcplife -p 181 # only trace PID 181
39 ./tcplife -L 80 # only trace local port 80
40 ./tcplife -L 80,81 # only trace local ports 80 and 81
41 ./tcplife -D 80 # only trace remote port 80
42"""
43parser = argparse.ArgumentParser(
44 description="Trace the lifespan of TCP sessions and summarize",
45 formatter_class=argparse.RawDescriptionHelpFormatter,
46 epilog=examples)
47parser.add_argument("-T", "--time", action="store_true",
48 help="include time column on output (HH:MM:SS)")
49parser.add_argument("-t", "--timestamp", action="store_true",
50 help="include timestamp on output (seconds)")
51parser.add_argument("-w", "--wide", action="store_true",
52 help="wide column output (fits IPv6 addresses)")
53parser.add_argument("-s", "--csv", action="store_true",
Edward Bettsfdf9b082017-10-10 21:13:28 +010054 help="comma separated values output")
Brendan Gregg797c3ec2016-10-19 18:55:10 -070055parser.add_argument("-p", "--pid",
56 help="trace this PID only")
57parser.add_argument("-L", "--localport",
58 help="comma-separated list of local ports to trace.")
59parser.add_argument("-D", "--remoteport",
60 help="comma-separated list of remote ports to trace.")
61args = parser.parse_args()
62debug = 0
63
64# define BPF program
65bpf_text = """
66#include <uapi/linux/ptrace.h>
67#define KBUILD_MODNAME "foo"
68#include <linux/tcp.h>
69#include <net/sock.h>
70#include <bcc/proto.h>
71
72BPF_HASH(birth, struct sock *, u64);
73
74// separate data structs for ipv4 and ipv6
75struct ipv4_data_t {
76 // XXX: switch some to u32's when supported
77 u64 ts_us;
78 u64 pid;
79 u64 saddr;
80 u64 daddr;
81 u64 ports;
82 u64 rx_b;
83 u64 tx_b;
84 u64 span_us;
85 char task[TASK_COMM_LEN];
86};
87BPF_PERF_OUTPUT(ipv4_events);
88
89struct ipv6_data_t {
90 u64 ts_us;
91 u64 pid;
92 unsigned __int128 saddr;
93 unsigned __int128 daddr;
94 u64 ports;
95 u64 rx_b;
96 u64 tx_b;
97 u64 span_us;
98 char task[TASK_COMM_LEN];
99};
100BPF_PERF_OUTPUT(ipv6_events);
101
102struct id_t {
103 u32 pid;
104 char task[TASK_COMM_LEN];
105};
106BPF_HASH(whoami, struct sock *, struct id_t);
Brendan Gregge023bc82017-12-29 22:46:27 -0800107"""
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700108
Brendan Gregge023bc82017-12-29 22:46:27 -0800109#
110# XXX: The following is temporary code for older kernels, Linux 4.14 and
111# older. It uses kprobes to instrument tcp_set_state(). On Linux 4.15 and
112# later, the tcp:tcp_set_state tracepoint should be used instead, as is
113# done by the code that follows this. In the distant future (2021?), this
114# kprobe code can be removed. This is why there is so much code
115# duplication: to make removal easier.
116#
117bpf_text_kprobe = """
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700118int kprobe__tcp_set_state(struct pt_regs *ctx, struct sock *sk, int state)
119{
120 u32 pid = bpf_get_current_pid_tgid() >> 32;
121
122 // lport is either used in a filter here, or later
123 u16 lport = sk->__sk_common.skc_num;
124 FILTER_LPORT
125
126 // dport is either used in a filter here, or later
127 u16 dport = sk->__sk_common.skc_dport;
128 FILTER_DPORT
129
130 /*
131 * This tool includes PID and comm context. It's best effort, and may
132 * be wrong in some situations. It currently works like this:
Brendan Gregg4fd7d322016-11-28 17:57:20 -0800133 * - record timestamp on any state < TCP_FIN_WAIT1
134 * - cache task context on:
135 * TCP_SYN_SENT: tracing from client
136 * TCP_LAST_ACK: client-closed from server
137 * - do output on TCP_CLOSE:
138 * fetch task context if cached, or use current task
139 */
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700140
Brendan Gregg42d00a42016-11-30 16:55:45 -0800141 // capture birth time
142 if (state < TCP_FIN_WAIT1) {
143 /*
144 * Matching just ESTABLISHED may be sufficient, provided no code-path
145 * sets ESTABLISHED without a tcp_set_state() call. Until we know
146 * that for sure, match all early states to increase chances a
147 * timestamp is set.
148 * Note that this needs to be set before the PID filter later on,
149 * since the PID isn't reliable for these early stages, so we must
150 * save all timestamps and do the PID filter later when we can.
151 */
152 u64 ts = bpf_ktime_get_ns();
153 birth.update(&sk, &ts);
154 }
155
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700156 // record PID & comm on SYN_SENT
Brendan Gregg4fd7d322016-11-28 17:57:20 -0800157 if (state == TCP_SYN_SENT || state == TCP_LAST_ACK) {
Brendan Gregg42d00a42016-11-30 16:55:45 -0800158 // now we can PID filter, both here and a little later on for CLOSE
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700159 FILTER_PID
160 struct id_t me = {.pid = pid};
161 bpf_get_current_comm(&me.task, sizeof(me.task));
162 whoami.update(&sk, &me);
163 }
164
Brendan Gregg4fd7d322016-11-28 17:57:20 -0800165 if (state != TCP_CLOSE)
166 return 0;
167
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700168 // calculate lifespan
169 u64 *tsp, delta_us;
170 tsp = birth.lookup(&sk);
171 if (tsp == 0) {
Brendan Gregg42d00a42016-11-30 16:55:45 -0800172 whoami.delete(&sk); // may not exist
173 return 0; // missed create
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700174 }
175 delta_us = (bpf_ktime_get_ns() - *tsp) / 1000;
Brendan Gregg42d00a42016-11-30 16:55:45 -0800176 birth.delete(&sk);
177
178 // fetch possible cached data, and filter
179 struct id_t *mep;
180 mep = whoami.lookup(&sk);
181 if (mep != 0)
182 pid = mep->pid;
183 FILTER_PID
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700184
185 // get throughput stats. see tcp_get_info().
186 u64 rx_b = 0, tx_b = 0, sport = 0;
187 struct tcp_sock *tp = (struct tcp_sock *)sk;
188 rx_b = tp->bytes_received;
189 tx_b = tp->bytes_acked;
190
191 u16 family = sk->__sk_common.skc_family;
192
193 if (family == AF_INET) {
194 struct ipv4_data_t data4 = {.span_us = delta_us,
195 .rx_b = rx_b, .tx_b = tx_b};
196 data4.ts_us = bpf_ktime_get_ns() / 1000;
197 data4.saddr = sk->__sk_common.skc_rcv_saddr;
198 data4.daddr = sk->__sk_common.skc_daddr;
199 // a workaround until data4 compiles with separate lport/dport
Brendan Gregg4fd7d322016-11-28 17:57:20 -0800200 data4.pid = pid;
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700201 data4.ports = ntohs(dport) + ((0ULL + lport) << 32);
202 if (mep == 0) {
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700203 bpf_get_current_comm(&data4.task, sizeof(data4.task));
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700204 } else {
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700205 bpf_probe_read(&data4.task, sizeof(data4.task), (void *)mep->task);
206 }
207 ipv4_events.perf_submit(ctx, &data4, sizeof(data4));
208
209 } else /* 6 */ {
210 struct ipv6_data_t data6 = {.span_us = delta_us,
211 .rx_b = rx_b, .tx_b = tx_b};
212 data6.ts_us = bpf_ktime_get_ns() / 1000;
213 bpf_probe_read(&data6.saddr, sizeof(data6.saddr),
214 sk->__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr32);
215 bpf_probe_read(&data6.daddr, sizeof(data6.daddr),
216 sk->__sk_common.skc_v6_daddr.in6_u.u6_addr32);
217 // a workaround until data6 compiles with separate lport/dport
218 data6.ports = ntohs(dport) + ((0ULL + lport) << 32);
Brendan Gregg4fd7d322016-11-28 17:57:20 -0800219 data6.pid = pid;
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700220 if (mep == 0) {
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700221 bpf_get_current_comm(&data6.task, sizeof(data6.task));
222 } else {
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700223 bpf_probe_read(&data6.task, sizeof(data6.task), (void *)mep->task);
224 }
225 ipv6_events.perf_submit(ctx, &data6, sizeof(data6));
226 }
227
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700228 if (mep != 0)
229 whoami.delete(&sk);
230
231 return 0;
232}
233"""
234
Brendan Gregge023bc82017-12-29 22:46:27 -0800235bpf_text_tracepoint = """
236TRACEPOINT_PROBE(tcp, tcp_set_state)
237{
238 u32 pid = bpf_get_current_pid_tgid() >> 32;
239 // sk is mostly used as a UUID, once for skc_family, and two tcp stats:
240 struct sock *sk = (struct sock *)args->skaddr;
241
242 // lport is either used in a filter here, or later
243 u16 lport = args->sport;
244 FILTER_LPORT
245
246 // dport is either used in a filter here, or later
247 u16 dport = args->dport;
248 FILTER_DPORT
249
250 /*
251 * This tool includes PID and comm context. It's best effort, and may
252 * be wrong in some situations. It currently works like this:
253 * - record timestamp on any state < TCP_FIN_WAIT1
254 * - cache task context on:
255 * TCP_SYN_SENT: tracing from client
256 * TCP_LAST_ACK: client-closed from server
257 * - do output on TCP_CLOSE:
258 * fetch task context if cached, or use current task
259 */
260
261 // capture birth time
262 if (args->newstate < TCP_FIN_WAIT1) {
263 /*
264 * Matching just ESTABLISHED may be sufficient, provided no code-path
265 * sets ESTABLISHED without a tcp_set_state() call. Until we know
266 * that for sure, match all early states to increase chances a
267 * timestamp is set.
268 * Note that this needs to be set before the PID filter later on,
269 * since the PID isn't reliable for these early stages, so we must
270 * save all timestamps and do the PID filter later when we can.
271 */
272 u64 ts = bpf_ktime_get_ns();
273 birth.update(&sk, &ts);
274 }
275
276 // record PID & comm on SYN_SENT
277 if (args->newstate == TCP_SYN_SENT || args->newstate == TCP_LAST_ACK) {
278 // now we can PID filter, both here and a little later on for CLOSE
279 FILTER_PID
280 struct id_t me = {.pid = pid};
281 bpf_get_current_comm(&me.task, sizeof(me.task));
282 whoami.update(&sk, &me);
283 }
284
285 if (args->newstate != TCP_CLOSE)
286 return 0;
287
288 // calculate lifespan
289 u64 *tsp, delta_us;
290 tsp = birth.lookup(&sk);
291 if (tsp == 0) {
292 whoami.delete(&sk); // may not exist
293 return 0; // missed create
294 }
295 delta_us = (bpf_ktime_get_ns() - *tsp) / 1000;
296 birth.delete(&sk);
297
298 // fetch possible cached data, and filter
299 struct id_t *mep;
300 mep = whoami.lookup(&sk);
301 if (mep != 0)
302 pid = mep->pid;
303 FILTER_PID
304
305 // get throughput stats. see tcp_get_info().
306 u64 rx_b = 0, tx_b = 0, sport = 0;
307 struct tcp_sock *tp = (struct tcp_sock *)sk;
308 bpf_probe_read(&rx_b, sizeof(rx_b), &tp->bytes_received);
309 bpf_probe_read(&tx_b, sizeof(tx_b), &tp->bytes_acked);
310
311 u16 family = 0;
312 bpf_probe_read(&family, sizeof(family), &sk->__sk_common.skc_family);
313
314 if (family == AF_INET) {
315 struct ipv4_data_t data4 = {.span_us = delta_us,
316 .rx_b = rx_b, .tx_b = tx_b};
317 data4.ts_us = bpf_ktime_get_ns() / 1000;
318 bpf_probe_read(&data4.saddr, sizeof(u32), args->saddr);
319 bpf_probe_read(&data4.daddr, sizeof(u32), args->daddr);
320 // a workaround until data4 compiles with separate lport/dport
321 data4.ports = dport + ((0ULL + lport) << 32);
322 data4.pid = pid;
323
324 if (mep == 0) {
325 bpf_get_current_comm(&data4.task, sizeof(data4.task));
326 } else {
327 bpf_probe_read(&data4.task, sizeof(data4.task), (void *)mep->task);
328 }
329 ipv4_events.perf_submit(args, &data4, sizeof(data4));
330
331 } else /* 6 */ {
332 struct ipv6_data_t data6 = {.span_us = delta_us,
333 .rx_b = rx_b, .tx_b = tx_b};
334 data6.ts_us = bpf_ktime_get_ns() / 1000;
335 bpf_probe_read(&data6.saddr, sizeof(data6.saddr), args->saddr_v6);
336 bpf_probe_read(&data6.daddr, sizeof(data6.daddr), args->saddr_v6);
337 // a workaround until data6 compiles with separate lport/dport
338 data6.ports = dport + ((0ULL + lport) << 32);
339 data6.pid = pid;
340 if (mep == 0) {
341 bpf_get_current_comm(&data6.task, sizeof(data6.task));
342 } else {
343 bpf_probe_read(&data6.task, sizeof(data6.task), (void *)mep->task);
344 }
345 ipv6_events.perf_submit(args, &data6, sizeof(data6));
346 }
347
348 if (mep != 0)
349 whoami.delete(&sk);
350
351 return 0;
352}
353"""
354
Brendan Greggefa6ee92017-12-30 12:48:48 -0800355if (BPF.tracepoint_exists("tcp", "tcp_set_state")):
Brendan Gregge023bc82017-12-29 22:46:27 -0800356 bpf_text += bpf_text_tracepoint
357else:
358 bpf_text += bpf_text_kprobe
359
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700360# code substitutions
361if args.pid:
362 bpf_text = bpf_text.replace('FILTER_PID',
363 'if (pid != %s) { return 0; }' % args.pid)
364if args.remoteport:
365 dports = [int(dport) for dport in args.remoteport.split(',')]
366 dports_if = ' && '.join(['dport != %d' % ntohs(dport) for dport in dports])
367 bpf_text = bpf_text.replace('FILTER_DPORT',
368 'if (%s) { birth.delete(&sk); return 0; }' % dports_if)
369if args.localport:
370 lports = [int(lport) for lport in args.localport.split(',')]
371 lports_if = ' && '.join(['lport != %d' % lport for lport in lports])
372 bpf_text = bpf_text.replace('FILTER_LPORT',
373 'if (%s) { birth.delete(&sk); return 0; }' % lports_if)
374bpf_text = bpf_text.replace('FILTER_PID', '')
375bpf_text = bpf_text.replace('FILTER_DPORT', '')
376bpf_text = bpf_text.replace('FILTER_LPORT', '')
377
378if debug:
379 print(bpf_text)
380
381# event data
382TASK_COMM_LEN = 16 # linux/sched.h
383
384class Data_ipv4(ct.Structure):
385 _fields_ = [
386 ("ts_us", ct.c_ulonglong),
387 ("pid", ct.c_ulonglong),
388 ("saddr", ct.c_ulonglong),
389 ("daddr", ct.c_ulonglong),
390 ("ports", ct.c_ulonglong),
391 ("rx_b", ct.c_ulonglong),
392 ("tx_b", ct.c_ulonglong),
393 ("span_us", ct.c_ulonglong),
394 ("task", ct.c_char * TASK_COMM_LEN)
395 ]
396
397class Data_ipv6(ct.Structure):
398 _fields_ = [
399 ("ts_us", ct.c_ulonglong),
400 ("pid", ct.c_ulonglong),
401 ("saddr", (ct.c_ulonglong * 2)),
402 ("daddr", (ct.c_ulonglong * 2)),
403 ("ports", ct.c_ulonglong),
404 ("rx_b", ct.c_ulonglong),
405 ("tx_b", ct.c_ulonglong),
406 ("span_us", ct.c_ulonglong),
407 ("task", ct.c_char * TASK_COMM_LEN)
408 ]
409
410#
411# Setup output formats
412#
413# Don't change the default output (next 2 lines): this fits in 80 chars. I
414# know it doesn't have NS or UIDs etc. I know. If you really, really, really
415# need to add columns, columns that solve real actual problems, I'd start by
416# adding an extended mode (-x) to included those columns.
417#
418header_string = "%-5s %-10.10s %s%-15s %-5s %-15s %-5s %5s %5s %s"
419format_string = "%-5d %-10.10s %s%-15s %-5d %-15s %-5d %5d %5d %.2f"
420if args.wide:
421 header_string = "%-5s %-16.16s %-2s %-26s %-5s %-26s %-5s %6s %6s %s"
422 format_string = "%-5d %-16.16s %-2s %-26s %-5s %-26s %-5d %6d %6d %.2f"
423if args.csv:
424 header_string = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s"
425 format_string = "%d,%s,%s,%s,%s,%s,%d,%d,%d,%.2f"
426
427# process event
428def print_ipv4_event(cpu, data, size):
429 event = ct.cast(data, ct.POINTER(Data_ipv4)).contents
430 global start_ts
431 if args.time:
432 if args.csv:
433 print("%s," % strftime("%H:%M:%S"), end="")
434 else:
435 print("%-8s " % strftime("%H:%M:%S"), end="")
436 if args.timestamp:
437 if start_ts == 0:
438 start_ts = event.ts_us
439 delta_s = (float(event.ts_us) - start_ts) / 1000000
440 if args.csv:
441 print("%.6f," % delta_s, end="")
442 else:
443 print("%-9.6f " % delta_s, end="")
Rafael F78948e42017-03-26 14:54:25 +0200444 print(format_string % (event.pid, event.task.decode(),
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700445 "4" if args.wide or args.csv else "",
446 inet_ntop(AF_INET, pack("I", event.saddr)), event.ports >> 32,
447 inet_ntop(AF_INET, pack("I", event.daddr)), event.ports & 0xffffffff,
448 event.tx_b / 1024, event.rx_b / 1024, float(event.span_us) / 1000))
449
450def print_ipv6_event(cpu, data, size):
451 event = ct.cast(data, ct.POINTER(Data_ipv6)).contents
452 global start_ts
453 if args.time:
454 if args.csv:
455 print("%s," % strftime("%H:%M:%S"), end="")
456 else:
457 print("%-8s " % strftime("%H:%M:%S"), end="")
458 if args.timestamp:
459 if start_ts == 0:
460 start_ts = event.ts_us
461 delta_s = (float(event.ts_us) - start_ts) / 1000000
462 if args.csv:
463 print("%.6f," % delta_s, end="")
464 else:
465 print("%-9.6f " % delta_s, end="")
Rafael F78948e42017-03-26 14:54:25 +0200466 print(format_string % (event.pid, event.task.decode(),
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700467 "6" if args.wide or args.csv else "",
468 inet_ntop(AF_INET6, event.saddr), event.ports >> 32,
469 inet_ntop(AF_INET6, event.daddr), event.ports & 0xffffffff,
470 event.tx_b / 1024, event.rx_b / 1024, float(event.span_us) / 1000))
471
472# initialize BPF
473b = BPF(text=bpf_text)
474
475# header
476if args.time:
477 if args.csv:
478 print("%s," % ("TIME"), end="")
479 else:
480 print("%-8s " % ("TIME"), end="")
481if args.timestamp:
482 if args.csv:
483 print("%s," % ("TIME(s)"), end="")
484 else:
485 print("%-9s " % ("TIME(s)"), end="")
486print(header_string % ("PID", "COMM",
487 "IP" if args.wide or args.csv else "", "LADDR",
488 "LPORT", "RADDR", "RPORT", "TX_KB", "RX_KB", "MS"))
489
490start_ts = 0
491
492# read events
Mark Drayton5f5687e2017-02-20 18:13:03 +0000493b["ipv4_events"].open_perf_buffer(print_ipv4_event, page_cnt=64)
494b["ipv6_events"].open_perf_buffer(print_ipv6_event, page_cnt=64)
Brendan Gregg797c3ec2016-10-19 18:55:10 -0700495while 1:
496 b.kprobe_poll()