在Python中创建ICMP traceroute

时间:2012-04-04 09:10:52

标签: python sockets udp icmp traceroute

我正在尝试在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')

3 个答案:

答案 0 :(得分:3)

不确定为什么你选择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套接字发送到远程系统,使用(希望)未打开的端口作为目标端口。这会从远程系统(类型3,代码3)中引发ICMP响应。此时,您将需要一个ICMP注册的套接字来处理ICMP回复,这需要root(或Windows上的管理员)权限。

创建icmp echo标头非常简单:

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

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

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

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

因为struct模块处理的是#34; packed"二进制数据,我建议您熟悉位移和位运算符。当你进一步进入原始套接字时,你需要这个,特别是当你需要从可能重叠或不重叠的位字段中提取某些位时。例如,以下是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(只是这个字段,而不是标题的其余部分,作为示例),您必须:

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

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

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

作为学习位移和操作的另一个例子,取IP头的VER字段。这是一个半字节大小的字段,即4比特。为了提取这个,你可以做以下的例子(假设IHL为零):

# 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,这不是预期的。反之,为了#34;打包&#34;值4进入一个字节的高4位,执行以下操作:

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

希望这能指出你正确的方向。

答案 1 :(得分:0)

老问题,但是为了清晰起见又增加了另一点,因为我最近不得不这样做。

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

  1. 使用正确的校验和创建ICMP标头(如Eugene在上面所述)
  2. 使用sockopts
  3. 设置套接字的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)
    

    其中&#39; ttl&#39;是您正在设置的TTL的整数值

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

    我在已经在pyping模块中编写的内容之后对我的头文件包/解压缩建模;只需查找标题类型= 11,代码0表示中间跃点,标题类型= 0表示当您到达目的地时。

    Scapy工作正常,但速度较慢,是一种额外的外部依赖。我能够在一个下午用AS查找(通过RADb)和地理定位编写一个强大的traceroute,只使用套接字。

答案 2 :(得分:-2)

结束了使用scapy编写自己的内容,因为这是不可能的。