|
| 1 | +#! /usr/bin/env python |
| 2 | +""" |
| 3 | +This script filters one or more provided pcap files (not pcapng), creating |
| 4 | +an output file containing only packets with a supplied Community ID hash. |
| 5 | +
|
| 6 | +The output file's packets retain timestamp and all packet data from the |
| 7 | +originally supplied pcap file(s). |
| 8 | +
|
| 9 | +This is based heavily on the "community-id-pcap" script in the same |
| 10 | +directory and retains all of its limitations and caveats at the time this |
| 11 | +script was created. |
| 12 | +
|
| 13 | +Currently supported protocols include IP, IPv6, ICMP, ICMPv6, TCP, |
| 14 | +UDP, SCTP. |
| 15 | +
|
| 16 | +Please note: the protocol parsing implemented in this script relies |
| 17 | +on the dpkt module and is somewhat simplistic: |
| 18 | +
|
| 19 | +- dpkt seems to struggle with some SCTP packets, for which it fails |
| 20 | + to register SCTP even though its header is correctly present. |
| 21 | +
|
| 22 | +- The script doesn't try to get nested network layers (IP over IPv6, |
| 23 | + IP in IP, etc) right. It expects either IP or IPv6, and it expects |
| 24 | + a transport-layer protocol (including the ICMPs here) as the |
| 25 | + immediate next layer. |
| 26 | +""" |
| 27 | +import argparse |
| 28 | +import gzip |
| 29 | +import sys |
| 30 | + |
| 31 | +import communityid |
| 32 | + |
| 33 | +try: |
| 34 | + import dpkt |
| 35 | +except ImportError: |
| 36 | + print('This needs the dpkt Python module') |
| 37 | + sys.exit(1) |
| 38 | + |
| 39 | +from dpkt.ethernet import Ethernet #pylint: disable=import-error |
| 40 | +from dpkt.ip import IP #pylint: disable=import-error |
| 41 | +from dpkt.ip6 import IP6 #pylint: disable=import-error |
| 42 | +from dpkt.icmp import ICMP #pylint: disable=import-error |
| 43 | +from dpkt.icmp6 import ICMP6 #pylint: disable=import-error |
| 44 | +from dpkt.tcp import TCP #pylint: disable=import-error |
| 45 | +from dpkt.udp import UDP #pylint: disable=import-error |
| 46 | +from dpkt.sctp import SCTP #pylint: disable=import-error |
| 47 | + |
| 48 | +class PcapFilter(object): |
| 49 | + def __init__(self, commid, pcap, commidfilter, outputwriter): |
| 50 | + self._commid = commid |
| 51 | + self._pcap = pcap |
| 52 | + self._commidfilter = commidfilter |
| 53 | + self._outputwriter = outputwriter |
| 54 | + |
| 55 | + def process(self): |
| 56 | + if self._pcap.endswith('.gz'): |
| 57 | + opener=gzip.open |
| 58 | + else: |
| 59 | + opener=open |
| 60 | + |
| 61 | + with opener(self._pcap, 'r+b') as inhdl: |
| 62 | + reader = dpkt.pcap.Reader(inhdl) |
| 63 | + for tstamp, pktdata in reader: |
| 64 | + self._process_packet(tstamp, pktdata, self._outputwriter) |
| 65 | + |
| 66 | + def _process_packet(self, tstamp, pktdata, outputwriter): |
| 67 | + pkt = self._packet_parse(pktdata) |
| 68 | + |
| 69 | + if not pkt: |
| 70 | + return |
| 71 | + |
| 72 | + if IP in pkt: |
| 73 | + saddr = pkt[IP].src |
| 74 | + daddr = pkt[IP].dst |
| 75 | + elif IP6 in pkt: |
| 76 | + saddr = pkt[IP6].src |
| 77 | + daddr = pkt[IP6].dst |
| 78 | + else: |
| 79 | + return |
| 80 | + |
| 81 | + tpl = None |
| 82 | + |
| 83 | + if TCP in pkt: |
| 84 | + tpl = communityid.FlowTuple( |
| 85 | + dpkt.ip.IP_PROTO_TCP, saddr, daddr, |
| 86 | + pkt[TCP].sport, pkt[TCP].dport) |
| 87 | + |
| 88 | + elif UDP in pkt: |
| 89 | + tpl = communityid.FlowTuple( |
| 90 | + dpkt.ip.IP_PROTO_UDP, saddr, daddr, |
| 91 | + pkt[UDP].sport, pkt[UDP].dport) |
| 92 | + |
| 93 | + elif SCTP in pkt: |
| 94 | + tpl = communityid.FlowTuple( |
| 95 | + dpkt.ip.IP_PROTO_SCTP, saddr, daddr, |
| 96 | + pkt[SCTP].sport, pkt[SCTP].dport) |
| 97 | + |
| 98 | + elif ICMP in pkt: |
| 99 | + tpl = communityid.FlowTuple( |
| 100 | + dpkt.ip.IP_PROTO_ICMP, saddr, daddr, |
| 101 | + pkt[ICMP].type, pkt[ICMP].code) |
| 102 | + |
| 103 | + elif ICMP6 in pkt: |
| 104 | + tpl = communityid.FlowTuple( |
| 105 | + dpkt.ip.IP_PROTO_ICMP6, saddr, daddr, |
| 106 | + pkt[ICMP6].type, pkt[ICMP6].code) |
| 107 | + |
| 108 | + if tpl is None: |
| 109 | + # Fallbacks to other IP protocols: |
| 110 | + if IP in pkt: |
| 111 | + tpl = communityid.FlowTuple(pkt[IP].p, saddr, daddr) |
| 112 | + elif IP6 in pkt: |
| 113 | + tpl = communityid.FlowTuple(pkt[IP].nxt, saddr, daddr) |
| 114 | + |
| 115 | + if tpl is None: |
| 116 | + return |
| 117 | + |
| 118 | + res = self._commid.calc(tpl) |
| 119 | + |
| 120 | + if res == self._commidfilter: |
| 121 | + outputwriter.writepkt(pktdata, tstamp) |
| 122 | + |
| 123 | + def _packet_parse(self, pktdata): |
| 124 | + """ |
| 125 | + Parses the protocols in the given packet data and returns the |
| 126 | + resulting packet (here, as a dict indexed by the protocol layers |
| 127 | + in form of dpkt classes). |
| 128 | + """ |
| 129 | + layer = Ethernet(pktdata) |
| 130 | + pkt = {} |
| 131 | + |
| 132 | + if isinstance(layer.data, IP): |
| 133 | + pkt[IP] = layer = layer.data |
| 134 | + elif isinstance(layer.data, IP6): |
| 135 | + # XXX This does not correctly skip IPv6 extension headers |
| 136 | + pkt[IP6] = layer = layer.data |
| 137 | + else: |
| 138 | + return pkt |
| 139 | + |
| 140 | + if isinstance(layer.data, ICMP): |
| 141 | + pkt[ICMP] = layer.data |
| 142 | + elif isinstance(layer.data, ICMP6): |
| 143 | + pkt[ICMP6] = layer.data |
| 144 | + elif isinstance(layer.data, TCP): |
| 145 | + pkt[TCP] = layer.data |
| 146 | + elif isinstance(layer.data, UDP): |
| 147 | + pkt[UDP] = layer.data |
| 148 | + elif isinstance(layer.data, SCTP): |
| 149 | + pkt[SCTP] = layer.data |
| 150 | + |
| 151 | + return pkt |
| 152 | + |
| 153 | +def main(): |
| 154 | + parser = argparse.ArgumentParser(description='Community ID pcap filtering utility') |
| 155 | + parser.add_argument('pcaps', metavar='PCAP', nargs='+', |
| 156 | + help='PCAP packet capture files') |
| 157 | + parser.add_argument('--filter', metavar='FILTER', required=True, |
| 158 | + help='Community ID string in base64 format to filter from input pcap file(s)') |
| 159 | + parser.add_argument('--output', metavar='OUTPUT', required=True, |
| 160 | + help='Output pcap file to create and place matching packets into') |
| 161 | + parser.add_argument('--seed', type=int, default=0, metavar='NUM', |
| 162 | + help='Seed value for hash operations') |
| 163 | + args = parser.parse_args() |
| 164 | + |
| 165 | + commid = communityid.CommunityID(args.seed) |
| 166 | + |
| 167 | + # if outfile exists, quit |
| 168 | + try: |
| 169 | + outhdl = open(args.output, 'xb') |
| 170 | + except FileExistsError: |
| 171 | + print('Error: output file %s already exists. Exiting.' % (args.output)) |
| 172 | + return 2 |
| 173 | + else: |
| 174 | + writer = dpkt.pcap.Writer(outhdl) |
| 175 | + |
| 176 | + for pcap in args.pcaps: |
| 177 | + itr = PcapFilter(commid, pcap, args.filter, writer) |
| 178 | + itr.process() |
| 179 | + |
| 180 | + return 0 |
| 181 | + |
| 182 | +if __name__ == '__main__': |
| 183 | + sys.exit(main()) |
0 commit comments