libtin
- 一、概述
- 1. 可移植性
- 2. 特性
- 二、基础知识
- 1. PDU
- 2. 地址类
- 3. 地址范围类
- 4. 网络接口
- 5. 写pcap文件
- 三、嗅探
- 1.嗅探基础
- 2. 嗅探器配置
- 3. 循环嗅探
- 4. 使用迭代器嗅探
- 6. 包对象
- 7. 读取pcap文件
- 8. 包的解析
- 四、发送包
- 1. 发送网络层pdu
- 2. 发送链路层pdu
- 3. 发送和接收响应
- 校验和计算
- 线程安全
- 五、TCP 流
- 1. StreamFollower
- 2. 使用流
- 3. 处理流数据
- 4. 结论
- 六、协议
- 1. Ethernet II
- 2. IP
- 3. TCP
一、概述
libtin是一个高级、跨平台的c++网络数据包嗅探和制作库。
它的主要目的是为c++开发人员提供一种简单、高效、平台和端序无关的方法来创建需要发送、接收和操作网络数据包的工具。它使用BSD-2许可证,并托管在github上。
这个库使用起来非常简单。作为一个简短的例子,这是如何使用它来打印在 eth0 接口中捕获的每个TCP数据包的源和目的地址和端口:
#include <iostream>
#include <tins/tins.h>
using namespace Tins;
using namespace std;
bool callback(const PDU &pdu) {
// Find the IP layer
const IP &ip = pdu.rfind_pdu<IP>();
// Find the TCP layer
const TCP &tcp = pdu.rfind_pdu<TCP>();
cout << ip.src_addr() << ':' << tcp.sport() << " -> "
<< ip.dst_addr() << ':' << tcp.dport() << endl;
return true;
}
int main() {
Sniffer("eth0").sniff_loop(callback);
}
高层api不代表效率低下,libtins的设计始终牢记效率。事实上,它是最快的数据包嗅探和解释库之一。基准测试部分包含对其工作速度的一些实际测量。这个库值得信赖,花费在测试库上的时间几乎与开发库的时间一样多。在撰写本文时,共有624个单元测试,用于检查libtin中的所有内容是否符合预期。
1. 可移植性
libtin可以在Windows、OSX以及小端和大端GNU/Linux和FreeBSD操作系统上工作。这意味着开发的嗅探应用程序,交叉编译并直接在ARM或MIPS路由器或任何其他具有嗅探功能的设备上执行它,只要它有足够的RAM。(libtin是~10MB)
2. 特性
libtin支持以下协议和特性:
- 网络数据包制作。
- 包嗅探和自动包解释。
- 读写PCAP文件。
- 动态地跟踪和重组TCP流。
- 解密WEP和WPA2(TKIP和CCMP)动态加密802.11数据帧并解释解密的内容。
- 至少可以在以下架构上正常工作:x86, x64, ARM和MIPS(可能更多)。
- 支持的协议:
- IEEE 802.11
- IEEE 802.3
- IEEE 802.1q
- Ethernet
- ARP
- IP
- IPv6
- ICMP
- ICMPv6
- TCP
- UDP
- DHCP
- DHCPv6
- DNS
- RadioTap
- MPLS
- EAPOL
- PPPoE
- STP
- LLC
- LLC+SNAP
- Linux Cooked Capture
- PPI
- PKTAP
- NULL/Loopback
二、基础知识
1. PDU
libtins每一个数据包都是PDU的子类,我们可以首先看一下什么是PDU对象。libtins库中实现的每个PDU(如IP、TCP、UDP等)都是一个继承了抽象类PDU的类。这个类包含可以检索实际协议数据单元大小及其类型的方法。它还包含一个名为 send 的方法,该方法允许我们通过网络有效地发送数据包。
PDU 对象也支持堆叠。这意味着一个PDU对象(不考虑其实际类型)可以有0个或1个内部PDU。这是一种非常合乎逻辑的想象网络数据包的方式。假设创建了一个以太网II帧,然后在其上添加一个IP数据报,后面是一个TCP帧。实际上的网络帧也是这么封装的,这个结构看起来像这样:
PDU的内部PDU可以使用方法PDU::inner_pdu()来去检索查询:
#include <tins/tins.h>
using namespace Tins;
int main() {
EthernetII eth;
IP *ip = new IP();
TCP *tcp = new TCP();
// tcp is ip's inner pdu
ip->inner_pdu(tcp);
// ip is eth's inner pdu
eth.inner_pdu(ip);
}
方法 PDU::inner_pdu(PDU*) 将给定的参数设置为被调用方的内部PDU。作为参数传递的对象必须是使用 operator new 分配的,从那时起,该PDU现在由其父单元拥有,这意味着该对象的销毁将由其父单元处理。因此,在上面的示例中没有实际的内存泄漏。在eth的析构函数中,分配的IP和TCP对象都将被销毁,它们的内存将被释放,这个库其实帮我们管理了一部分内存。
如果我们想存储一个副本而不是实际的指针,我们可以使用 PDU::clone 函数,它返回PDU的具体类型的副本,包括它所有堆叠的内部PDU,这样就避免了隐式共享的问题。
有一种更简单的方法来嵌套pdu。对于使用scapy的用户,我们可能习惯于使用除法运算符创建PDU堆栈。libtin也支持这一点!
上面的代码可以重写如下:
#include <tins/tins.h>
using namespace Tins;
int main() {
// Simple stuff, no need to use pointers!
EthernetII eth = EthernetII() / IP() / TCP();
// Retrieve a pointer to the stored TCP PDU
TCP *tcp = eth.find_pdu<TCP>();
// You can also retrieve a reference. This will throw a
// pdu_not_found exception if there is no such PDU in this packet.
IP &ip = eth.rfind_pdu<IP>();
}
注意,在上面的例子中创建的IP和TCP临时对象是使用PDU::clone()方法克隆的。
2. 地址类
IP地址和硬件地址都使用IPv4Address, IPv6Address和 HWAddress<> 类处理。所有这些类都可以由std::string或c-string 字符串去构造,其中包含适当的表示形式(点表示法表示IPv4Address,分号表示法表示IPv6Addresses等)。
std::string lo_string("127.0.0.1");
IPv4Address lo("127.0.0.1");
IPv4Address empty; // represents the address 0.0.0.0
// IPv6
IPv6Address lo_6("::1");
// Write it to stdout
std::cout << "Lo: " << lo << std::endl;
std::cout << "Empty: " << empty << std::endl;
std::cout << "Lo6: " << lo_6 << std::endl;
这个地址可以隐式地转换为整型值,但这是在库中使用的,所以我们不必担心它。如上所述,我们可以注意到,默认构造的IPv4Address对应于点标记的地址0.0.0.0。
这些类还提供了一个uint32_t类型的构造函数,这在为函数/构造函数的某些参数使用默认值时非常有用。在上面示例的最后几行中,将IPv4和IPv6地址都写入标准输出。这些类定义了输出操作符(operator<<),因此更容易序列化它们。
HWAddress<>类模板定义如下:
template<size_t n, typename Storage = uint8_t>
class HWAddress;
其中n个非类型模板参数表示地址的长度(对于网络接口通常为6),而Storage模板参数表示这n个元素中的每个元素的类型(通常不应该改变,uint8_t应该这样做)。
HWAddress对象可以由std::string、c-string、const Storage*和任意长度的HWAddress组成。它们也可以比较是否相等,并提供一些辅助函数来允许对地址进行迭代:
HWAddress<6> hw_addr("01:de:22:01:09:af");
std::cout << hw_addr << std::endl;
std::cout << std::hex;
// prints individual bytes
for (auto i : hw_addr) {
std::cout << static_cast<int>(i) << std::endl;
}
3. 地址范围类
libtin还支持地址范围。这对于几个目的非常有用,例如将流量分类到不同的子网中。
创建地址范围是非常直观的,使用斜线点或网络掩码,很像计算机网络的掩码表示方法:
/* IPv4 */
// 192.168.1.0-255
IPv4Range range1 = IPv4Address("192.168.1.0") / 24;
// Same as above
IPv4Range range2 = IPv4Range::from_mask("192.168.1.0", "255.255.255.0");
/* IPv6 */
// dead:0000:0000:0000:0000:0000:0000:0000-00ff
IPv6Range range3 = IPv6Address("dead::") / 120;
// Same as above
IPv6Range range4 = IPv6Range::from_mask("dead::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00");
现在,我们能用地址范围做什么?我们既可以迭代它,也可以询问它是否有一个特定的地址在该网络内:
IPv4Range range = IPv4Address("192.168.1.0") / 24;
range.contains("192.168.1.250"); // Yey, it belongs to this network, return true
range.contains("192.168.0.100"); // NOPE, return false
// print all address
for (const auto &addr : range) {
std::cout << addr << std::endl;
}
但是等等,还有更多。我们还可以创建硬件地址范围。为什么这个有用?使用它,我们可以使用OUI说明符来确定哪个是特定网络设备的供应商:
// Some OUI which belongs to Intel
auto range = HWAddress<6>("00:19:D1:00:00:00") / 24;
// Does this address belong to Intel?
if (range.contains("00:19:d1:22:33:44")) {
std::cout << "It's Intel!" << std::endl;
}
4. 网络接口
这里回顾的最后一个helper类是NetworkInterface。该类表示网络接口的抽象。它可以从网口的名称(作为字符串)或者IPv4Address 构造。最后一个构造函数创建了一个接口,如果某个数据包被发送到给定的ip地址,该接口将作为网关:
NetworkInterface lo("lo");
// this would be lo
NetworkInterface lo1(IPv4Address("127.0.0.1"));
我们还可以使用NetworkInterface::name()检索网络接口的名称。请注意,该函数在每次调用时都会搜索系统的接口并检索名称,因此最好调用它一次并存储返回值,这样节约时间。
5. 写pcap文件
pcap 文件就是 Wireshark的保存网络数据包的一种格式。
参考 https://wiki.wireshark.org/Development/LibpcapFileFormat#File_Format
向pcap文件写入数据包也非常简单。packketwriter类接受要在其中存储数据包的文件的名称作为参数,以及一个数据链路类型,该类型指示哪一层将被写入文件。这意味着,如果我们正在编写EthernetII pdu,我们应该使用DataLinkType<EthernetII>标志,而在无线接口上,我们应该使用DataLinkType<RadioTap>或DataLinkType<Dot11>,具体的取值取决于设备中使用的封装。
// We'll write packets to /tmp/test.pcap. Use EthernetII as the link
// layer protocol.
PacketWriter writer("/tmp/test.pcap", DataLinkType<EthernetII>());
// Now create another writer, but this time we'll use RadioTap.
PacketWriter other_writer("bleh.pcap", DataLinkType<RadioTap>());
创建PacketWriter后,可以使用PacketWriter::write方法向其写入pdu。这个方法包含2个重载:一个接受一个PDU&,另一个接受两个模板转发迭代器,start和end。后者将遍历范围[start, end]并写入存储在范围每个位置的pdu。如果*start产生一个PDU&,或者解解它几次导致一个PDU&,这都可以工作。这意味着std::vector<std::unique_ptr>::迭代器也可以工作。
这个例子创建了一个std::vector,其中包含一个EthernetII PDU,并使用两个重载将其写入pcap文件:
#include <tins/tins.h>
#include <vector>
using namespace Tins;
int main() {
// We'll write packets to /tmp/test.pcap. The lowest layer will be
// EthernetII, so we use the appropriate identifier.
PacketWriter writer("/tmp/test.pcap", DataLinkType<EthernetII>());
// A vector containing one EthernetII PDU.
std::vector<EthernetII> vec(1, EthernetII("00:da:fe:13:ad:fa"));
// Write the PDU(s) in the vector(only one, in this case).
writer.write(vec.begin(), vec.end());
// Write the same PDU once again, using another overload.
writer.write(vec[0]);
}
现在使用上面列出的大多数类来创建一个数据包并发送它:
#include <tins/tins.h>
#include <cassert>
#include <iostream>
#include <string>
using namespace Tins;
int main() {
// We'll use the default interface(default gateway)
NetworkInterface iface = NetworkInterface::default_interface();
/* Retrieve this structure which holds the interface's IP,
* broadcast, hardware address and the network mask.
*/
NetworkInterface::Info info = iface.addresses();
/* Create an Ethernet II PDU which will be sent to
* 77:22:33:11:ad:ad using the default interface's hardware
* address as the sender.
*/
EthernetII eth("77:22:33:11:ad:ad", info.hw_addr);
/* Create an IP PDU, with 192.168.0.1 as the destination address
* and the default interface's IP address as the sender.
*/
eth /= IP("192.168.0.1", info.ip_addr);
/* Create a TCP PDU using 13 as the destination port, and 15
* as the source port.
*/
eth /= TCP(13, 15);
/* Create a RawPDU containing the string "I'm a payload!".
*/
eth /= RawPDU("I'm a payload!");
// The actual sender
PacketSender sender;
// Send the packet through the default interface
sender.send(eth, iface);
}
该数据包的创建可以在一行中完成,使用operator/而不是operator/=:
// same as above, just shorter
EthernetII eth = EthernetII("77:22:33:11:ad:ad", info.hw_addr) /
IP("192.168.0.1", info.ip_addr) /
TCP(13, 15) /
RawPDU("I'm a payload!");
三、嗅探
1.嗅探基础
嗅探是通过Sniffer类完成的。这个类接受一个libpcap字符串过滤器,让我们去对某些网络设备抓包。
一旦设置了过滤器,就有两个函数允许检索嗅探包。
- Sniffer::next_packet
- Sniffer::sniff_loop
第一个是Sniffer::next_packet。这个成员函数允许我们使用提供的过滤器来检索数据包:
// We want to sniff on eth0. This will capture packets of at most 64 kb.
Sniffer sniffer("eth0");
// Only retrieve IP datagrams which are sent from 192.168.0.1
sniffer.set_filter("ip src 192.168.0.1");
// Retrieve the packet.
PDU *some_pdu = sniffer.next_packet();
// Do something with some_pdu...
....
// Delete it.
delete some_pdu;
2. 嗅探器配置
从3.2版开始,有一个类表示可以提供给嗅探器的不同参数,以影响嗅探会话。它们都是不同libpcap函数的包装器,比如pcap_setfilter、pcap_set_promise等。这是对其他Sniffer构造函数所采用的许多参数的改进。
例如,如果我们想捕获端口80上的数据包,在混杂模式下嗅探并设置快照长度为400字节,我们可以这样做:
// Create sniffer configuration object.
SnifferConfiguration config;
config.set_filter("port 80");
config.set_promisc_mode(true);
config.set_snap_len(400);
// Construct a Sniffer object, using the configuration above.
Sniffer sniffer("eth0", config);
注意:如果我们注意到嗅探到的数据包是突发的,或者它们的捕获有延迟(例如1秒),这很可能是由于libpcap >= v1.5默认使用缓冲模式。如果我们希望尽可能快地获取数据包,请确保使用 SnifferConfiguration::set_immediate_mode 使用立即模式。
3. 循环嗅探
除了Sniffer::next_packet之外,还有另一种方法可以从Sniffer对象中提取数据包。很常见的情况是,我们希望嗅探大量数据包,直到满足某些特定条件。在这种情况下,最好使用Sniffer::sniff_loop。
该方法接受一个模板函函数作为实参,实参必须定义一个具有以下签名之一的操作符:
bool operator()(PDU&);
bool operator()(const PDU&);
// These are only allowed when compiling in C++11 mode.
bool operator()(Packet&);
bool operator()(const Packet&);
对 Sniffer::sniff_loop 的调用将使嗅探器开始处理数据包。将使用每个处理过的包作为其参数调用回调函数。如果在某个时刻,我们想停止嗅探,那么我们的回调函数应该返回false。如果继续返回true, 那么Sniffer对象将继续循环。
函数对象将是复制构造的,因此它必须实现复制语义。有一个辅助模板函数,它接受一个指向模板参数类型对象的指针和一个成员函数,并返回一个HandlerProxy。该对象实现了所需的操作符,在该操作符中,它使用给定的对象指针将调用转发给所提供的成员函数指针:
#include <tins/tins.h>
using namespace Tins;
bool doo(PDU&) {
return false;
}
struct foo {
void bar() {
SnifferConfiguration config;
config.set_promisc_mode(true);
config.set_filter("ip src 192.168.0.100");
Sniffer sniffer("eth0", config);
/* Uses the helper function to create a proxy object that
* will call this->handle. If you're using boost or C++11,
* you could use boost::bind or std::bind, that will also
* work.
*/
sniffer.sniff_loop(make_sniffer_handler(this, &foo::handle));
// Also valid
sniffer.sniff_loop(doo);
}
bool handle(PDU&) {
// Don't process anything
return false;
}
};
int main() {
foo f;
f.bar();
}
正如我们所看到的,使用 Sniffer::sniff_loop 进行嗅探不仅是处理多个数据包的简单方法,而且在使用类时还可以使代码更加整洁。
在上面的例子中,我们知道我们正在嗅探IP地址 192.168.0.100 发送的IP pdu,但是我们的函数接受一个PDU&。我们想要搜索存储在参数内的IP PDU(可能是EthernetII类型)。幸运的是,我们可以要求PDU在其整个PDU堆栈(包括其自身)中搜索某个PDU类型,并返回对它的引用,也就是会一直找。如果报文中没有找到PDU,则抛出pdu_not_found异常:
bool doo(PDU &some_pdu) {
// Search for it. If there is no IP PDU in the packet,
// the loop goes on
const IP &ip = some_pdu.rfind_pdu<IP>(); // non-const works as well
std::cout << "Destination address: " << ip->dst_addr() << std::endl;
// Just one packet please
return false;
}
void test() {
SnifferConfiguration config;
config.set_promisc_mode(true);
config.set_filter("ip src 192.168.0.100");
Sniffer sniffer("eth0", config);
sniffer.sniff_loop(doo);
}
使循环嗅探机制优于逐个获取数据包的另一个原因是异常处理。Sniffer::sniff_loop捕获函函数体中抛出的pdu_not_found和malformmed_packet异常。这意味着我们可以使用PDU::rfind_pdu,而不必担心是否找不到这样的PDU,因为嗅探器会捕捉到异常,嗅探会话将继续。
在Windows上有点区别,需要去官网看Windows额外嗅探部分,会有一些平台的配置。
4. 使用迭代器嗅探
还有另一种方法可以从Sniffer对象中检索数据包。这个类定义了begin()和end()两个方法,它们返回前向迭代器。这些可以用来在嗅探数据包时检索数据包:
Sniffer s = ...;
for (auto &packet : s) {
// packet is a Packet&
process(packet);
}
6. 包对象
如果我们需要存储PDU和时间戳对象,那么我们应该使用Packet类。数据包包含PDU和时间戳,可以复制和移动。
让我们来看一个例子,在这个例子中,我们将从网络中读取的10个数据包存储到一个向量中:
#include <vector>
#include <tins/tins.h>
using namespace Tins;
int main() {
std::vector<Packet> vt;
Sniffer sniffer("eth0");
while (vt.size() != 10) {
// next_packet returns a PtrPacket, which can be implicitly converted to Packet.
vt.push_back(sniffer.next_packet());
}
// Done, now let's check the packets
for (const auto& packet : vt) {
// Is there an IP PDU somewhere?
if (packet.pdu()->find_pdu<IP>()) {
// Just print timestamp's seconds and IP source address
std::cout << "At: " << packet.timestamp().seconds()
<< " - " << packet.pdu()->rfind_pdu<IP>().src_addr()
<< std::endl;
}
}
}
我们可能已经注意到,Packet对象也可以与Sniffer::next_packet一起使用:
Sniffer sniffer("eth0");
// PDU pointer, as mentioned at the beginning
std::unique_ptr<PDU> pdu_ptr(sniffer.next_packet());
// auto cleanup, no need to use pointers!
Packet packet = sniffer.next_packet();
// If there was some kind of error, packet.pdu() == nullptr,
// so we need to check that.
if (packet) {
process_packet(packet); // whatever
}
在Sniffer::sniff_loop上使用的函子对象也可以接受数据包,但只有在c++ 11模式下编译时才可以。
7. 读取pcap文件
读取pcap格式的文件非常简单。FileSniffer类以要打开的文件的名称作为参数,并允许我们处理其中的数据包。Sniffer和FileSniffer都继承自BaseSniffer, BaseSniffer是实际实现next_packet和sniff_loop的类。因此,我们可以像在上面的例子中使用Sniffer一样使用FileSniffer类:
#include <tins/tins.h>
#include <iostream>
#include <stddef.h>
using namespace Tins;
size_t counter(0);
bool count_packets(const PDU &) {
counter++;
// Always keep looping. When the end of the file is found,
// our callback will simply not be called again.
return true;
}
int main() {
FileSniffer sniffer("/tmp/some_pcap_file.pcap");
sniffer.sniff_loop(count_packets);
std::cout << "There are " << counter << " packets in the pcap file\n";
}
8. 包的解析
既然我们已经了解了从网络接口读取pcap文件和嗅探的方法,那么我们将了解如何执行数据包解释。
每次从其中一个源读取数据包时,都会创建该源的链路层类型的对象(EthernetII, RadioTap等)。这些类型的对象中的每一个都根据其内部标志检测哪一个是下一个PDU的类型,创建它,将它添加为子PDU,并传播相同的动作。
除传输层协议外,每个实例化的PDU都执行此操作。这意味着,例如,如果从以太网接口嗅探到DNS数据包,我们将得到以下结构:
然后我们可以使用RawPDU的有效载荷来解释构造DNS对象的DNS数据包:
// This is a handler used in Sniffer::sniff_loop
bool handler(const PDU& pkt) {
// Lookup the UDP PDU
const UDP &udp = pkt.rfind_pdu<UDP>();
// We need source/destination port to be 53
if (udp.sport() == 53 || udp.dport() == 53) {
// Interpret it as DNS. This might throw, but Sniffer catches it
DNS dns = pkt.rfind_pdu<RawPDU>().to<DNS>();
// Just print out each query's domain name
for (const auto &query : dns.queries()) {
std::cout << query.dname() << std::endl;
}
}
return true;
}
对于其他协议(如DHCP),应该使用相同的机制。如果我们想知道为什么传输层pdu不能自动解释应用层协议,原因是效率。应用层协议,如DNS,需要比底层协议更多的处理才能解析它们。此外,有些应用程序甚至可能不需要使用这些协议,因此让它们为额外的处理,这样的开销是不划算的。
四、发送包
PacketSender类负责在网络上发送数据包。在内部,它为不同的套接字层(例如2层和3层)存储原始套接字。
当调用PacketSender::send(PDU&)时,PDU参数被序列化成一个字节数组,并通过相应的套接字发送。
1. 发送网络层pdu
发送网络层pdu,如IP和IPv6非常直观:
PacketSender sender;
IP pkt = IP("192.168.0.1") / TCP(22) / RawPDU("foo");
sender.send(pkt);
注意,在IP构造函数中没有指定源地址。默认情况下使用地址0.0.0.0。但是,在发送网络层PDU时,如果源地址为0.0.0.0,则PDU会在路由表中查找哪个应该是源地址,并自动设置。这已经由网络驱动程序完成了,但是一些传输层协议(如TCP)在计算校验和时需要这个地址,因此也必须由库完成,也就是要自己组装。
2. 发送链路层pdu
在发送链路层pdu(如etherneii)时,还有一件事应该记住。在这种情况下,数据包必须通过特定的网络接口发送。我们可以在发送时指定:
PacketSender sender;
EthernetII pkt = EthernetII() / IP() / TCP() / RawPDU("foo");
sender.send(pkt, "eth0"); // send it through eth0
// if you're sending multiple packets, you might want to create
// the NetworkInterface object once
NetworkInterface iface("eth0");
sender.send(pkt, iface);
这将通过eth0接口发送数据包。
使用同一个网络接口发送多个数据包是很常见的。PacketSender包含一个默认接口,当使用PacketSender::send(PDU&)过载时,在该接口中发送链路层PDU。
PacketSender sender("eth0");
EthernetII pkt = EthernetII() / IP() / TCP() / RawPDU("foo");
sender.send(pkt); // send it through eth0 as well
std::cout << sender.default_interface().name() << std::endl;
sender.default_interface("eth1");
sender.send(pkt); // now we're sending through eth1.
注意,必须带有网口信息发送,不然缺省情况下该PacketSender 无效,需要在发送链路层pdu之前进行设置,下面就是没指定 eth0 或者 eth1 如下图所示:
PacketSender sender;
EthernetII pkt = EthernetII() / IP() / TCP() / RawPDU("foo");
sender.send(pkt); // throws invalid_interface
3. 发送和接收响应
到目前为止,我们已经了解了如何发送数据包,但是如果我们期望对该数据包进行响应,该怎么办呢?让我们以ARP请求为例。发送后,我们很可能希望收到响应。
这可以通过在发送数据包时嗅探来实现,检查每个嗅探的数据包,直到找到响应。然而,为了匹配数据包响应,有必要执行几个协议相关的比较。在ARP响应的情况下,这将相当简单。但是,其他协议需要检查目的地址和源地址、端口、标识号等。
幸运的是,库中已经包含了发送和接收机制。这可以通过使用PacketSender::send_recv来实现,它提供了两个重载:
PDU *send_recv(PDU &pdu);
PDU *send_recv(PDU &pdu, const NetworkInterface &iface);
NetworkInterface参数的作用与PacketSender::send相同。
让我们看看如何使用它来执行ARP请求并接收其响应:
// The address to resolve
IPv4Address to_resolve("192.168.0.1");
// The interface we'll use, since we need the sender's HW address
NetworkInterface iface(to_resolve);
// The interface's information
auto info = iface.addresses();
// Make the request
EthernetII eth = ARP::make_arp_request(to_resolve, info.ip_addr, info.hw_addr);
// The sender
PacketSender sender;
// Send and receive the response.
std::unique_ptr<PDU> response(sender.send_recv(eth, iface));
// Did we receive anything?
if (response) {
const ARP &arp = response->rfind_pdu<ARP>();
std::cout << "Hardware address: " << arp.sender_hw_addr() << std::endl;
}
注意,在PacketSender::send_recv中,从套接字读取的数据包将与发送的数据包进行匹配,直到找到有效的数据包。
顺便说一句,硬件地址可以很容易地解析,使用Utils::resolve_hwaddr:
// The sender
PacketSender sender;
// Will throw std::runtime_error if resolving fails
HWAddress<6> addr = Utils::resolve_hwaddr("192.168.0.1", sender);
std::cout << "Hardware address: " << addr << std::endl;
回到发送和接收机制,我们也可以用它来确定TCP端口是否打开:
// The sender
PacketSender sender;
// The SYN to be sent.
IP pkt = IP("192.168.0.1") / TCP(22, 1337);
pkt.rfind_pdu<TCP>().set_flag(TCP::SYN, 1);
// Send and receive the response.
std::unique_ptr<PDU> response(sender.send_recv(pkt));
// Did we receive anything?
if (response) {
TCP &tcp = response->rfind_pdu<TCP>();
if (tcp.get_flag(TCP::RST)) {
std::cout << "Port is closed!" << std::endl;
}
else {
std::cout << "Port is open!" << std::endl;
}
}
作为最后一个例子,下面的代码使用PacketSender::send_recv解析域名:
// The sender
PacketSender sender;
// The DNS request
IP pkt = IP("8.8.8.8") / UDP(53, 1337) / DNS();
// Add the query
pkt.rfind_pdu<DNS>().add_query({ "www.google.com", DNS::A, DNS::IN });
// We want the query to be resolverd recursively
pkt.rfind_pdu<DNS>().recursion_desired(1);
// Send and receive the response.
std::unique_ptr<PDU> response(sender.send_recv(pkt));
// Did we receive anything?
if (response) {
// Interpret the response
DNS dns = response->rfind_pdu<RawPDU>().to<DNS>();
// Print responses
for (const auto &record : dns.answers()) {
std::cout << record.dname() << " - " << record.data() << std::endl;
}
}
校验和计算
在上面的示例中,使用的一些协议(如IP和TCP)包含校验和字段。此校验和必须在每次发送数据包时计算。libtins会自动完成:每次数据包被序列化时(在PacketSender::send中),都会计算校验和;所以我们没有必要担心他们。
线程安全
需要注意的一点是,原始套接字打开操作不是线程安全的,所以如果我们有多个编写器,我们应该显式地自己打开所需的套接字(这可以通过PacketSender::open_l2_socket和PacketSender::open_l3_socket来完成)。否则,套接字将在需要时打开。
五、TCP 流
从3.4版开始,libtin提供了一组类,这些类允许以一种非常简单但功能强大的方式重组TCP流。在引入这些类之前,有一个TCPStreamFollower类可以完成这种工作,但是以一种不那么可扩展的不可用的方式。
这些新类的目标是提供一种非常简单的方式来跟踪流,处理数据,获取属性等等,使用一个简单的基于回调的接口。流将处理乱序数据,重新组合,并让用户处理它,而不必处理数据包、有效负载、序列号等。
所有这些类都需要使用c++ 11,因为它们使用std::function作为指定回调的方式。因此,我们应该使用一些相当新的编译器来使用它。如果我们使用的是GCC, 4.6可能就足够了,甚至可能是更旧的版本。
1. StreamFollower
我们应该知道的主要类是StreamFollower。这个类将处理TCP数据包,查看其中使用的IP地址和端口。每当看到一个新的4元组(客户端地址、客户端端口、服务器地址、服务器端口)时,它将为该TCP流创建一些上下文,并执行用户提供的回调来通知它的创建。之后,属于该流的所有数据包将被转发到正确的对象,让它处理数据并更新其内部状态。
StreamFollower的另一个职责是检测流中的错误。假设我们有一个高丢包(例如,我们的程序不能足够快地处理数据包),我们不想为永远不会重新组装的流保持缓冲数据,或者为实际上关闭但FIN/RST数据包没有捕获的流存储状态和数据。由于这个原因,这个类将检测这些事件(太多的缓冲数据包,流超时等),并在发生这种情况时删除它们的状态。
作为一个简单的例子,如何创建一个StreamFollower并设置一些回调:
#include <tins/tcp_ip/stream_follower.h>
using Tins::TCPIP::Stream;
using Tins::TCPIP::StreamFollower;
// New stream is seen
void on_new_stream(Stream& stream) {
}
// A stream was terminated. The second argument is the reason why it was terminated
void on_stream_terminated(Stream& stream, StreamFollower::TerminationReason reason) {
}
// Create our follower
Tins::TCPIP::StreamFollower follower;
// Set the callback for new streams. Note that this is a std::function, so you
// could use std::bind and use a member function for this
follower.new_stream_callback(&on_new_stream);
// Now set up the termination callback. This will be called whenever a stream is
// stopped being followed for some of the reasons explained above
follower.stream_termination_callback(&on_stream_terminated);
// Now create some sniffer
Sniffer sniffer = ...;
// And start sniffing, forwarding all packets to our follower
sniffer.sniff_loop([&](PDU& pdu) {
follower.process_packed(pdu);
return true;
})
注意,StreamFollower::process_packet有另一个接受Packet的重载。我们应该尝试使用此过载,因为它将使流在实际数据包时间超时,而不是使用系统时钟。
这是重组TCP流的第一步。在接下来的部分中,我们将看到如何使用它做一些有用的事情。
2. 使用流
一旦在StreamFollower上配置了新流的回调,我们可能想要对新流做一些事情。流允许我们为流上发生的不同事件配置回调。
每当有新的、准备处理的数据时,就会生成数据事件。这意味着具有下一个预期序列号的数据包到达并且其有效负载可用,加上之前可能已经接收但由于第一个数据包的数据丢失而无法处理的所有乱序有效负载。
我们可以选择订阅客户机和服务器数据事件。这意味着,当每个流上有来自客户端或服务器的新数据时,我们可以在不同的回调上收到通知。让我们在一个简短的例子中使用它:
// This will be called when there's new client data
void on_client_data(Stream& stream) {
// Get the client's payload, this is a vector<uint8_t>
const Stream::payload_type& payload = stream.client_payload();
// Now do something with it!
}
// This will be called when there's new server data
void on_server_data(Stream& stream) {
// Process the server's data
}
// New stream is seen
void on_new_stream(Stream& stream) {
// Configure the client and server data callbacks
stream.client_data_callback(&on_client_data);
stream.server_data_callback(&on_server_data);
// Done!
}
就是这样,我们之前构造的StreamFollower将继续处理数据包并将它们转发给正确的Stream对象,这些对象将在适当的时候执行这些回调。
我们也可以订阅每个流上的其他事件。其中之一是close事件,它在流正确关闭时执行。我们可以通过调用Stream:: stream_close_callback来实现。
3. 处理流数据
现在我们已经了解了如何使用流的基础知识,让我们看看其他一些特性。
默认情况下,每当流中有新数据可用时,该数据将被移动到流的有效负载中,数据回调将被执行,然后该数据将被擦除。这样做是为了使数据不会开始缓冲,从而使内存使用量上升,直到流关闭(或者内存耗尽)。如果我们想缓冲数据并使用我们自己的处理方式,那么我们应该调用以下函数:
// New stream is seen
void on_new_stream(Stream& stream) {
// Disables auto-deleting the client's data after the callback is executed
stream.auto_cleanup_client_data(true);
// Same thing for the server's data
stream.auto_cleanup_server_data(true);
// Or a shortcut to doing this for both:
stream.auto_cleanup_payloads(true);
}
如果我们只计划处理客户端的数据而不是服务器的数据,那么我们应该调用ignore_client/server_data。否则,即使我们没有设置回调,数据仍然会被缓冲,并根据需要重新排序:
// New stream is seen
void on_new_stream(Stream& stream) {
// We don't even want to buffer the client's data
stream.ignore_client_data():
}
4. 结论
这应该使我们对如何使用StreamFollower和Stream类有了一个相当好的介绍。我们可以查看HTTP请求示例.
六、协议
libtin提供了对几种网络协议的支持。文档包含关于库中存在的每个类、方法和函数的信息。但是,我们可能不希望仅仅为了学习如何制作一个简单的TCP数据包而阅读整个文档。
在本节中,我们将了解我们在家庭网络中最可能看到的一些协议是如何在库中实现的。
1. Ethernet II
Ethernet II 协议由以太II类表示,实际上非常简单。它只包含获取和设置目标地址和源地址以及有效负载类型的方法。它还包含一个构造函数,可以让我们选择性地指定这两个地址:
// Both addresses are 00:00:00:00:00:00
EthernetII eth;
eth.dst_addr("01:02:03:04:05:06");
eth.src_addr("00:01:02:03:04:05");
// Same as above, just shorter
EthernetII eth2("01:02:03:04:05:06", "00:01:02:03:04:05");
2. IP
IP类包含更多的方法,既用于访问协议字段,也用于添加和检索存储的选项。在这个例子中,我们将修改其中的一些字段:
// Both addresses are 0.0.0.0
IP ip;
ip.dst_addr("192.168.0.100");
ip.src_addr("192.168.0.50");
// Same as above, just shorter
IP ip2("192.168.0.100", "192.168.0.50");
// Set the time-to-live attribute
ip2.ttl(10);
// Set the type-of-service attribute
ip2.tos(3);
IP是支持TLV(类型-长度-值)编码选项的众多协议之一。这意味着存储在协议中的每个选项都包含一个表示其类型的字段,另一个保存其长度,第三个包含实际数据。
每个包含选项的类(如IP、TCP和DHCP等)的行为都是相同的。对于任何包含选项的类型T, T::option是实际存储选项的类型。每个协议提供两个成员函数:
- 它给那个PDU添加了一个选项。
void T::add_option(const T::option&)
- 找到一个选项。
const T::option* T::search_option(T::option::option_type)
使用这些成员函数,必须知道组成每个选项类型的字段的长度及其端序。由于这非常麻烦,因此这些协议都为每个有效选项提供了一个getter和一个setter。如果该选项不存在于PDU中,getter将总是抛出option_not_found异常。
在IP的情况下,这些是一些支持的getter /setter选项:
IP ip;
// Sets the Stream Identifier option.
ip.stream_identifier(165);
// Sets the Record Route option.
ip.record_route(
{ // Constructing a record_route_type object
2, // pointer
{ "192.168.0.1", "192.168.0.2" } // routes
}
);
// Retrieve the Record Route option.
IP::record_route_type routes = ip.record_route();
// Echo
std::cout << static_cast<int>(routes.pointer) << std::endl;
std::copy(
routes.routes.begin(),
routes.routes.end(),
std::ostream_iterator<IP::address_type>(std::cout, "\n")
);
// This will throw an option_not_found exception.
auto x = ip.security();
3. TCP
TCP也包含几种选项类型,因此上面提到的关于它们的所有内容仍然有效。让我们来看看如何制作一些基本的TCP帧:
// Both source and destination ports are 0.
TCP tcp;
tcp.dport(22);
tcp.sport(22334);
// Same as above
TCP tcp2(22, 22334);
// Set the sequence number
tcp.seq(0x9283);
// Set the acknowledge number
tcp.ack_seq(0x9283);
// Set the SYN flag
tcp.set_flag(TCP::SYN, 1);
// This will be available as of libtins 1.2
tcp.flags(TCP::SYN | TCP::ACK);
// Get the SYN flag
auto s = tcp.get_flag(TCP::SYN);
// This will be available as of libtins 1.2
bool is_syn_ack = (tcp.flags() == (TCP::SYN | TCP::ACK));
// Set some options
tcp.sack_permitted();
if (tcp.has_sack_permitted()) {
// whatever
}
tcp.altchecksum(TCP::CHK_8FLETCHER);