Created
June 30, 2019 22:07
-
-
Save tomkins/c1fec82499fa273c6e1712147867bfa5 to your computer and use it in GitHub Desktop.
Python IPv6 Neighbor Advertisements
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# all-routers ff02::2 | |
# server1 fe80::f03c:91ff:1122:3344 | |
# server2 fe80::f03c:91ff:5566:7788 | |
exec /usr/local/sbin/pyv6na.py \ | |
2001:db8::1 \ | |
2001:db8::2 \ | |
--interface enp0s3 \ | |
--destination ff02::2 \ | |
fe80::f03c:91ff:1122:3344 \ | |
fe80::f03c:91ff:5566:7788 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
from __future__ import print_function | |
import argparse | |
from itertools import product | |
import logging | |
import sys | |
import threading | |
from scapy.all import ( | |
get_if_hwaddr, in6_addrtomac, in6_mactoifaceid, | |
in6_ismlladdr, Ether, IPv6, ICMPv6ND_NA, ICMPv6NDOptDstLLAddr, sendp | |
) | |
class Pyv6NA(object): | |
def __init__(self, interface, count, announce_ips, destination_ips): | |
self.logger = logging.getLogger('pyv6na') | |
self.interface = interface | |
self.announce_ips = announce_ips | |
self.destination_ips = destination_ips | |
# Need the link local IPv6 and MAC addresses for sending packets | |
local_mac = get_if_hwaddr(interface) | |
self.local_ipv6 = self.mac_to_linklocal(local_mac) | |
# One part of the packet which stays constant | |
self.lla_packet = ICMPv6NDOptDstLLAddr(lladdr=local_mac) | |
# Avoid sleeping if count is 1, or sendp will loop/sleep | |
if count == 1: | |
self.count = None | |
self.interval = 0 | |
else: | |
self.count = count | |
self.interval = 1 | |
def mac_to_linklocal(self, mac_address): | |
""" | |
Minor utility method which converts a MAC address to a link-local address. | |
""" | |
return 'FE80::{}'.format(in6_mactoifaceid(mac_address)) | |
def send_packets(self): | |
""" | |
Send Neighbor Advertisement packets. | |
A thread is created per announcement packet (announce IP and destination IP). This method | |
will block until all child threads have sent their packets. | |
""" | |
thread_list = [] | |
for announce_ip, destination_ip in product(self.announce_ips, self.destination_ips): | |
send_na_thread = threading.Thread(target=self.send_na, kwargs={ | |
'announce_ip': announce_ip, | |
'destination_ip': destination_ip, | |
}) | |
thread_list.append(send_na_thread) | |
send_na_thread.start() | |
for send_na_thread in thread_list: | |
send_na_thread.join() | |
self.logger.info('Finished announcements') | |
def send_na(self, announce_ip, destination_ip): | |
""" | |
Send a Neighbor Advertisement to destination_ip, announcing the availability of | |
announce_ip. | |
Destination IP can be either a multicast link-local address (ffx2::/16), or an address | |
generated with SLAAC. | |
""" | |
if in6_ismlladdr(destination_ip): | |
self.send_multicast_na(announce_ip, destination_ip) | |
else: | |
self.send_linklocal_na(announce_ip, destination_ip) | |
def send_multicast_na(self, announce_ip, destination_ip): | |
""" | |
Send a multicast Neighbor Advertisement to destination_ip, announcing the availability of | |
announce_ip. | |
Destination IP must be a multicast link-local address (ffx2::/16), scapy will generate the | |
destination MAC automatically. | |
""" | |
self.logger.info('Announcing %s to multicast %s', announce_ip, destination_ip) | |
ether_packet = Ether() # auto calculated for us | |
ipv6_packet = IPv6(src=self.local_ipv6, dst=destination_ip) | |
na_packet = ICMPv6ND_NA(R=0, tgt=announce_ip) | |
full_packet = ether_packet / ipv6_packet / na_packet / self.lla_packet | |
sendp( | |
full_packet, inter=self.interval, count=self.count, iface=self.interface, | |
verbose=False | |
) | |
def send_linklocal_na(self, announce_ip, destination_ip): | |
""" | |
Send a link-local Neighbor Advertisement to destination_ip, announcing the availability of | |
announce_ip. | |
Destination IP must be a SLAAC generated address (either global or link-local). The MAC | |
address is taken from the destination IP, any packets will be sent to the link-local | |
address. | |
""" | |
self.logger.info('Announcing %s to link-local %s', announce_ip, destination_ip) | |
destination_mac = in6_addrtomac(destination_ip) | |
remote_ipv6 = self.mac_to_linklocal(destination_mac) | |
ether_packet = Ether(dst=destination_mac) | |
ipv6_packet = IPv6(src=self.local_ipv6, dst=remote_ipv6) | |
na_packet = ICMPv6ND_NA(R=0, tgt=announce_ip) | |
full_packet = ether_packet / ipv6_packet / na_packet / self.lla_packet | |
sendp( | |
full_packet, inter=self.interval, count=self.count, iface=self.interface, | |
verbose=False | |
) | |
if __name__ == '__main__': | |
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) | |
parser = argparse.ArgumentParser() | |
parser.add_argument( | |
'--interface', default='eth0', help='Network interface to send packets from' | |
) | |
parser.add_argument( | |
'--count', default=5, type=int, help='Number of packets per announcement to send' | |
) | |
parser.add_argument( | |
'--destination', metavar='destination_ip', dest='destination_ips', nargs='*', | |
default=['ff02::2'], help='Destination IPv6 address to send NA packets' | |
) | |
parser.add_argument( | |
'announce_ips', metavar='announce_ip', nargs='+', | |
help='IPv6 address announced in Neighbor Advertisement packets' | |
) | |
args = parser.parse_args() | |
announcer = Pyv6NA(**vars(args)) | |
announcer.send_packets() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment