05 获取地理信息
——非复吕蒙之三日,但见楚庄之飞鸣。
wireshark中是没有获取地理信息功能的,这算是一个新future
我们使用开源的数据库ip2region来做。
项目地址:https://github.com/lionsoul2014/ip2region
我们能拿到一个ip2region.xdb文件,如果要使用这类信息库文件,首先得加载它
xdb_search_t searcher("ip2region/ip2region.xdb");
searcher.init_content();
写个小demo测试一下
#include "../third_library/ip2region/xdb_search.h"
#include <string>
#include <iostream>
int main() {
xdb_search_t searcher("../third_library/ip2region/ip2region.xdb");
searcher.init_content();
std::string ip = "211.21.92.114";
std::string location = searcher.search(ip);
std::cout << ip << ": " << location << std::endl;
return 0;
}
compile:
g++ -std=c++14 .\02_ip2region.cpp
..\third_library\ip2region\xdb_search.cc -o ip2region_test.exe -lws2_32
输出:
211.21.92.114: 中国|0|台湾省|0|中华电信
好使~
region的格式是固定的:国家 | 区域 | 省份 | 城市 | ISP
归属地格式化
既然要用到第三方库,那么地理信息相关的功能应封装成一个工具类。ip2region_util;
之后我们的packet信息输出时需要增加源ip和目的ip的归属信息。
效果如图:
06 面向对象封装,添加日志功能
TsharkManager封装
为了方便维护,将与tshark有关的操作都封装成TsharkManager
类。
在visual studio中,右键项目->添加->类,添加TsharkManager。
到目前为止,我们有关tshark的操作都封装到TsharkManager
类中
我们之前做了哪些操作呢?
- 我们可以读取离线文件的数据,逐行解析
- 我们可以将读取的数据保存在数据结构中,在需要的时候取用
所以我们可以封装出以下类:
class TsharkManager
{
public:
TsharkManager(std::string workDir);
~TsharkManager();
bool analysisFile(std::string filePath);
void printAllPacket();
bool getPacketHexData(uint32_t frameNumber, std::vector<unsigned char>& data);
private:
bool parseLine(std::string line, std::shared_ptr<Packet> packet);
private:
std::string tsharkPath;
std::string currentFilePath;
IP2RegionUtil ip2regionUtil;
std::unordered_map<uint32_t, std::shared_ptr<Packet>> allPackets;
};
日志框架loguru
当程序具有一定规模后,便需要引入日志模块。这样做有如下好处:
- 可以将日志按照严重程度分级,常见的日志分级方式如下:
- debug: 较为verbose的信息
- info: 往往是某个模块重要功能的详细信息
- notice/warning:需要注意的信息
- error:错误!
- c++中的std::cout在多线程环境下容易混乱
- 缺乏时间戳和文件信息,调试时不直观
- 性能问题
以上,比起自己造一个日志轮子,我们选择loguru
作为第三方日志库。
GitHub – emilk/loguru: A lightweight C++ logging library
初始化:
loguru::init(argc, argv);
loguru::add_file("nb_easy_tshark.log", loguru::Append, loguru::Verbosity_MAX);
各级别打印:
LOG_F(ERROR, "Failed to open file: %s", packet_file.c_str());
LOG_F(WARNING, "Failed to open file: %s", packet_file.c_str());
LOG_F(INFO, "Failed to open file: %s", packet_file.c_str());
07 获取编程网卡的枚举
现在我们知道tshark -D
命令可以获取当前系统中的网卡枚举。
对比一下 Wireshark上的网卡名吧。
若是对标wireshark,实际上我们想要的是网卡名中最后()里的内容。
所以我们这里需要做一些字符串操作
- 过滤掉我们不需要的网卡,如ciscodump,etwdump…
- 获取
tshark -D
网卡的打印,而后得到id,name,和remark
/* get name and remark
* e.g: 6. \Device\NPF_{FFCB4D95-E737-4DCA-B016-522C9BA641B5} (WLAN)
* id:6
* name:\Device\NPF_{FFCB4D95-E737-4DCA-B016-522C9BA641B5}
* remark:WLAN
*/
最终测试结果如下:
bingo~
08 网卡数据包采集分析
我们之前的分析都是基于离线数据包的分析,而现在我们需要和wireshark一样,可以实时分析从网卡上抓到的数据包的数据。
tshark -i WLAN
tashark 会抓取网卡上流经的数据报文,并加以打印。
既然我们要对数据包进行实时的分析,那么我们也就需要扩展TsharkManager
的类函数
public:
bool startCapture(std::string adapterName);
bool stopCapture();
使用多线程抓包
新建一个线程用于实时抓包.
- 数据面:实时抓包
- 管理面:控制相关
bool TsharkManager::startCapture(std::string adapterName) {
LOG_F(INFO, "start to capture, adapterName %s", adapterName.c_str());
stopFlag = false;
captureWorkerThread = std::make_shared<std::thread>(&TsharkManager::captureWorkerThreadEntry,
this, "\"" + adapterName + "\"");
return true;
}
开始抓包
我们之前实现了分析离线数据包的函数 analysisFile
, 现在只需要稍微改动一下就能实现实施抓取数据包的功能:
- tshark的参数,由
-r
改为-i
adapter. 表示采集网卡数据 - 增加一些参数:
-w
:指定保存数据表-f
: 指定保存为pcap格式
- 在while循环中增加了stopFlag。
停止抓包
bool TsharkManager::stopCapture() {
LOG_F(INFO, "now stop capture pcap");
stopFlag = true;
captureWorkerThread->join();
return true;
}
而后修改main函数适配即可。
实时抓包后打印:
无数据时如何退出?
如果网卡一直没有数据包,那么循环里的fgets会一直阻塞不返回,所以主线程去join
它的时候会一直等待,程序会卡死,除非我们强制杀死进程。
如何解决这个问题:
- 可以使用非阻塞io和select/poll,发现有数据到来时才使用fget去尝试读取
volatile int running = 1;
void* data_thread_func(void* arg) {
FILE* pipe = (FILE*)arg;
int fd = fileno(pipe);
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
char buffer[1024];
fd_set readfds;
struct timeval timeout;
while (running) {
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
timeout.tv_sec = 0;
timeout.tv_usec = 100000;
int result = select(fd + 1, &readfds, NULL, NULL, &timeout);
if (result > 0 && FD_ISSET(fd, &readfds)) {
if (fgets(buffer, sizeof(buffer), pipe) != NULL) {
printf("Received: %s", buffer);
}
} else if (result == 0) {
continue;
} else {
perror("select");
break;
}
}
fclose(pipe);
return NULL;
}
- 既然无流量时fgets会阻塞,那么首先确认下fgets什么时候会正常返回?
- 读到换行符
\n
,此时换行符也会被包含在返回的字符串中。 - 读到指定的字符数
n-1
,并自动在末尾加上空字符\0
。 - 到达文件末尾
EOF
.
因为没有数据,所以前两种都不满足,我们可以直接使用kill tshark进程的方式,杀死后pipe就会断开,fgets会到达EOF。
kill tshark
要杀死tshark,就需要
- 找到tshark的pid
- 封装kill的函数(win/linux 双平台)
static FILE* PopenEx(std::string command, PID_T* pidOut = nullptr);
static int Kill(PID_T pid);
而后在停止抓包的时候,提前杀死tshark进程即可。