测试ARP/ND双发效果的小工具

上一篇Blog里说了一下关于ARP/ND双发的实现,但是还遗留了一个小问题,就是如何测试最终的效果,毕竟正常情况下,ARP还有ND相关的报文,都是由内核协议栈根据需要发出的,不太稳定,总不能一直抓包等着内核发包吧?所以还是需要借助一些工具来实现。

ARP双发检测

ARP双发的测试还是比较简单的,毕竟大家都知道arping这个工具,可以用来发送ARP请求并接收ARP应答。使用起来也是非常顺畅的:

# arping 192.68.100.1 -c 2
ARPING 192.68.100.1 from 192.68.100.21 eth0
Unicast reply from 192.68.100.1 [00:11:22:33:44:01]  1.315ms
Unicast reply from 192.68.100.1 [00:11:22:33:44:01]  1.355ms
Unicast reply from 192.68.100.1 [00:11:22:33:44:01]  1.112ms
Unicast reply from 192.68.100.1 [00:11:22:33:44:01]  1.233ms
Sent 2 probes (1 broadcast(s))
Received 4 response(s)

可以发现,arping工具发送了2个ARP请求,成功地收到了4次ARP应答,这表明ARP双发功能正常。两次请求中,第一次是广播请求,可以模拟第一次学习MAC地址时的场景,第二次是单播请求,可以模拟已有MAC后的确认场景,尝试抓包,也是可以看到结果是符合预期的:

# sudo tcpdump -i eth0 arp -nnn
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes

20:16:03.982709 ARP, Request who-has 192.68.100.1 (ff:ff:ff:ff:ff:ff) tell 192.68.100.21, length 28
20:16:03.983231 ARP, Reply 192.68.100.1 is-at 00:11:22:33:44:01, length 46
20:16:03.983298 ARP, Reply 192.68.100.1 is-at 00:11:22:33:44:01, length 46
20:16:04.982738 ARP, Request who-has 192.68.100.1 (00:11:22:33:44:01) tell 192.68.100.21, length 28
20:16:04.983343 ARP, Reply 192.68.100.1 is-at 00:11:22:33:44:01, length 46
20:16:04.983412 ARP, Reply 192.68.100.1 is-at 00:11:22:33:44:01, length 46

6 packets captured
6 packets received by filter
0 packets dropped by kernel

ND双发检测

接下来轮到IPv6的ND报文了,这里介绍一个工具包NDisc6,可以部分替代arping的功能,为什么是部分替代呢?因为和arping不同,ndisc6不支持发送单播NS报文,只支持发送组播报文,这样就只能模拟第一次学习的情况,没办法模拟后续了,我们先用这个工具模拟一下组播的场景:

# sudo ndisc6 2408:fffe::1 eth0 -m
Soliciting 2408:fffe::1 (2408:fffe::1) on eth0...
Target link-layer address: 00:10:00:54:00:24
 from 2408:fffe::1
Target link-layer address: 00:10:00:54:00:24
 from 2408:fffe::1

可以看到发送一个NS,收到两个NA,说明ND双发功能也是正常的。尝试抓包,也是可以看到结果是符合预期的:

# sudo tcpdump -i eth0 icmp6 -nnn
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes

20:18:18.959590 IP6 fe80::f816:3eff:fefa:556f > ff02::1:ff00:1: ICMP6, neighbor solicitation, who has 2408:fffe::1, length 32
20:18:18.961594 IP6 2408:fffe::1 > fe80::f816:3eff:fefa:556f: ICMP6, neighbor advertisement, tgt is 2408:fffe::1, length 32
20:18:18.962687 IP6 2408:fffe::1 > fe80::f816:3eff:fefa:556f: ICMP6, neighbor advertisement, tgt is 2408:fffe::1, length 32

3 packets captured
3 packets received by filter
0 packets dropped by kernel

那单播NS报文怎么办呢?因为没找到合适的工具,因此还是借助AI自己实现了一个脚本来完成这个任务:

#!/usr/bin/env python3
"""
IPv6邻居发现工具 - 类似arping的NS/NA实现
使用Scapy发送和接收ICMPv6邻居发现报文
"""

import argparse
import time
from scapy.all import Ether, IPv6, ICMPv6ND_NS, ICMPv6ND_NA, srp, get_if_list

def send_ns(target_ip, iface=None, timeout=1, retry=1, verbose=False, dst_mac=None):
    """
    发送一个NS报文并等待所有NA响应
    :param target_ip: 目标IPv6地址
    :param iface: 网络接口名称
    :param timeout: 超时时间(秒)
    :param retry: 重试次数(仅在没有收到任何响应时重试)
    :param verbose: 详细输出模式
    :param dst_mac: 目标MAC地址(单播模式)
    :return: 响应列表 [(src_ip, src_mac), ...] 或 []
    """
    if iface and iface not in get_if_list():
        print(f"警告: 接口 {iface} 不存在")
        return []

    dst_mac = dst_mac if dst_mac else "33:33:ff:00:00:00"
    ns_pkt = Ether(dst=dst_mac) / \
             IPv6(dst=target_ip) / \
             ICMPv6ND_NS(tgt=target_ip)
    
    if verbose:
        print("发送NS报文:")
        ns_pkt.show()

    all_responses = []
    
    for attempt in range(retry):
        if verbose:
            print(f"尝试 {attempt+1}/{retry}...")
        
        # 发送一个NS报文并收集所有响应,在超时时间内持续监听
        answered, unanswered = srp(ns_pkt, iface=iface, timeout=timeout, verbose=0, multi=True)
        
        # 处理所有收到的响应
        for sent, received in answered:
            if received.haslayer(ICMPv6ND_NA):
                src_ip = received[IPv6].src
                src_mac = received[Ether].src
                response_info = (src_ip, src_mac)
                all_responses.append(response_info)
                if verbose:
                    print(f"收到NA响应 #{len(all_responses)}:")
                    received.show()
        
        # 如果收到了响应,就不再重试
        if all_responses:
            if verbose:
                print(f"收到 {len(all_responses)} 个NA响应,停止重试")
            break
        elif verbose:
            print("本轮未收到响应")
    
    return all_responses

def main():
    parser = argparse.ArgumentParser(description="IPv6邻居发现工具")
    parser.add_argument("target", help="目标IPv6地址")
    parser.add_argument("-i", "--iface", help="网络接口名称")
    parser.add_argument("-t", "--timeout", type=int, default=1,
                       help="超时时间(秒)")
    parser.add_argument("-c", "--count", type=int, default=1,
                       help="重试次数(默认1次,仅在无响应时重试)")
    parser.add_argument("-v", "--verbose", action="store_true",
                       help="详细输出模式")
    parser.add_argument("-m", "--mac",
                       help="指定目标MAC地址(单播模式)")
    args = parser.parse_args()

    if args.verbose:
        print("可用网络接口:", ", ".join(get_if_list()))

    results = send_ns(args.target, args.iface, args.timeout,
                     args.count, args.verbose, args.mac)
    if results:
        print(f"\n收到 {len(results)} 个NA响应:")
        for i, (ip, mac) in enumerate(results, 1):
            print(f"  {i}. {args.target} 的MAC地址是 {mac} (来自 {ip})")
    else:
        print(f"\n无法获取 {args.target} 的MAC地址")

if __name__ == "__main__":
    main()

借助这个脚本,就可以实现发送单播NS,并收集所有的ND返回:

# sudo python3 ip6ndisc.py 2408:fffe::1 -m 00:10:00:54:00:24

收到 2 个NA响应:
  1. 2408:fffe::1 的MAC地址是 00:10:00:54:00:24 (来自 2408:fffe::1)
  2. 2408:fffe::1 的MAC地址是 00:10:00:54:00:24 (来自 2408:fffe::1)

抓包也符合预期:

# sudo tcpdump -i eth0 icmp6 -nnn
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
14:27:25.782766 IP6 2408:fffe::21:1 > 2408:fffe::1: ICMP6, neighbor solicitation, who has 2408:fffe::1, length 24
14:27:25.784537 IP6 2408:fffe::1 > 2408:fffe::21:1: ICMP6, neighbor advertisement, tgt is 2408:fffe::1, length 32
14:27:25.784739 IP6 2408:fffe::1 > 2408:fffe::21:1: ICMP6, neighbor advertisement, tgt is 2408:fffe::1, length 32

3 packets captured
3 packets received by filter
0 packets dropped by kernel