Skip to content

Instantly share code, notes, and snippets.

@oskar456
Last active July 15, 2026 12:22
Show Gist options
  • Select an option

  • Save oskar456/f5b6307d8ead46dac0aad6ab3558410d to your computer and use it in GitHub Desktop.

Select an option

Save oskar456/f5b6307d8ead46dac0aad6ab3558410d to your computer and use it in GitHub Desktop.
Gratuitous IPv6 Neighbor Advertisement of GUAs using Scapy
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.14"
# dependencies = [
# "scapy>=2.7.0",
# ]
# ///
import socket
import time
from scapy.all import *
def fix_lladdr(lladdr):
"""
Remove interface-id from the second quad of a link-local address.
"""
mask = inet_pton(socket.AF_INET6, "ffff::ffff:ffff:ffff:ffff")
addr = inet_pton(socket.AF_INET6, lladdr)
return inet_ntop(socket.AF_INET6, in6_and(addr, mask))
def resolve_ndp(dst, src, iface):
"""
Do an NDP MAC address resolution.
The implementation in Scapy is broken on macOS due to interface ids
in the second quad.
"""
res = neighsol(dst, src, iface)
if res is not None:
if ICMPv6NDOptDstLLAddr in res:
return res[ICMPv6NDOptDstLLAddr].lladdr
else:
return res.src
def do_ping(addrs, mac, iface, dst="2001:7fd::1", timeout=2):
"""
Send ICMP echo request from each of addrs using Ether header mac
to destination dst with timeout timeout.
Return tuple of lists of working and broken source IP addresses
"""
ping = ICMPv6EchoRequest()
pkts = (mac/IPv6(src=a, dst=dst)/ping for a in addrs)
ans, unans = srp(pkts, iface=iface, timeout=timeout)
working = [a.answer[IPv6].dst for a in ans]
broken = [a[IPv6].src for a in unans]
return working, broken
def gratuitous_na(addrs, src, hwaddr, iface):
"""
Send Neighbor advertisements for addresses in the list addrs
from a Link-local address src, pointing at hwaddr MAC address.
"""
dst = "ff02::2"
gwmac = getmacbyip6(dst)
mac = Ether(src=hwaddr, dst=gwmac)
ip = IPv6(src=src, dst=dst)
na = ICMPv6ND_NA(O=1, R=0, S=0)
opt = ICMPv6NDOptDstLLAddr(lladdr=hwaddr)
for a in addrs:
na.tgt = a
pkt = mac/ip/na/opt
print(a)
sendp(pkt, iface=iface)
def main():
"""
Enumerate all non-link-local IPv6 addresses on the default interface.
Send a gratious Neighbor advertisement with each address to the
default gateway.
"""
conf.verb = 0
iface, src, dst = conf.route6.route("::/0")
src, dst = (fix_lladdr(a) for a in [src, dst])
hwaddr = get_if_hwaddr(iface)
gwmac = resolve_ndp(dst, src, iface)
print(f"Iface {iface}")
print(f"Src {src}\t{hwaddr}")
print(f"Dst {dst}\t{gwmac}")
addrs = (x[0] for x in in6_getifaddr()
if x[1] == IPV6_ADDR_GLOBAL and x[2] == iface)
print("Priming router's neighbor cache by pinging k.root-servers.net")
mac = Ether(src=hwaddr, dst=gwmac)
working, broken = do_ping(addrs, mac, iface)
if len(working) > 0:
print("Following addresses seem to be working well:")
print("\n".join(working))
if len(broken) > 0:
print("Sending gratuitous NAs for broken addresses:")
gratuitous_na(broken, src, hwaddr, iface)
print("Checking ping again")
time.sleep(1)
working, broken = do_ping(broken, mac, iface)
if len(broken) > 0:
print("These addresses are still broken:")
print("\n".join(broken))
else:
print("All addresses are working now.")
else:
print("All addresses are working. Nothing to do.")
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment