0

我正在尝试在 Python 中实现基于 ICMP 的 Traceroute。我找到了一个非常有用的指南 ( https://blogs.oracle.com/ksplice/entry/learning_by_doing_writing_your ),它允许我创建一个基于 UDP 的 Traceroute,所以只需要修改。但是,我环顾四周,无法更改发送套接字并使其工作。有人可以帮助我吗?

 #!/usr/bin/python

import socket

def main(dest_name):
    dest_addr = socket.gethostbyname(dest_name)
    port = 33434
    max_hops = 30
    icmp = socket.getprotobyname('icmp')
    udp = socket.getprotobyname('udp')
    ttl = 1
    while True:
        recv_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)
        send_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, udp)
        send_socket.setsockopt(socket.SOL_IP, socket.IP_TTL, ttl)
        recv_socket.bind(("", port))
        send_socket.sendto("", (dest_name, port))
        curr_addr = None
        curr_name = None
        try:
            _, curr_addr = recv_socket.recvfrom(512)
            curr_addr = curr_addr[0]
            try:
                curr_name = socket.gethostbyaddr(curr_addr)[0]
            except socket.error:
                curr_name = curr_addr
        except socket.error:
            pass
        finally:
            send_socket.close()
            recv_socket.close()

        if curr_addr is not None:
            curr_host = "%s (%s)" % (curr_name, curr_addr)
        else:
            curr_host = "*"
        print "%d\t%s" % (ttl, curr_host)

        ttl += 1
        if curr_addr == dest_addr or ttl > max_hops:
            break

if __name__ == "__main__":
    main('google.com')
4

3 回答 3

4

不知道你为什么选择 scapy (虽然它是一个不错的模块),因为这当然可以只使用 python。要发送 ICMP 数据包,您只需发送 recv_socket。为了发送这个套接字,您需要首先创建一个 ICMP 数据包。

但是,您似乎想要通过 ICMP 套接字发送 UDP 数据包。这不会像你想象的那样工作。

首先,让我说显然存在一个 Linux 内核补丁,它允许 SOCK_DGRAM 和 IPPROTO_ICMP: ICMP sockets (linux)。我没有测试过这个。

但是,一般来说,这种套接字标志的组合是行不通的。这是因为 ICMP 套接字需要一个 ICMP 标头。如果您要通过 recv_socket 发送一个空字符串,类似于 send_socket,内核将丢弃该数据包。此外,如果您要在 ICMP 标头上叠加 UDP 标头,则接收系统只会对收到的 ICMP 标头做出反应,并将 UDP 标头视为附加到 ICMP 的数据。事实上,在它给你的 ICMP 回复中,远程系统会将你的 UDP 报头简单地包含为你发送给它的数据。

您能够通过 send_socket 发送空字符串的原因是内核已经为您创建了 UDP 标头,您通过该 UDP 套接字发送的内容只是作为数据附加到 UDP 标头。与 ICMP 套接字不同。正如我所写,您需要创建 icmp 标头以在此套接字上发送。

UDP“ping”中发生的实际情况是这样的:UDP数据包通过UDP套接字发送到远程系统,使用(希望)未打开的端口作为目标端口。这会引发来自远程系统的 ICMP 响应(类型 3,代码 3)。此时,您将需要一个 ICMP 注册的套接字来处理 ICMP 回复,这需要 root(或 Windows 上的管理员)权限。

创建 icmp 回显头非常简单:

import struct
icmp = struct.pack(">BBHHH", 8, 0, 0, 0, 0)
icmp = struct.pack(">BBHHH", 8, 0, checksum(icmp), 0, 0)

这个标题有几点需要注意。首先,参数四和五是“标识符”和“序列号”字段。通常,标识符设置为进程 ID,而序列号从 1 开始,因此您可以跟踪发送到回复的序列。在这个例子中,我将数字设置为零只是为了说明(尽管如果你不关心它们,保持这种方式是完全正常的)。

其次,看到我提到的 checksum(icmp) 函数了吗?我假设您可以访问执行此操作的代码。它被称为 1 的补码校验和,并在 RFC 1071 下得到批准。校验和至关重要,因为如果没有正确计算,接收系统可能不会将数据包转发到任何 ICMP 套接字处理程序。

第三,如果您想知道为什么我最初创建带有零校验和的标头,那是因为内核在创建实际校验和并将其添加回标头之前基于该字段为零的校验和结果。

因为 struct 模块处理所谓的“打包”二进制数据,所以我建议您熟悉位移位和位运算符。当您进一步进入原始套接字时,您将需要这个,特别是当您需要从可能重叠或可能不重叠的位字段中提取某些位时。例如,以下是 IP 标头:

    0                   1                   2                   3   
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 0 |Version|  IHL  |Type of Service|          Total Length         |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 4 |         Identification        |Flags|      Fragment Offset    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 8 |  Time to Live |    Protocol   |         Header Checksum       |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
12 |                       Source Address                          |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
16 |                    Destination Address                        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
20 |                    Options                    |    Padding    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

假设您想在源字段中插入 IP 地址 192.168.1.10,首先您必须注意它的长度,4 个字节。要将其正确插入到结构中(仅此字段,而不是标题的其余部分,例如),您必须:

struct.pack(">I", 192 << 24| 168 << 16| 1 << 8| 10)

这样做会为该字段添加正确的整数 3232235786L。

(有些读者可能会指出 struct.pack(">BBBB", 192, 168, 1, 10 可能会得到相同的结果)。虽然这适用于这个用例,但一般来说它是不正确的。在这种情况下, 因为IP地址是面向字节的,所以有效;但是,不面向字节的字段,例如校验和字段,将失败,因为校验和的结果值大于255,也就是说,它是预期的是一个 16 位的值。所以一般不要这样做,即使用协议字段期望的确切位。)

作为学习位移和操作的另一个例子,以IP头的VER字段为例。这是一个半字节大小的字段,即 4 位。为了提取这一点,您将执行以下示例(假设国际人道法为零):

# Assume ip[] already has a full IP header.
ver = struct.unpack("!B", ip[0])[0]

# This produces the integer 4, which is required for sending IPV4
ver >> 4 

简而言之,我将一个单字节值右移了 4 位。在此降档中,与旧值相比,新值现在如下所示:

# OLD VALUE IN BINARY; value is 64
# 01000000

# NEW VALUE IN BINARY; value is 4
# 00000100

需要注意的是,如果没有这种转变,检查原始二进制值将导致 64,这不是预期的。相反,为了将值 4 “打包”到一个字节的高 4 位中,请执行以下操作:

# New value will be 64, or binary 01000000
x = 4 << 4

希望这能为您指明正确的方向。

于 2014-03-17T19:34:12.800 回答
0

老问题,但为了清楚起见,我最近不得不这样做。

您可以使用套接字编写本机 Python 版本的 ICMP 跟踪路由。要解决的两个重要问题是:

  1. 使用正确的校验和创建 ICMP 标头(如 Eugene 上面所述)
  2. 使用 sockopts 设置套接字的 TTL

对于第一个问题,pyping 模块中有一个很好的示例,它可以完美运行;这是我在编写我的 traceroute 实用程序时使用的。

对于第二个,它很简单:

current_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, 
                               socket.getprotobyname("icmp"))
current_socket.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, ttl)

其中 'ttl' 是您设置的 TTL 的整数值

然后只需在增加 TTL 的控制结构中进行发送/接收并观察返回数据包中的类型/代码。

我根据 pyping 模块中已经编写的内容建模了我的头文件包/解包;只需查找标头类型 = 11,中间跃点代码为 0,标头类型 = 0 表示您到达目的地时。

Scapy 工作正常,但速度较慢,并且是一个额外的外部依赖项。我能够在一个下午使用 AS-lookup(通过 RADb)和地理定位编写一个健壮的跟踪路由,只使用套接字。

于 2017-01-06T04:52:10.143 回答
-3

最终使用 scapy 编写了我自己的,因为这是不可能的。

于 2012-11-09T08:51:25.880 回答