libtins初探-抓包嗅探

news2024/11/15 19:36:28

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, IPv6AddressHWAddress<> 类处理。所有这些类都可以由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数据包,我们将得到以下结构:

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);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1944279.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【第一天】计算机网络 TCP/IP模型和OSI模型,从输入URL到页面显示发生了什么

TCP/IP模型和OSI模型 这两个模型属于计算机网络的体系结构。 OSI模型是七层模型&#xff0c;从上到下包括&#xff1a; 应用层&#xff0c;表示层&#xff0c;会话层&#xff0c;传输层&#xff0c;网络层&#xff0c;数据链路层&#xff0c;物理层 TCP/IP模型是四层模型&…

BGP选路之Next Hop

原理概述 当一台BGP路由器中存在多条去往同一目标网络的BGP路由时&#xff0c;BGP协议会对这些BGP路由的属性进行比较,以确定出去往该目标网络的最优BGP路由,然后将该最优BGP路由与去往同一目标网络的其他协议路由进行比较&#xff0c;从而决定是否将该最优BGP路由放进P路由表中…

PCB工艺边设计准则

在PCB设计时&#xff0c;通常会在电路板的边缘预留一定的空间&#xff0c;这部分空间被称为工艺边。它有助于在生产过程中确保电路板的尺寸和形状的准确性。以使得组装时更加顺畅、便捷。而工艺边的加工&#xff0c;使得线路板上的元件可以精准地与设备对接&#xff0c;从而提高…

C++学习笔记-operator关键字:重载与自定义操作符

在C编程中&#xff0c;operator关键字扮演着极其重要且独特的角色。它允许开发者为内置类型或自定义类型重载或定义新的操作符行为。这一特性极大地增强了C的表达能力&#xff0c;使得代码更加直观、易于理解和维护。本文将深入探讨C中operator关键字的使用&#xff0c;包括操作…

【ffmpeg命令入门】ffplay常用命令

文章目录 前言ffplay的简介FFplay 的基本用法常用参数及其作用示例 效果演示图播放普通视频播放网络媒体流RTSP 总结 前言 FFplay 是 FFmpeg 套件中的一个强大的媒体播放器&#xff0c;它基于命令行接口&#xff0c;允许用户以灵活且高效的方式播放音频和视频文件。作为一个简…

vscode 寻找全部分支的提交

vscode 寻找全部分支的提交 Git Graph

UE5 C++跑酷练习(Part2)

一.首先GameMode里有Actor数组&#xff0c;组装直线路&#xff0c;和左右路 #include "CoreMinimal.h" #include "GameFramework/GameModeBase.h" #include "RunGANGameMode.generated.h"UCLASS(minimalapi) class ARunGANGameMode : public AG…

【目录】8051汇编与C语言系列教程

8051汇编与C语言系列教程 作者将狼才鲸创建日期2024-07-23 CSDN文章地址&#xff1a;【目录】8051汇编与C语言系列教程本Gitee仓库原始地址&#xff1a;才鲸嵌入式/8051_c51_单片机从汇编到C_从Boot到应用实践教程 一、本教程目录 序号教程名称简述教程链接1点亮LCD灯通过IO…

【技术支持案例】使用S32K144+NSD8381驱动电子膨胀阀

文章目录 1. 前言2. 问题描述3. 理论分析3.1 NSD8381如何连接电机3.2 S32K144和NSD8381的软件配置 4.测试验证4.1 测试环境4.2 测试效果4.3 测试记录 1. 前言 最近有客户在使用S32K144NSD8381驱动电子膨胀阀时&#xff0c;遇到无法正常驱动电子膨胀阀的情况。因为笔者也是刚开…

分享一个Springer模板关于论文作者和单位信息的修改范例,以及Applied Intelligence期刊latex模板的下载链接

在这篇文章中&#xff0c;我写一些关于解决springer期刊提供的LaTex模板参考文献格式为作者年份时的顺序问题以及如何在正文中将参考文献格式引用成[1]这种数字格式类似的经验&#xff0c;该篇帖子里还分享了一个大佬关于springer模板完整的修改流程&#xff0c;有需要的伙伴可…

如何实现可视化、智能化、自动化的文件采集?一文了解

内部数据文件采集需求在多个行业中都非常重要&#xff0c;以下是一些涉及此场景需求的行业&#xff1a; 1.大数据行业&#xff1a;随着大数据的行业应用不断深入&#xff0c;物联网、智能家居、数字政务等领域的大数据技术应用逐渐成熟&#xff0c;数据采集的需求也将被逐步激…

0723,UDP通信(聪明小辉聪明小辉),HTTP协议

我就是一个爱屋及乌的人&#xff01;&#xff01;&#xff01;&#xff01; #include "network_disk_kai.h" 昨天的epoll&#xff1a; 可恶抄错代码了 epoll_s.csockect return listenfdsetsockoptsockaddr_in bind listenfd & serveraddr…

Zilliz 推出 Spark Connector:简化非结构化数据处理流程

随着人工智能&#xff08;AI&#xff09;和深度学习&#xff08;Deep Learning&#xff09;技术的高速发展&#xff0c;使用神经网络模型将数据转化为 Embedding 向量 已成为处理非结构化数据并实现语义检索的首选方法&#xff0c;广泛应用于搜索、推荐系统等 AI 业务中。 以生…

Windows 11+Visual Studio 2022 环境OpenCV+CUDA 12.5安装及踩坑笔记

周六日在家捣腾了一下&#xff0c;把过程记录下来。 前置条件 Visual Studio C 生成工具和本机显卡适配的CUDA与CUDA匹配的cuDNNPython 3NumPyOpenCV源代码以及对应版本的OpenCV-contrib模块源码CMake Visual Studio 下载Visual Studio&#xff08;我本机的是VS2022&#xf…

虚拟局域网配置与分析-VLAN

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 一、相关知识 虚拟局域网&#xff08;Virtual Local Area Network&#xff0c;VLAN&#xff09;是一组逻辑上的设备和用户&#xff1b;不受物理位置的…

二、【Python】入门 - 【PyCharm】安装教程

往期博主文章分享文章&#xff1a; 【机器学习】专栏http://t.csdnimg.cn/sQBvw 目录 第一步&#xff1a;PyCharm下载 第二步&#xff1a;安装&#xff08;点击安装包打开下图页面&#xff09; 第三步&#xff1a;科学使用&#xff0c;请前往下载最新工具及教程&#xff1a…

前端:Vue学习-3

前端&#xff1a;Vue学习-3 1. 自定义指令2. 插槽2.1 插槽 - 后备内容&#xff08;默认值&#xff09;2.2 插槽 - 具名插槽2.3 插槽 - 作用域插槽 3. Vue - 路由3.1 路由模块封装3.2 声明式导航 router-link 高亮3.3 自定义匹配的类名3.4 声明式导肮 - 跳转传参3.5 Vue路由 - 重…

C#初级——条件判断语句和循环语句

条件判断语句 简单的条件判断语句&#xff0c;if()里面进行条件判断&#xff0c;如果条件判断正确就执行语句块1&#xff0c;如果不符合就执行语句块2。 if (条件判断) { 语句块1 } else { 语句块2 } int age 18;if (age < 18){Console.WriteLine("未…

Python面试宝典第18题:单词搜索

题目 给定一个m x n的二维字符网格board和一个字符串单词word。如果word存在于网格中&#xff0c;返回true。否则&#xff0c;返回false。单词必须按照字母顺序&#xff0c;通过相邻的单元格内的字母构成。所谓相邻单元格&#xff0c;是那些水平相邻或垂直相邻的单元格。 备注&…

Blender材质-PBR与纹理材质

1.PBR PBR:Physically Based Rendering 基于物理的渲染 BRDF:Bidirection Reflectance Distribution Function 双向散射分散函数 材质着色操作如下图&#xff1a; 2.纹理材质 左上角&#xff1a;编辑器类型中选择&#xff0c;着色器编辑器 新建着色器 -> 新建纹理 -> 新…