Skip to content

Instantly share code, notes, and snippets.

@tomkins
Created June 30, 2019 22:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save tomkins/c1fec82499fa273c6e1712147867bfa5 to your computer and use it in GitHub Desktop.
Save tomkins/c1fec82499fa273c6e1712147867bfa5 to your computer and use it in GitHub Desktop.
Python IPv6 Neighbor Advertisements
#!/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
#!/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