osmocom-bb/src/target/trx_toolkit/trx_sniff.py

285 lines
8.2 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# TRX Toolkit
# Scapy-based TRX interface sniffer
#
# (C) 2018-2020 by Vadim Yanitskiy <axilirator@gmail.com>
#
# All Rights Reserved
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
APP_CR_HOLDERS = [("2018-2020", "Vadim Yanitskiy <axilirator@gmail.com>")]
import logging as log
import argparse
import sys
import scapy.all
from app_common import ApplicationBase
from data_dump import DATADumpFile
from data_msg import *
class Application(ApplicationBase):
# Counters
cnt_burst_dropped_num = 0
cnt_burst_num = 0
cnt_frame_last = None
cnt_frame_num = 0
# Internal variables
lo_trigger = False
def __init__(self):
self.app_print_copyright(APP_CR_HOLDERS)
self.argv = self.parse_argv()
# Configure logging
self.app_init_logging(self.argv)
# Open requested capture file
if self.argv.output_file is not None:
self.ddf = DATADumpFile(self.argv.output_file)
def run(self):
# Compose a list of permitted UDP ports
rx_port_list = ["port %d" % (port + 102) for port in self.argv.base_ports]
tx_port_list = ["port %d" % (port + 2) for port in self.argv.base_ports]
# Arguments to be passed to scapy.all.sniff()
sniff_args = {
"filter" : "udp and (%s)" % " or ".join(rx_port_list + tx_port_list),
"prn" : self.pkt_handler,
"store" : 0,
}
if self.argv.cap_file is not None:
log.info("Reading packets from '%s'..." % self.argv.cap_file)
sniff_args["offline"] = self.argv.cap_file
else:
log.info("Listening on interface '%s'..." % self.argv.sniff_if)
sniff_args["iface"] = self.argv.sniff_if
if self.argv.cap_filter is not None:
log.info("Using additional capture filter '%s'" % self.argv.cap_filter)
sniff_args["filter"] += " and (%s)" % self.argv.cap_filter
# Start sniffing...
scapy.all.sniff(**sniff_args)
# Scapy registers its own signal handler
self.shutdown()
def pkt_handler(self, ether):
# Prevent loopback packet duplication
if self.argv.sniff_if == "lo" and self.argv.cap_file is None:
self.lo_trigger = not self.lo_trigger
if not self.lo_trigger:
return
# Extract a TRX payload
ip = ether.payload
udp = ip.payload
trx = udp.payload
# Convert to bytearray
msg_raw = bytearray(trx.load)
# Determine a burst direction (L1 <-> TRX)
tx_dir = udp.sport > udp.dport
# Create an empty DATA message
msg = TxMsg() if tx_dir else RxMsg()
# Attempt to parse the payload as a DATA message
try:
msg.parse_msg(msg_raw)
msg.validate()
except ValueError as e:
desc = msg.desc_hdr()
if desc == "":
desc = "parsing error"
log.warning("Ignoring an incorrect message (%s): %s" % (desc, e))
self.cnt_burst_dropped_num += 1
return
# Poke burst pass filter
if not self.burst_pass_filter(msg):
self.cnt_burst_dropped_num += 1
return
# Debug print
log.debug("%s burst: %s", "L1 -> TRX" if tx_dir else "TRX -> L1", msg.desc_hdr())
# Poke message handler
self.msg_handle(msg)
# Poke burst counter
rc = self.burst_count(msg.fn, msg.tn)
if rc is True:
self.shutdown()
def burst_pass_filter(self, msg):
# Direction filter
if self.argv.direction is not None:
if self.argv.direction == "TRX": # L1 -> TRX
if not isinstance(msg, TxMsg):
return False
elif self.argv.direction == "L1": # TRX -> L1
if not isinstance(msg, RxMsg):
return False
# Timeslot filter
if self.argv.pf_tn is not None:
if msg.tn != self.argv.pf_tn:
return False
# Frame number filter
if self.argv.pf_fn_lt is not None:
if msg.fn > self.argv.pf_fn_lt:
return False
if self.argv.pf_fn_gt is not None:
if msg.fn < self.argv.pf_fn_gt:
return False
# Message type specific filtering
if isinstance(msg, RxMsg):
# NOPE.ind filter
if not self.argv.pf_nope_ind and msg.nope_ind:
return False
# RSSI filter
if self.argv.pf_rssi_min is not None:
if msg.rssi < self.argv.pf_rssi_min:
return False
if self.argv.pf_rssi_max is not None:
if msg.rssi > self.argv.pf_rssi_max:
return False
# Burst passed ;)
return True
def msg_handle(self, msg):
if self.argv.verbose:
print(msg.burst)
# Append a new message to the capture
if self.argv.output_file is not None:
self.ddf.append_msg(msg)
def burst_count(self, fn, tn):
# Update frame counter
if self.cnt_frame_last is None:
self.cnt_frame_last = fn
self.cnt_frame_num += 1
else:
if fn != self.cnt_frame_last:
self.cnt_frame_num += 1
# Update burst counter
self.cnt_burst_num += 1
# Stop sniffing after N bursts
if self.argv.burst_count is not None:
if self.cnt_burst_num == self.argv.burst_count:
log.info("Collected required amount of bursts")
return True
# Stop sniffing after N frames
if self.argv.frame_count is not None:
if self.cnt_frame_num == self.argv.frame_count:
log.info("Collected required amount of frames")
return True
return False
def shutdown(self):
log.info("Shutting down...")
# Print statistics
log.info("%u bursts handled, %u dropped" \
% (self.cnt_burst_num, self.cnt_burst_dropped_num))
# Exit
sys.exit(0)
def parse_argv(self):
parser = argparse.ArgumentParser(prog = "trx_sniff",
description = "Scapy-based TRX interface sniffer")
parser.add_argument("-v", "--verbose",
dest = "verbose", action = "store_true",
help = "Print burst bits to stdout")
# Register common logging options
self.app_reg_logging_options(parser)
trx_group = parser.add_argument_group("TRX interface")
trx_group.add_argument("-p", "--base-port", "--base-ports",
dest = "base_ports", type = int, metavar = "PORT",
default = [5700, 6700], nargs = "*",
help = "Set base port number (default %(default)s)")
trx_group.add_argument("-o", "--output-file", metavar = "FILE",
dest = "output_file", type = str,
help = "Write bursts to a capture file")
input_group = trx_group.add_mutually_exclusive_group()
input_group.add_argument("-i", "--sniff-interface",
dest = "sniff_if", type = str, default = "lo", metavar = "IF",
help = "Set network interface (default '%(default)s')")
input_group.add_argument("-r", "--capture-file",
dest = "cap_file", type = str, metavar = "FILE",
help = "Read packets from a PCAP file")
trx_group.add_argument("-f", "--capture-filter",
dest = "cap_filter", type = str, metavar = "FILTER",
help = "Set additional capture filter (e.g. 'host 192.168.1.2')")
cnt_group = parser.add_argument_group("Count limitations (optional)")
cnt_group.add_argument("--frame-count", metavar = "N",
dest = "frame_count", type = int,
help = "Stop after sniffing N frames")
cnt_group.add_argument("--burst-count", metavar = "N",
dest = "burst_count", type = int,
help = "Stop after sniffing N bursts")
pf_group = parser.add_argument_group("Filtering (optional)")
pf_group.add_argument("--direction",
dest = "direction", type = str, choices = ["TRX", "L1"],
help = "Burst direction")
pf_group.add_argument("--timeslot", metavar = "TN",
dest = "pf_tn", type = int, choices = range(0, 8),
help = "TDMA timeslot number (equal TN)")
pf_group.add_argument("--frame-num-lt", metavar = "FN",
dest = "pf_fn_lt", type = int,
help = "TDMA frame number (lower than FN)")
pf_group.add_argument("--frame-num-gt", metavar = "FN",
dest = "pf_fn_gt", type = int,
help = "TDMA frame number (greater than FN)")
pf_group.add_argument("--no-nope-ind",
dest = "pf_nope_ind", action = "store_false",
help = "Ignore NOPE.ind (NOPE / IDLE indications)")
pf_group.add_argument("--rssi-min", metavar = "RSSI",
dest = "pf_rssi_min", type = int,
help = "Minimum RSSI value (e.g. -75)")
pf_group.add_argument("--rssi-max", metavar = "RSSI",
dest = "pf_rssi_max", type = int,
help = "Maximum RSSI value (e.g. -50)")
return parser.parse_args()
if __name__ == '__main__':
app = Application()
app.run()