tools/tcpconnect: add option -c to count connects

Add -c to count all active connections per dest ip/port so we can
easily spot the heavy outbound connection attempts.

    # ./tcpconnect.py -c
    Tracing connect ... Hit Ctrl-C to end
    ^C
    LADDR                 RADDR                    RPORT              CONNECTS
    192.168.10.50         172.217.21.194           443                70
    192.168.10.50         172.213.11.195           443                34
    192.168.10.50         172.212.22.194           443                21
    [...]
diff --git a/tools/tcpconnect.py b/tools/tcpconnect.py
index cb3e83b..eb12667 100755
--- a/tools/tcpconnect.py
+++ b/tools/tcpconnect.py
@@ -4,7 +4,7 @@
 # tcpconnect    Trace TCP connect()s.
 #               For Linux, uses BCC, eBPF. Embedded C.
 #
-# USAGE: tcpconnect [-h] [-t] [-p PID] [-P PORT [PORT ...]]
+# USAGE: tcpconnect [-h] [-c] [-t] [-p PID] [-P PORT [PORT ...]]
 #
 # All connection attempts are traced, even if they ultimately fail.
 #
@@ -17,6 +17,7 @@
 # 25-Sep-2015   Brendan Gregg   Created this.
 # 14-Feb-2016      "      "     Switch to bpf_perf_output.
 # 09-Jan-2019   Takuma Kume     Support filtering by UID
+# 30-Jul-2019   Xiaozhou Liu    Count connects.
 
 from __future__ import print_function
 from bcc import BPF
@@ -24,6 +25,7 @@
 import argparse
 from socket import inet_ntop, ntohs, AF_INET, AF_INET6
 from struct import pack
+from time import sleep
 
 # arguments
 examples = """examples:
@@ -34,6 +36,7 @@
     ./tcpconnect -P 80,81  # only trace port 80 and 81
     ./tcpconnect -U        # include UID
     ./tcpconnect -u 1000   # only trace UID 1000
+    ./tcpconnect -c        # count connects per src ip and dest ip/port
 """
 parser = argparse.ArgumentParser(
     description="Trace TCP connects",
@@ -49,6 +52,8 @@
     help="include UID on output")
 parser.add_argument("-u", "--uid",
     help="trace this UID only")
+parser.add_argument("-c", "--count", action="store_true",
+    help="count connects per src ip and dest ip/port")
 parser.add_argument("--ebpf", action="store_true",
     help=argparse.SUPPRESS)
 args = parser.parse_args()
@@ -87,6 +92,21 @@
 };
 BPF_PERF_OUTPUT(ipv6_events);
 
+// separate flow keys per address family
+struct ipv4_flow_key_t {
+    u32 saddr;
+    u32 daddr;
+    u16 dport;
+};
+BPF_HASH(ipv4_count, struct ipv4_flow_key_t);
+
+struct ipv6_flow_key_t {
+    unsigned __int128 saddr;
+    unsigned __int128 daddr;
+    u16 dport;
+};
+BPF_HASH(ipv6_count, struct ipv6_flow_key_t);
+
 int trace_connect_entry(struct pt_regs *ctx, struct sock *sk)
 {
     u64 pid_tgid = bpf_get_current_pid_tgid();
@@ -130,26 +150,9 @@
     FILTER_PORT
 
     if (ipver == 4) {
-        struct ipv4_data_t data4 = {.pid = pid, .ip = ipver};
-        data4.uid = bpf_get_current_uid_gid();
-        data4.ts_us = bpf_ktime_get_ns() / 1000;
-        data4.saddr = skp->__sk_common.skc_rcv_saddr;
-        data4.daddr = skp->__sk_common.skc_daddr;
-        data4.dport = ntohs(dport);
-        bpf_get_current_comm(&data4.task, sizeof(data4.task));
-        ipv4_events.perf_submit(ctx, &data4, sizeof(data4));
-
+        IPV4_CODE
     } else /* 6 */ {
-        struct ipv6_data_t data6 = {.pid = pid, .ip = ipver};
-        data6.uid = bpf_get_current_uid_gid();
-        data6.ts_us = bpf_ktime_get_ns() / 1000;
-        bpf_probe_read(&data6.saddr, sizeof(data6.saddr),
-            skp->__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr32);
-        bpf_probe_read(&data6.daddr, sizeof(data6.daddr),
-            skp->__sk_common.skc_v6_daddr.in6_u.u6_addr32);
-        data6.dport = ntohs(dport);
-        bpf_get_current_comm(&data6.task, sizeof(data6.task));
-        ipv6_events.perf_submit(ctx, &data6, sizeof(data6));
+        IPV6_CODE
     }
 
     currsock.delete(&tid);
@@ -168,7 +171,58 @@
 }
 """
 
+struct_init = { 'ipv4':
+        { 'count' :
+               """
+               struct ipv4_flow_key_t flow_key = {};
+               flow_key.saddr = skp->__sk_common.skc_rcv_saddr;
+               flow_key.daddr = skp->__sk_common.skc_daddr;
+               flow_key.dport = ntohs(dport);
+               ipv4_count.increment(flow_key);""",
+          'trace' :
+               """
+               struct ipv4_data_t data4 = {.pid = pid, .ip = ipver};
+               data4.uid = bpf_get_current_uid_gid();
+               data4.ts_us = bpf_ktime_get_ns() / 1000;
+               data4.saddr = skp->__sk_common.skc_rcv_saddr;
+               data4.daddr = skp->__sk_common.skc_daddr;
+               data4.dport = ntohs(dport);
+               bpf_get_current_comm(&data4.task, sizeof(data4.task));
+               ipv4_events.perf_submit(ctx, &data4, sizeof(data4));"""
+               },
+        'ipv6':
+        { 'count' :
+               """
+               struct ipv6_flow_key_t flow_key = {};
+               bpf_probe_read(&flow_key.saddr, sizeof(flow_key.saddr),
+                   skp->__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr32);
+               bpf_probe_read(&flow_key.daddr, sizeof(flow_key.daddr),
+                   skp->__sk_common.skc_v6_daddr.in6_u.u6_addr32);
+               flow_key.dport = ntohs(dport);
+               ipv6_count.increment(flow_key);""",
+          'trace' :
+               """
+               struct ipv6_data_t data6 = {.pid = pid, .ip = ipver};
+               data6.uid = bpf_get_current_uid_gid();
+               data6.ts_us = bpf_ktime_get_ns() / 1000;
+               bpf_probe_read(&data6.saddr, sizeof(data6.saddr),
+                   skp->__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr32);
+               bpf_probe_read(&data6.daddr, sizeof(data6.daddr),
+                   skp->__sk_common.skc_v6_daddr.in6_u.u6_addr32);
+               data6.dport = ntohs(dport);
+               bpf_get_current_comm(&data6.task, sizeof(data6.task));
+               ipv6_events.perf_submit(ctx, &data6, sizeof(data6));"""
+               }
+        }
+
 # code substitutions
+if args.count:
+    bpf_text = bpf_text.replace("IPV4_CODE", struct_init['ipv4']['count'])
+    bpf_text = bpf_text.replace("IPV6_CODE", struct_init['ipv6']['count'])
+else:
+    bpf_text = bpf_text.replace("IPV4_CODE", struct_init['ipv4']['trace'])
+    bpf_text = bpf_text.replace("IPV6_CODE", struct_init['ipv6']['trace'])
+
 if args.pid:
     bpf_text = bpf_text.replace('FILTER_PID',
         'if (pid != %s) { return 0; }' % args.pid)
@@ -219,6 +273,18 @@
         inet_ntop(AF_INET6, event.saddr).encode(), inet_ntop(AF_INET6, event.daddr).encode(),
         event.dport))
 
+def depict_cnt(counts_tab, l3prot='ipv4'):
+    for k, v in sorted(counts_tab.items(), key=lambda counts: counts[1].value, reverse=True):
+        depict_key = ""
+        if l3prot == 'ipv4':
+            depict_key = "%-25s %-25s %-20s" %  ((inet_ntop(AF_INET, pack('I', k.saddr))),
+                                              inet_ntop(AF_INET, pack('I', k.daddr)), k.dport)
+        else:
+            depict_key = "%-25s %-25s %-20s" % ((inet_ntop(AF_INET6, k.saddr)),
+                                              inet_ntop(AF_INET6, k.daddr), k.dport)
+
+        print ("%s %-10d" % (depict_key, v.value))
+
 # initialize BPF
 b = BPF(text=bpf_text)
 b.attach_kprobe(event="tcp_v4_connect", fn_name="trace_connect_entry")
@@ -226,21 +292,36 @@
 b.attach_kretprobe(event="tcp_v4_connect", fn_name="trace_connect_v4_return")
 b.attach_kretprobe(event="tcp_v6_connect", fn_name="trace_connect_v6_return")
 
-# header
-if args.timestamp:
-    print("%-9s" % ("TIME(s)"), end="")
-if args.print_uid:
-    print("%-6s" % ("UID"), end="")
-print("%-6s %-12s %-2s %-16s %-16s %-4s" % ("PID", "COMM", "IP", "SADDR",
-    "DADDR", "DPORT"))
-
-start_ts = 0
-
-# read events
-b["ipv4_events"].open_perf_buffer(print_ipv4_event)
-b["ipv6_events"].open_perf_buffer(print_ipv6_event)
-while 1:
+print("Tracing connect ... Hit Ctrl-C to end")
+if args.count:
     try:
-        b.perf_buffer_poll()
+        while 1:
+            sleep(99999999)
     except KeyboardInterrupt:
-        exit()
+        pass
+
+    # header
+    print("\n%-25s %-25s %-20s %-10s" % (
+        "LADDR", "RADDR", "RPORT", "CONNECTS"))
+    depict_cnt(b["ipv4_count"])
+    depict_cnt(b["ipv6_count"], l3prot='ipv6')
+# read events
+else:
+    # header
+    if args.timestamp:
+        print("%-9s" % ("TIME(s)"), end="")
+    if args.print_uid:
+        print("%-6s" % ("UID"), end="")
+    print("%-6s %-12s %-2s %-16s %-16s %-4s" % ("PID", "COMM", "IP", "SADDR",
+        "DADDR", "DPORT"))
+
+    start_ts = 0
+
+    # read events
+    b["ipv4_events"].open_perf_buffer(print_ipv4_event)
+    b["ipv6_events"].open_perf_buffer(print_ipv6_event)
+    while 1:
+        try:
+            b.perf_buffer_poll()
+        except KeyboardInterrupt:
+            exit()