#!/usr/bin/env python
#
# solisten      Trace TCP listen events
#               For Linux, uses BCC, eBPF. Embedded C.
#
# USAGE: solisten.py [-h] [-p PID] [--show-netns]
#
# This is provided as a basic example of TCP connection & socket tracing.
# It could be usefull in scenarios where load balancers needs to be updated
# dynamically as application is fully initialized.
#
# All IPv4 listen attempts are traced, even if they ultimately fail or the
# the listening program is not willing to accept().
#
# Copyright (c) 2016 Jean-Tiare Le Bigot.
# Licensed under the Apache License, Version 2.0 (the "License")
#
# 04-Mar-2016	Jean-Tiare Le Bigot	Created this.

import os
import socket
import netaddr
import argparse
from bcc import BPF
import ctypes as ct

# Arguments
examples = """Examples:
    ./solisten.py              # Stream socket listen
    ./solisten.py -p 1234      # Stream socket listen for specified PID only
    ./solisten.py --netns 4242 # " for the specified network namespace ID only
    ./solisten.py --show-netns # Show network ns ID (useful for containers)
"""

parser = argparse.ArgumentParser(
    description="Stream sockets listen",
    formatter_class=argparse.RawDescriptionHelpFormatter,
    epilog=examples)
parser.add_argument("--show-netns", action="store_true",
    help="show network namespace")
parser.add_argument("-p", "--pid", default=0, type=int,
    help="trace this PID only")
parser.add_argument("-n", "--netns", default=0, type=int,
    help="trace this Network Namespace only")


# BPF Program
bpf_text = """
#include <net/sock.h>
#include <net/inet_sock.h>
#include <net/net_namespace.h>
#include <bcc/proto.h>

// Common structure for UDP/TCP IPv4/IPv6
struct listen_evt_t {
    u64 ts_us;
    u64 pid_tgid;
    u64 backlog;
    u64 netns;
    u64 proto;    // familiy << 16 | type
    u64 lport;    // use only 16 bits
    u64 laddr[2]; // IPv4: store in laddr[0]
    char task[TASK_COMM_LEN];
};
BPF_PERF_OUTPUT(listen_evt);

// Send an event for each IPv4 listen with PID, bound address and port
int kprobe__inet_listen(struct pt_regs *ctx, struct socket *sock, int backlog)
{
        // cast types. Intermediate cast not needed, kept for readability
        struct sock *sk = sock->sk;
        struct inet_sock *inet = inet_sk(sk);

        // Built event for userland
        struct listen_evt_t evt = {
            .ts_us = bpf_ktime_get_ns() / 1000,
            .backlog = backlog,
        };

        // Get process comm. Needs LLVM >= 3.7.1
        // see https://github.com/iovisor/bcc/issues/393
        bpf_get_current_comm(evt.task, TASK_COMM_LEN);

        // Get socket IP family
        u16 family = sk->__sk_common.skc_family;
        evt.proto = family << 16 | SOCK_STREAM;

        // Get PID
        evt.pid_tgid = bpf_get_current_pid_tgid();

        ##FILTER_PID##

        // Get port
        bpf_probe_read(&evt.lport, sizeof(u16), &(inet->inet_sport));
        evt.lport = ntohs(evt.lport);

        // Get network namespace id, if kernel supports it
#ifdef CONFIG_NET_NS
        evt.netns = sk->__sk_common.skc_net.net->ns.inum;
#else
        evt.netns = 0;
#endif

        ##FILTER_NETNS##

        // Get IP
        if (family == AF_INET) {
            bpf_probe_read(evt.laddr, sizeof(u32), &(inet->inet_rcv_saddr));
            evt.laddr[0] = be32_to_cpu(evt.laddr[0]);
        } else if (family == AF_INET6) {
            bpf_probe_read(evt.laddr, sizeof(evt.laddr),
                           sk->__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr32);
            evt.laddr[0] = be64_to_cpu(evt.laddr[0]);
            evt.laddr[1] = be64_to_cpu(evt.laddr[1]);
        }

        // Send event to userland
        listen_evt.perf_submit(ctx, &evt, sizeof(evt));

        return 0;
};
"""

# event data
TASK_COMM_LEN = 16      # linux/sched.h
class ListenEvt(ct.Structure):
    _fields_ = [
        ("ts_us", ct.c_ulonglong),
        ("pid_tgid", ct.c_ulonglong),
        ("backlog", ct.c_ulonglong),
        ("netns", ct.c_ulonglong),
        ("proto", ct.c_ulonglong),
        ("lport", ct.c_ulonglong),
        ("laddr", ct.c_ulonglong * 2),
        ("task", ct.c_char * TASK_COMM_LEN)
    ]

    # TODO: properties to unpack protocol / ip / pid / tgid ...

# Format output
def event_printer(show_netns):
    def print_event(cpu, data, size):
        # Decode event
        event = ct.cast(data, ct.POINTER(ListenEvt)).contents

        pid = event.pid_tgid & 0xffffffff
        proto_family = event.proto & 0xff
        proto_type = event.proto >> 16 & 0xff

        if proto_family == socket.SOCK_STREAM:
            protocol = "TCP"
        elif proto_family == socket.SOCK_DGRAM:
            protocol = "UDP"
        else:
            protocol = "UNK"

        address = ""
        if proto_type == socket.AF_INET:
            protocol += "v4"
            address = netaddr.IPAddress(event.laddr[0])
        elif proto_type == socket.AF_INET6:
            address = netaddr.IPAddress(event.laddr[0] << 64 | event.laddr[1],
                                        version=6)
            protocol += "v6"

        # Display
        if show_netns:
            print("%-6d %-12.12s %-12s %-6s %-8s %-5s %-39s" % (
                pid, event.task, event.netns, protocol, event.backlog,
                event.lport, address,
            ))
        else:
            print("%-6d %-12.12s %-6s %-8s %-5s %-39s" % (
                pid, event.task, protocol, event.backlog,
                event.lport, address,
            ))

    return print_event

if __name__ == "__main__":
    # Parse arguments
    args = parser.parse_args()

    pid_filter = ""
    netns_filter = ""

    if args.pid:
        pid_filter = "if (evt.pid_tgid != %d) return 0;" % args.pid
    if args.netns:
        netns_filter = "if (evt.netns != %d) return 0;" % args.netns

    bpf_text = bpf_text.replace("##FILTER_PID##", pid_filter)
    bpf_text = bpf_text.replace("##FILTER_NETNS##", netns_filter)

    # Initialize BPF
    b = BPF(text=bpf_text)
    b["listen_evt"].open_perf_buffer(event_printer(args.show_netns))

    # Print headers
    if args.show_netns:
        print("%-6s %-12s %-12s %-6s %-8s %-5s %-39s" %
              ("PID", "COMM", "NETNS", "PROTO", "BACKLOG", "PORT", "ADDR"))
    else:
        print("%-6s %-12s %-6s %-8s %-5s %-39s" %
              ("PID", "COMM", "PROTO", "BACKLOG", "PORT", "ADDR"))

    # Read events
    while 1:
        b.kprobe_poll()
