Last active
July 15, 2026 12:22
-
-
Save oskar456/f5b6307d8ead46dac0aad6ab3558410d to your computer and use it in GitHub Desktop.
Gratuitous IPv6 Neighbor Advertisement of GUAs using Scapy
This file contains hidden or 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 -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