ostinato/common/pcapfileformat.cpp
Srivats P b0a81fb231 Change recalc cksum defaults implementation
These are the existing and desired defaults -
 * GUI: recalcCksum = true
 * PCAP import test: recalcCksum = false

With the existing implementation, doing PCAP import in 'raw' mode twice
would lead to recalcCksum being checked in the dialog the second time.
This was because we were always forcing it to be true for the GUI case
without checking for anything.
2022-01-23 21:46:42 +05:30

752 lines
22 KiB
C++

/*
Copyright (C) 2011 Srivats P.
This file is part of "Ostinato"
This 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 3 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.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#include "pcapfileformat.h"
#include "pdmlreader.h"
#include "ostprotolib.h"
#include "streambase.h"
#include "hexdump.pb.h"
#include <QDataStream>
#include <QFile>
#include <QFileInfo>
#include <QProcess>
#include <QTemporaryFile>
#include <QtGlobal>
const quint32 kPcapFileMagic = 0xa1b2c3d4;
const quint32 kPcapFileMagicSwapped = 0xd4c3b2a1;
const quint32 kNanoSecondPcapFileMagic = 0xa1b23c4d;
const quint32 kNanoSecondPcapFileMagicSwapped = 0x4d3cb2a1;
const quint16 kPcapFileVersionMajor = 2;
const quint16 kPcapFileVersionMinor = 4;
const quint32 kMaxSnapLen = 65535;
const quint32 kDltEthernet = 1;
PcapFileFormat pcapFileFormat;
PcapImportOptionsDialog::PcapImportOptionsDialog(QVariantMap *options)
: QDialog(NULL)
{
setupUi(this);
setAttribute(Qt::WA_DeleteOnClose);
options_ = options;
viaPdml->setChecked(options_->value("ViaPdml").toBool());
// XXX: By default this key is absent - so that pcap import tests
// evaluate to false and hence show minimal diffs.
// However, for the GUI user, this should be enabled by default.
recalculateCksums->setChecked(
options_->value("RecalculateCksums", QVariant(true))
.toBool());
doDiff->setChecked(options_->value("DoDiff").toBool());
connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
}
PcapImportOptionsDialog::~PcapImportOptionsDialog()
{
}
void PcapImportOptionsDialog::accept()
{
options_->insert("ViaPdml", viaPdml->isChecked());
options_->insert("RecalculateCksums", recalculateCksums->isChecked());
options_->insert("DoDiff", doDiff->isChecked());
QDialog::accept();
}
PcapFileFormat::PcapFileFormat()
{
importOptions_.insert("ViaPdml", true);
importOptions_.insert("DoDiff", true);
}
PcapFileFormat::~PcapFileFormat()
{
}
bool PcapFileFormat::open(const QString fileName,
OstProto::StreamConfigList &streams, QString &error)
{
bool isOk = false;
QFile file(fileName);
QTemporaryFile file2;
quint32 magic;
uchar gzipMagic[2];
bool nsecResolution = false;
int len;
PcapFileHeader fileHdr;
PcapPacketHeader pktHdr;
OstProto::Stream *prevStream = NULL;
quint64 lastXsec = 0;
int pktCount;
qint64 byteCount = 0;
qint64 byteTotal;
QByteArray pktBuf;
bool tryConvert = true;
if (!file.open(QIODevice::ReadOnly))
goto _err_open;
len = file.peek((char*)gzipMagic, sizeof(gzipMagic));
if (len < int(sizeof(gzipMagic)))
goto _err_reading_magic;
if ((gzipMagic[0] == 0x1f) && (gzipMagic[1] == 0x8b))
{
QProcess gzip;
emit status("Decompressing...");
emit target(0);
if (!file2.open())
{
error.append("Unable to open temporary file to uncompress .gz\n");
goto _err_unzip_fail;
}
qDebug("decompressing to %s", qPrintable(file2.fileName()));
gzip.setStandardOutputFile(file2.fileName());
gzip.start(OstProtoLib::gzipPath(),
QStringList()
<< "-d"
<< "-c"
<< fileName);
if (!gzip.waitForStarted(-1))
{
error.append(QString("Unable to start gzip. Check path in Preferences.\n"));
goto _err_unzip_fail;
}
if (!gzip.waitForFinished(-1))
{
error.append(QString("Error running gzip\n"));
goto _err_unzip_fail;
}
file2.seek(0);
fd_.setDevice(&file2);
}
else
{
fd_.setDevice(&file);
}
_retry:
byteTotal = fd_.device()->size() - sizeof(fileHdr);
emit status("Reading File Header...");
emit target(0);
fd_ >> magic;
qDebug("magic = %08x", magic);
if (magic == kPcapFileMagic)
{
// Do nothing
}
else if (magic == kNanoSecondPcapFileMagic)
{
nsecResolution = true;
}
else if ((magic == kPcapFileMagicSwapped)
|| (magic == kNanoSecondPcapFileMagicSwapped))
{
// Toggle Byte order
if (fd_.byteOrder() == QDataStream::BigEndian)
fd_.setByteOrder(QDataStream::LittleEndian);
else
fd_.setByteOrder(QDataStream::BigEndian);
nsecResolution = (magic == kNanoSecondPcapFileMagicSwapped);
}
else // Not a pcap file (could be pcapng or something else)
{
if (tryConvert)
{
// Close and reopen the temp file to be safe
file2.close();
if (!file2.open())
{
error.append("Unable to open temporary file to convert to PCAP\n");
goto _err_convert2pcap;
}
fd_.setDevice(0); // disconnect data stream from file
if (convertToStandardPcap(fileName, file2.fileName(), error))
{
fd_.setDevice(&file2);
tryConvert = false;
goto _retry;
}
else
{
error = QString(tr("Unable to convert %1 to standard PCAP format"))
.arg(fileName);
goto _err_convert2pcap;
}
}
else
goto _err_bad_magic;
}
qDebug("reading filehdr");
fd_ >> fileHdr.versionMajor;
fd_ >> fileHdr.versionMinor;
fd_ >> fileHdr.thisZone;
fd_ >> fileHdr.sigfigs;
fd_ >> fileHdr.snapLen;
fd_ >> fileHdr.network;
qDebug("version check");
if ((fileHdr.versionMajor != kPcapFileVersionMajor) ||
(fileHdr.versionMinor != kPcapFileVersionMinor))
goto _err_unsupported_version;
#if 1
// XXX: we support only Ethernet, for now
if (fileHdr.network != kDltEthernet)
goto _err_unsupported_encap;
#endif
pktBuf.resize(fileHdr.snapLen);
// XXX: PDML also needs the PCAP file to cross check packet bytes
// with the PDML data, so we can't do PDML conversion any earlier
// than this
qDebug("pdml check");
if (importOptions_.value("ViaPdml").toBool())
{
QProcess tshark;
QTemporaryFile pdmlFile;
PdmlReader reader(&streams, importOptions_);
if (!pdmlFile.open())
{
error.append("Unable to open temporary file to create PDML\n");
goto _non_pdml;
}
qDebug("generating PDML %s", qPrintable(pdmlFile.fileName()));
emit status("Generating PDML...");
emit target(0);
tshark.setStandardOutputFile(pdmlFile.fileName());
tshark.start(OstProtoLib::tsharkPath(),
QStringList()
<< QString("-r%1").arg(fileName)
<< "-otcp.desegment_tcp_streams:FALSE"
<< "-Tpdml");
if (!tshark.waitForStarted(-1))
{
error.append(QString("Unable to start tshark. Check path in preferences.\n"));
goto _non_pdml;
}
if (!tshark.waitForFinished(-1))
{
error.append(QString("Error running tshark\n"));
goto _non_pdml;
}
connect(&reader, SIGNAL(progress(int)), this, SIGNAL(progress(int)));
emit status("Reading PDML packets...");
emit target(100); // in percentage
// pdml reader needs pcap, so pass self
isOk = reader.read(&pdmlFile, this, &stop_);
if (stop_)
goto _user_cancel;
if (!isOk)
{
error.append(QString("Error processing PDML (%1, %2): %3\n")
.arg(reader.lineNumber())
.arg(reader.columnNumber())
.arg(reader.errorString()));
goto _exit;
}
if (!importOptions_.value("DoDiff").toBool())
goto _exit;
// !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!
// Let's do the diff ...
// !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!
QProcess awk;
QProcess diff;
QTemporaryFile originalTextFile;
QTemporaryFile importedPcapFile;
QTemporaryFile importedTextFile;
QTemporaryFile diffFile;
const QString kAwkFilter =
"/^[^0]/ { "
"printf \" %s \", $1;"
"for (i=4; i<NF; i++) printf \"%s \", $i;"
"next;"
"}"
"// {print}";
// Convert original file to text ...
if (!originalTextFile.open())
{
error.append("Unable to open temporary file to create text file "
"(original) for diff\n");
goto _diff_fail;
}
qDebug("generating text file (original) %s",
qPrintable(originalTextFile.fileName()));
emit status("Preparing original PCAP for diff...");
emit target(0);
tshark.setStandardOutputProcess(&awk);
awk.setStandardOutputFile(originalTextFile.fileName());
tshark.start(OstProtoLib::tsharkPath(),
QStringList()
<< QString("-r%1").arg(fileName)
<< "-otcp.desegment_tcp_streams:FALSE"
<< "-P"
<< "-x");
if (!tshark.waitForStarted(-1))
{
error.append(QString("Unable to start tshark. Check path in Preferences.\n"));
goto _diff_fail;
}
awk.start(OstProtoLib::awkPath(),
QStringList() << kAwkFilter);
if (!awk.waitForStarted(-1))
{
tshark.kill();
error.append(QString("Unable to start awk. Check path in Preferences.\n"));
goto _diff_fail;
}
if (!tshark.waitForFinished(-1))
{
error.append(QString("Error running tshark\n"));
goto _diff_fail;
}
if (!awk.waitForFinished(-1))
{
error.append(QString("Error running awk\n"));
goto _diff_fail;
}
// Save imported file as PCAP
if (!importedPcapFile.open())
{
error.append("Unable to open temporary file to create pcap file "
"from imported streams for diff\n");
goto _diff_fail;
}
if (!save(streams, importedPcapFile.fileName(), error))
{
error.append("Error saving imported streams as PCAP for diff");
goto _diff_fail;
}
// Convert imported file to text ...
if (!importedTextFile.open())
{
error.append("Unable to open temporary file to create text file "
"(imported) for diff\n");
goto _diff_fail;
}
qDebug("generating text file (imported) %s",
qPrintable(importedTextFile.fileName()));
emit status("Preparing imported PCAP for diff...");
emit target(0);
tshark.setStandardOutputProcess(&awk);
awk.setStandardOutputFile(importedTextFile.fileName());
tshark.start(OstProtoLib::tsharkPath(),
QStringList()
<< QString("-r%1").arg(importedPcapFile.fileName())
<< "-otcp.desegment_tcp_streams:FALSE"
<< "-P"
<< "-x");
if (!tshark.waitForStarted(-1))
{
error.append(QString("Unable to start tshark. Check path in Preferences.\n"));
goto _diff_fail;
}
awk.start(OstProtoLib::awkPath(),
QStringList() << kAwkFilter);
if (!awk.waitForStarted(-1))
{
tshark.kill();
error.append(QString("Unable to start awk. Check path in Preferences.\n"));
goto _diff_fail;
}
if (!tshark.waitForFinished(-1))
{
error.append(QString("Error running tshark\n"));
goto _diff_fail;
}
if (!awk.waitForFinished(-1))
{
error.append(QString("Error running awk\n"));
goto _diff_fail;
}
// Now do the diff of the two text files ...
if (!diffFile.open())
{
error.append("Unable to open temporary file to store diff\n");
goto _diff_fail;
}
qDebug("diffing %s and %s > %s",
qPrintable(originalTextFile.fileName()),
qPrintable(importedTextFile.fileName()),
qPrintable(diffFile.fileName()));
emit status("Taking diff...");
emit target(0);
diff.setStandardOutputFile(diffFile.fileName());
diff.start(OstProtoLib::diffPath(),
QStringList()
<< "-u"
<< "-F^ [1-9]"
<< QString("--label=%1 (actual)")
.arg(QFileInfo(fileName).fileName())
<< QString("--label=%1 (imported)")
.arg(QFileInfo(fileName).fileName())
<< originalTextFile.fileName()
<< importedTextFile.fileName());
if (!diff.waitForStarted(-1))
{
error.append(QString("Unable to start diff. Check path in Preferences.\n")
.arg(diff.exitCode()));
goto _diff_fail;
}
if (!diff.waitForFinished(-1))
{
error.append(QString("Error running diff\n"));
goto _diff_fail;
}
diffFile.close();
if (diffFile.size())
{
error.append(tr("<p>There is a diff between the original and imported streams. See details to review the diff.</p><p>Why a diff? See <a href='%1'>possible reasons</a>.</p>\n\n\n\n").arg("https://jump.ostinato.org/pcapdiff"));
diffFile.open();
diffFile.seek(0);
error.append(QString(diffFile.readAll()));
}
goto _exit;
}
_non_pdml:
qDebug("pcap resolution: %s", nsecResolution ? "nsec" : "usec");
emit status("Reading Packets...");
emit target(100); // in percentage
pktCount = 1;
while (!fd_.atEnd())
{
OstProto::Stream *stream = streams.add_stream();
OstProto::Protocol *proto = stream->add_protocol();
OstProto::HexDump *hexDump = proto->MutableExtension(OstProto::hexDump);
proto->mutable_protocol_id()->set_id(
OstProto::Protocol::kHexDumpFieldNumber);
readPacket(pktHdr, pktBuf);
// validations on inclLen <= origLen && inclLen <= snapLen
Q_ASSERT(pktHdr.inclLen <= fileHdr.snapLen); // TODO: convert to if
hexDump->set_content(pktBuf.data(), pktHdr.inclLen);
hexDump->set_pad_until_end(false);
stream->mutable_stream_id()->set_id(pktCount);
stream->mutable_core()->set_is_enabled(true);
stream->mutable_core()->set_frame_len(pktHdr.inclLen+4); // FCS
stream->mutable_control()->set_num_packets(1);
// setup packet rate to the timing in pcap (as close as possible)
// use quint64 rather than double to store micro/nano second as
// it has a larger range (~580 years) and therefore better accuracy
const quint64 kXsecsInSec = nsecResolution ? 1e9 : 1e6;
quint64 xsec = (pktHdr.tsSec*kXsecsInSec + pktHdr.tsUsec);
quint64 delta = xsec - lastXsec;
qDebug("pktCount = %d, delta = %llu", pktCount, delta);
if ((pktCount != 1) && delta)
stream->mutable_control()->set_packets_per_sec(double(kXsecsInSec)/delta);
if (prevStream)
prevStream->mutable_control()->CopyFrom(stream->control());
lastXsec = xsec;
prevStream = stream;
pktCount++;
byteCount += pktHdr.inclLen + sizeof(pktHdr);
emit progress(int(byteCount*100/byteTotal)); // in percentage
if (stop_)
goto _user_cancel;
}
isOk = true;
goto _exit;
_user_cancel:
isOk = true;
goto _exit;
_diff_fail:
goto _exit;
_err_unsupported_encap:
error = QString(tr("%1 has non-ethernet encapsulation (%2) which is "
"not supported - Sorry!"))
.arg(QFileInfo(fileName).fileName()).arg(fileHdr.network);
goto _exit;
_err_unsupported_version:
error = QString(tr("%1 is in PCAP version %2.%3 format which is "
"not supported - Sorry!"))
.arg(fileName).arg(fileHdr.versionMajor).arg(fileHdr.versionMinor);
goto _exit;
_err_bad_magic:
error = QString(tr("%1 is not a valid PCAP file")).arg(fileName);
goto _exit;
#if 0
_err_truncated:
error = QString(tr("%1 is too short")).arg(fileName);
goto _exit;
#endif
_err_unzip_fail:
goto _exit;
_err_reading_magic:
error = QString(tr("Unable to read magic from %1")).arg(fileName);
goto _exit;
_err_convert2pcap:
goto _exit;
_err_open:
error = QString(tr("Unable to open file: %1")).arg(fileName);
goto _exit;
_exit:
if (!error.isEmpty())
qDebug("%s", qPrintable(error));
file.close();
return isOk;
}
/*!
Converts a non-PCAP capture file to standard PCAP file format using tshark
Returns true if conversion was successful, false otherwise.
*/
bool PcapFileFormat::convertToStandardPcap(
QString fileName, QString outputFileName, QString &error)
{
qDebug("converting to PCAP %s", qPrintable(outputFileName));
emit status("Unsupported format. Converting to standard PCAP format...");
emit target(0);
QProcess tshark;
tshark.start(OstProtoLib::tsharkPath(),
QStringList()
<< QString("-r%1").arg(fileName)
<< "-Fnsecpcap"
<< QString("-w%1").arg(outputFileName));
if (!tshark.waitForStarted(-1))
{
error.append(QString("Unable to start tshark. Check path in preferences.\n"));
return false;
}
if (!tshark.waitForFinished(-1))
{
error.append(QString("Error running tshark\n"));
return false;
}
return true;
}
/*!
Reads packet meta data into pktHdr and packet content into buf.
Returns true if packet is read successfully, false otherwise.
*/
bool PcapFileFormat::readPacket(PcapPacketHeader &pktHdr, QByteArray &pktBuf)
{
quint32 len;
// TODO: chk fd_.status()
// read PcapPacketHeader
fd_ >> pktHdr.tsSec;
fd_ >> pktHdr.tsUsec;
fd_ >> pktHdr.inclLen;
fd_ >> pktHdr.origLen;
// TODO: chk fd_.status()
// XXX: should never be required, but we play safe
if (quint32(pktBuf.size()) < pktHdr.inclLen)
pktBuf.resize(pktHdr.inclLen);
// read Pkt contents
len = fd_.readRawData(pktBuf.data(), pktHdr.inclLen); // TODO: use while?
Q_ASSERT(len == pktHdr.inclLen); // TODO: remove assert
pktBuf.resize(len);
return true;
}
bool PcapFileFormat::save(const OstProto::StreamConfigList streams,
const QString fileName, QString &error)
{
bool isOk = false;
QFile file(fileName);
PcapFileHeader fileHdr;
PcapPacketHeader pktHdr;
QByteArray pktBuf;
if (!file.open(QIODevice::WriteOnly))
goto _err_open;
fd_.setDevice(&file);
fileHdr.magicNumber = kNanoSecondPcapFileMagic;
fileHdr.versionMajor = kPcapFileVersionMajor;
fileHdr.versionMinor = kPcapFileVersionMinor;
fileHdr.thisZone = 0;
fileHdr.sigfigs = 0;
fileHdr.snapLen = kMaxSnapLen;
fileHdr.network = kDltEthernet;
fd_ << fileHdr.magicNumber;
fd_ << fileHdr.versionMajor;
fd_ << fileHdr.versionMinor;
fd_ << fileHdr.thisZone;
fd_ << fileHdr.sigfigs;
fd_ << fileHdr.snapLen;
fd_ << fileHdr.network;
pktBuf.resize(kMaxSnapLen);
emit status("Writing Packets...");
emit target(streams.stream_size());
pktHdr.tsSec = 0;
pktHdr.tsUsec = 0;
for (int i = 0; i < streams.stream_size(); i++)
{
StreamBase s;
s.setId(i);
s.protoDataCopyFrom(streams.stream(i));
// TODO: expand frameIndex for each stream
s.frameValue((uchar*)pktBuf.data(), pktBuf.size(), 0);
pktHdr.inclLen = s.frameProtocolLength(0); // FIXME: stream index = 0
pktHdr.origLen = s.frameLen() - 4; // FCS; FIXME: Hardcoding
qDebug("savepcap i=%d, incl/orig len = %d/%d", i,
pktHdr.inclLen, pktHdr.origLen);
if (pktHdr.inclLen > fileHdr.snapLen)
pktHdr.inclLen = fileHdr.snapLen;
fd_ << pktHdr.tsSec;
fd_ << pktHdr.tsUsec;
fd_ << pktHdr.inclLen;
fd_ << pktHdr.origLen;
fd_.writeRawData(pktBuf.data(), pktHdr.inclLen);
if (s.packetRate())
{
quint64 delta = quint64(1e9/s.packetRate());
pktHdr.tsSec += delta/quint32(1e9);
pktHdr.tsUsec += delta % quint32(1e9);
}
if (pktHdr.tsUsec >= quint32(1e9))
{
pktHdr.tsSec++;
pktHdr.tsUsec -= quint32(1e9);
}
emit progress(i);
}
file.close();
isOk = true;
goto _exit;
_err_open:
error = QString(tr("Unable to open file: %1")).arg(fileName);
goto _exit;
_exit:
return isOk;
}
QDialog* PcapFileFormat::openOptionsDialog()
{
return new PcapImportOptionsDialog(&importOptions_);
}
bool PcapFileFormat::isMyFileFormat(const QString /*fileName*/)
{
// TODO
return true;
}
bool PcapFileFormat::isMyFileType(const QString fileType)
{
if (fileType.startsWith("PCAP"))
return true;
else
return false;
}