diff --git a/CHANGELOG b/CHANGELOG
index 4cda2ffd2..036b81613 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,8 @@
#Nmap Changelog ($Id$); -*-text-*-
+o [NSE] New script targets-ipv6-eui64 generates target IPv6 addresses from a
+ user-provided file of MAC addresses, using the EUI-64 method. [Daniel Miller]
+
o [NSE][GH#2973] New service probes and scripts for MikroTik's WinBox router
admin service. mikrotik-routeros-version queries the 'info' and 'list' files
to get the RouterOS version. mikrotik-routeros-username-brute brute-forces
diff --git a/scripts/script.db b/scripts/script.db
index da77f4416..4d750cfbc 100644
--- a/scripts/script.db
+++ b/scripts/script.db
@@ -559,6 +559,7 @@ Entry { filename = "stuxnet-detect.nse", categories = { "discovery", "intrusive"
Entry { filename = "supermicro-ipmi-conf.nse", categories = { "exploit", "vuln", } }
Entry { filename = "svn-brute.nse", categories = { "brute", "intrusive", } }
Entry { filename = "targets-asn.nse", categories = { "discovery", "external", "safe", } }
+Entry { filename = "targets-ipv6-eui64.nse", categories = { "discovery", } }
Entry { filename = "targets-ipv6-map4to6.nse", categories = { "discovery", } }
Entry { filename = "targets-ipv6-multicast-echo.nse", categories = { "broadcast", "discovery", "safe", } }
Entry { filename = "targets-ipv6-multicast-invalid-dst.nse", categories = { "broadcast", "discovery", "safe", } }
diff --git a/scripts/targets-ipv6-eui64.nse b/scripts/targets-ipv6-eui64.nse
new file mode 100644
index 000000000..a72fb0e1b
--- /dev/null
+++ b/scripts/targets-ipv6-eui64.nse
@@ -0,0 +1,121 @@
+local ipOps = require "ipOps"
+local io = require "io"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local target = require "target"
+
+description = [[
+This script runs in the pre-scanning phase to convert 48-bit MAC addresses to
+EUI-64 IPv6 addresses, which are often used for auto-configuration. Generated
+addresses may be added to the scan queue.
+
+The MAC addresses used as input are read from the file named by the
+targets-ipv6-eui64.input script-arg. A good source of these
+addresses would be an IPv4 host discovery Nmap scan.
+]]
+
+---
+-- @usage
+-- nmap -6 --script targets-ipv6-eui64 --script-args newtargets,targets-ipv6-eui64.input=macs.txt,targets-ipv6-subnet={2001:db8:c0ca::/64}
+--
+-- @output
+-- Pre-scan script results:
+-- | targets-ipv6-eui64:
+-- |_ 2001:db8:c0ca:0:1322:33ff:fe44:5566
+--
+-- @args targets-ipv6-eui64.input The input file containing 1 MAC address per line
+--
+-- @args targets-ipv6-subnet Table/single IPv6 address with prefix
+-- (Ex. 2001:db8:c0ca::/48 or
+-- { 2001:db8:c0ca::/48, 2001:db8:FEA::/48 })
+-- Default: fe80::/64
+--
+-- @xmloutput
+-- 2001:db8:c0ca:0:1322:33ff:fe44:5566
+
+
+author = "Daniel Miller"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {
+ "discovery",
+}
+
+local infile = stdnse.get_script_args(SCRIPT_NAME .. ".input")
+local subnets = stdnse.get_script_args("targets-ipv6-subnet") or "fe80::/64"
+
+prerule = function ()
+
+ if nmap.address_family() ~= "inet6" then
+ stdnse.verbose1("This script is IPv6 only.")
+ return false
+ end
+
+ if infile == nil then
+ stdnse.verbose1( "Missing script-arg %s.input", SCRIPT_NAME)
+ return false
+ end
+
+ return true
+end
+
+action = function ()
+
+ local file, err = io.open(infile, "r")
+ if not file then
+ stdnse.verbose1("Unable to open %s for reading: %s", infile, err)
+ return nil
+ end
+
+ local eui64 = {}
+ for mac in file:lines() do
+ local raw, err = stdnse.fromhex(mac:gsub("[:-]", ""))
+ if not raw or #raw ~= 6 then
+ stdnse.debug1("Invalid MAC: %s", mac)
+ else
+ local bytes = {raw:byte(1,-1)}
+ bytes[1] = bytes[1] ~ 0x2
+ local eui = string.pack("BBBBBBBB",
+ bytes[1], bytes[2], bytes[3],
+ 0xff, 0xfe,
+ bytes[4], bytes[5], bytes[6]
+ )
+ eui64[#eui64+1] = eui
+ end
+ end
+
+ if type(subnets) == "string" then
+ subnets = { subnets }
+ end
+
+ local results = {}
+ for _, subnet in ipairs(subnets) do
+ local addr, maskbits = subnet:match("^%s*([:%x]+)/(%d+)%s*$")
+ if not addr then
+ stdnse.verbose1("Invalid IPv6 subnet: %s", subnet)
+ else
+ if tonumber(maskbits) > 64 then
+ stdnse.verbose1("Subnet too small for EUI-64 addresses.")
+ else
+ local v6bin, err = ipOps.ip_to_str(addr, "inet6")
+ if not v6bin then
+ stdnse.verbose1("Error parsing %s as IPv6 address: %s", addr, err)
+ else
+ v6bin = v6bin:sub(1, 8)
+ for _, eui in ipairs(eui64) do
+ local ip6addr, err = ipOps.str_to_ip(v6bin .. eui, "inet6")
+ if not ip6addr then
+ stdnse.debug1("Failed to convert addr to IPv6")
+ else
+ results[#results+1] = ip6addr
+ target.add(ip6addr)
+ end
+ end
+ end
+ end
+ end
+ end
+ if next(results) then
+ return results
+ end
+end