#!/bin/env python3

# Script to analyze BFD packet delays. Example usage:
#
# ./bfd_delay.py capture.pcap
# tcpdump [...] -U -w - | tee capture.pcap | ./bfd_delay.py

import sys
import argparse
import re
import signal
import subprocess
from scapy.all import Packet, conf
from scapy.utils import PcapReader
from scapy.contrib.bfd import BFD

bfd_sessions = dict()
stop = False
ovs_bfd_map = dict()


class BFDSession:
    def __init__(self, my_disc, your_disc):
        self.my_disc = my_disc
        self.your_disc = your_disc
        self.last_packet_timestamp = None
        self.delays = []
        self.max_delay = 0.0
        self.min_delay = float("inf")
        self.total_delay = 0.0
        self.packet_count = 0

        (self.port_name, self.direction) = ovs_bfd_map.get(
            (my_disc, your_disc), (None, None)
        )

        print(f"New {self.summary()}")

    def summary(self):
        return (
            f"BFD session ({self.port_name} {self.direction})"
            f" [{self.my_disc:#010x}, {self.your_disc:#010x}]"
        )

    def add_packet(self, packet):
        timestamp = packet.time
        if self.last_packet_timestamp:
            delay = timestamp - self.last_packet_timestamp
            self.delays.append(delay)
            self.max_delay = max(self.max_delay, delay)
            self.min_delay = min(self.min_delay, delay)
            self.total_delay += delay
            self.packet_count += 1
            if delay > 1.5:
                print(
                    f"[{timestamp:.6f}] ALERT: {self.summary()} "
                    f"delay {delay:.6f}s > 1.5s"
                )
        self.last_packet_timestamp = timestamp

    def get_average_delay(self):
        return (
            self.total_delay / self.packet_count
            if self.packet_count > 0
            else 0.0
        )

    def stats(self):
        avg_delay = self.get_average_delay()
        return (
            f"{self.summary()}: "
            f"Packets: {self.packet_count}, "
            f"Min Delay: {self.min_delay:.6f}s, "
            f"Max Delay: {self.max_delay:.6f}s, "
            f"Avg Delay: {avg_delay:.6f}s"
        )


def signal_handler(sig, frame):
    global stop
    if not stop:
        print("\nSIGINT received. Shutting down and printing summary...")
        stop = True
    else:
        sys.exit(1)


signal.signal(signal.SIGINT, signal_handler)


def parse_ovs_bfd_output(output):
    """
    Parses the output of 'ovs-appctl bfd/show' and populates ovs_bfd_map.
    """
    current_port = None
    local_disc = None
    remote_disc = None

    for line in output.splitlines():
        line = line.strip()
        if line.startswith("----"):
            current_port = line.strip("---- ").strip()
            local_disc = None
            remote_disc = None
        elif "Local Discriminator:" in line:
            match = re.search(r"0x([0-9a-fA-F]+)", line)
            if match:
                local_disc = int(match.group(1), 16)
        elif "Remote Discriminator" in line:
            match = re.search(r"0x([0-9a-fA-F]+)", line)
            if match:
                remote_disc = int(match.group(1), 16)

        if current_port and local_disc and remote_disc:
            # Store in both directions for easy lookup
            ovs_bfd_map[(local_disc, remote_disc)] = (current_port, "Send")
            ovs_bfd_map[(remote_disc, local_disc)] = (current_port, "Recv")

            current_port = None
            local_disc = None
            remote_disc = None


def get_ovs_bfd_sessions_from_command():
    """
    Calls 'ovs-appctl bfd/show' and parses its output.
    """
    try:
        result = subprocess.run(
            ["ovs-appctl", "bfd/show"],
            capture_output=True,
            text=True,
            check=True,
        )
        parse_ovs_bfd_output(result.stdout)
        print(
            "Successfully retrieved OVS BFD sessions from 'ovs-appctl bfd/show'."
        )
    except FileNotFoundError:
        print(
            "Error: 'ovs-appctl' command not found. Is Open vSwitch installed and in PATH?"
        )
    except subprocess.CalledProcessError as e:
        print(f"Error running 'ovs-appctl bfd/show': {e}")
        print(f"Stderr: {e.stderr}")
    except Exception as e:
        print(
            f"An unexpected error occurred while getting OVS BFD sessions: {e}"
        )


def get_ovs_bfd_sessions_from_file(filepath):
    """
    Reads OVS BFD session information from a file and parses its content.
    """
    try:
        with open(filepath, "r") as f:
            output = f.read()
        parse_ovs_bfd_output(output)
        print(f"Successfully loaded OVS BFD sessions from file: {filepath}")
    except FileNotFoundError:
        print(f"Error: OVS BFD sessions file '{filepath}' not found.")
    except Exception as e:
        print(
            f"An unexpected error occurred while reading OVS BFD sessions from file: {e}"
        )


def process_packet(packet):
    global bfd_sessions
    if stop:
        return

    if packet.haslayer(BFD):
        bfd_layer = packet[BFD]
        my_disc = bfd_layer.my_discriminator
        your_disc = bfd_layer.your_discriminator
        session_key = tuple((my_disc, your_disc))

        if session_key not in bfd_sessions:
            bfd_sessions[session_key] = BFDSession(my_disc, your_disc)

        bfd_sessions[session_key].add_packet(packet)


def main():
    parser = argparse.ArgumentParser(
        description="Track BFD sessions in a pcap file."
    )
    parser.add_argument(
        "-o",
        "--ovs",
        action="store_true",
        help="Call 'ovs-appctl bfd/show' to get BFD sessions.",
    )
    parser.add_argument(
        "-b",
        "--bfd-show-file",
        help="Read OVS BFD session information from a file (output of 'ovs-appctl bfd/show').",
    )
    parser.add_argument(
        "filename",
        help="Path to the pcap file. " "If not provided, reads from stdin.",
        default="/dev/stdin",
        nargs="?",
    )
    args = parser.parse_args()

    if args.ovs and args.bfd_show_file:
        parser.error(
            "You can't use both --ovs and --bfd-show-file at the same time. Please choose one."
        )

    if args.ovs:
        get_ovs_bfd_sessions_from_command()
    elif args.bfd_show_file:
        get_ovs_bfd_sessions_from_file(args.bfd_show_file)

    try:
        reader = PcapReader(args.filename)
        print(f"Reading packets from file: {args.filename}")
        while not stop:
            packet = reader.read_packet()
            process_packet(packet)
            if stop:
                break
    except FileNotFoundError:
        print(f"Error: File '{args.file}' not found.")
        sys.exit(1)
    except EOFError:
        pass
    except Exception as e:
        print(f"Error reading pcap file: {e}")
        sys.exit(1)

    print("\n--- BFD Session Summary ---")
    for session_key, session in bfd_sessions.items():
        if session:
            print(session.stats())
    print("---------------------------\n")


if __name__ == "__main__":
    main()
