Describe the bug
With Homebrew Nmap 7.991 on macOS, an IPv6 SYN scan over a WireGuard utun interface fails unless I also specify its assigned IPv6 address using -S.
Nmap's debug output shows that it selects the interface's link-local IPv6 address internally and constructs its receive filter using that address. However, a simultaneous tcpdump capture on the same interface shows the matching probes transmitted with the interface's assigned ULA source address.
The captured TCP checksums are incorrect for the transmitted ULA source. Adjusting the checksum for the change from Nmap's selected link-local source to the captured ULA source accounts for the checksum difference exactly.
The scan reports the open port as filtered, with reason no-response. Explicitly supplying the assigned ULA with -S resolves the observed failure.
Environment
- macOS 27.0.1, build 26A434; arm64
- Runtime kernel: Darwin 27.0.0
- Nmap installed using Homebrew
- WireGuard interface: utun4
- Target: an IPv6 host reached through the VPN and a router
- Target service: TCP port 8920
- The target is reachable using normal TCP connections and a TCP connect scan (-sT)
Current version output collected during the investigation:
Nmap version 7.991 ( https://nmap.org )
Platform: arm-apple-darwin25.6.0
Compiled with: nmap-liblua-5.4.8 openssl-3.6.5 libssh2-1.11.1 libz-1.2.12 libpcre2-10.49 nmap-libpcap-1.10.6 nmap-libdnet-1.18.0 ipv6
Compiled without:
Available nsock engines: kqueue poll select
Address notation
The following labels replace actual addresses throughout the commands and trace excerpts:
- CLIENT_LINK_LOCAL: the fe80::… address assigned to utun4
- CLIENT_ULA: the fd… VPN address assigned to utun4
- SERVER_ULA: the target server's fd… address on another subnet
The relevant nmap --iflist entries list the link-local address before the ULA:
DEV IP/MASK TYPE UP MTU
utun4 CLIENT_LINK_LOCAL/64 point2point up 1420
utun4 CLIENT_ULA/120 point2point up 1420
Addresses have been replaced with labels for privacy. Checksum values below are quoted from the original capture and apply to the original addresses.
Steps to reproduce
- Use a macOS WireGuard interface with both a link-local IPv6 address and a routable assigned IPv6 address.
- Select a reachable IPv6 target beyond the VPN peer/router, with a known open TCP port.
- Start a capture on the VPN interface:
sudo tcpdump -i utun4 -nn -tttt -s 0 -vv \
'ip6 and host <SERVER_ULA> and tcp port 8920'
- Run an IPv6 SYN scan using an IPv6 literal, with DNS disabled:
sudo nmap -6 -sS -Pn -n -e utun4 \
-v --reason --packet-trace -d2 \
-p 8920 <SERVER_ULA>
- Repeat the same scan with the assigned VPN source address explicitly specified:
sudo nmap -6 -sS -Pn -n -e utun4 \
-S <CLIENT_ULA> \
-v --reason --packet-trace -d2 \
-p 8920 <SERVER_ULA>
The first scan fails. Adding -S <CLIENT_ULA> makes it work.
Observed Nmap output without -S
Nmap installs this capture filter:
Packet capture filter (device utun4): dst host CLIENT_LINK_LOCAL and (icmp or icmp6 or ((tcp or udp or sctp) and (src host SERVER_ULA)))
Its packet trace reports the link-local source:
SENT (0.0220s) TCP [[CLIENT_LINK_LOCAL]:55804 > [SERVER_ULA]:8920 S seq=124925575 win=0 <mss 1460,wscale 5>] IP [hopl=255 tclass=0 flow=0 payloadlen=24]
SENT (1.0240s) TCP [[CLIENT_LINK_LOCAL]:55806 > [SERVER_ULA]:8920 S seq=124794501 win=0 <mss 1460,wscale 5>] IP [hopl=255 tclass=0 flow=0 payloadlen=24]
The result is:
PORT STATE SERVICE REASON
8920/tcp filtered unknown no-response
Raw packets sent: 2 (128B) | Rcvd: 0 (0B)
Simultaneous tcpdump output
The matching source ports and TCP sequence numbers identify these as the same two probes. However, their captured IPv6 source is CLIENT_ULA:
2026-10-04 23:00:28.284355 IP6 (flowlabel 0x50600, hlim 255, next-header TCP (6) payload length: 24) CLIENT_ULA.55804 > SERVER_ULA.8920: Flags [S], cksum 0xad54 (incorrect -> 0xe041), seq 124925575, win 1024, options [mss 1460], length 0
2026-10-04 23:00:29.286331 IP6 (flowlabel 0xe0c00, hlim 255, next-header TCP (6) payload length: 24) CLIENT_ULA.55806 > SERVER_ULA.8920: Flags [S], cksum 0xad56 (incorrect -> 0xe043), seq 124794501, win 1024, options [mss 1460], length 0
No replies appeared in this capture.
Using the original addresses, an Internet-checksum adjustment replacing only the IPv6 pseudoheader source address gives:
0xad54 -> 0xe041
0xad56 -> 0xe043
Both results exactly match tcpdump's expected checksums.
This establishes a source-address/checksum discrepancy in the utun capture. I have not included a remote-server capture of this exact failing run.
Expected behaviour
When an interface has multiple IPv6 addresses, Nmap should choose a source appropriate for the destination and ensure that its TCP checksum and receive filter agree with the source address actually transmitted.
For this routed ULA destination, selecting the interface's link-local source internally is inappropriate.
The scan should identify the reachable open port without requiring an explicit -S workaround.
Possible implementation cause
A review of current upstream source appears consistent with the observed behaviour:
-
When -e is supplied without an explicit source address, Nmap calls devname2ipaddr():
https://github.com/nmap/nmap/blob/master/nmap.cc#L1704-L1710
-
devname2ipaddr() returns the first address matching the interface and address family. It does not consider the destination or IPv6 address scope:
https://github.com/nmap/nmap/blob/master/libnetutil/netutil.cc#L1656-L1671
-
The raw IPv6 socket send path used on platforms such as macOS skips the constructed IPv6 header and passes the transport payload to sendmsg(). In the inspected function, it does not bind the selected source or supply it through IPV6_PKTINFO:
https://github.com/nmap/nmap/blob/master/libnetutil/netutil.cc#L2718-L2808
-
The receive filter uses Nmap's internally selected source:
https://github.com/nmap/nmap/blob/master/scan_engine_raw.cc#L854-L874
My interpretation is that macOS selects the appropriate ULA source for the emitted packet, while Nmap retains its link-local source assumption for the checksum and receive filter.
The source review is of current upstream code; I have not audited the installed binary against its exact build source.
Related reports
This reproduction uses -n, an IPv6 literal, and a raw SYN scan. I have not found an existing report documenting this exact link-local/ULA mismatch.
Describe the bug
With Homebrew Nmap 7.991 on macOS, an IPv6 SYN scan over a WireGuard utun interface fails unless I also specify its assigned IPv6 address using -S.
Nmap's debug output shows that it selects the interface's link-local IPv6 address internally and constructs its receive filter using that address. However, a simultaneous tcpdump capture on the same interface shows the matching probes transmitted with the interface's assigned ULA source address.
The captured TCP checksums are incorrect for the transmitted ULA source. Adjusting the checksum for the change from Nmap's selected link-local source to the captured ULA source accounts for the checksum difference exactly.
The scan reports the open port as filtered, with reason no-response. Explicitly supplying the assigned ULA with -S resolves the observed failure.
Environment
Current version output collected during the investigation:
Address notation
The following labels replace actual addresses throughout the commands and trace excerpts:
The relevant nmap --iflist entries list the link-local address before the ULA:
Addresses have been replaced with labels for privacy. Checksum values below are quoted from the original capture and apply to the original addresses.
Steps to reproduce
sudo tcpdump -i utun4 -nn -tttt -s 0 -vv \ 'ip6 and host <SERVER_ULA> and tcp port 8920'The first scan fails. Adding -S <CLIENT_ULA> makes it work.
Observed Nmap output without -S
Nmap installs this capture filter:
Its packet trace reports the link-local source:
The result is:
Simultaneous tcpdump output
The matching source ports and TCP sequence numbers identify these as the same two probes. However, their captured IPv6 source is CLIENT_ULA:
No replies appeared in this capture.
Using the original addresses, an Internet-checksum adjustment replacing only the IPv6 pseudoheader source address gives:
Both results exactly match tcpdump's expected checksums.
This establishes a source-address/checksum discrepancy in the utun capture. I have not included a remote-server capture of this exact failing run.
Expected behaviour
When an interface has multiple IPv6 addresses, Nmap should choose a source appropriate for the destination and ensure that its TCP checksum and receive filter agree with the source address actually transmitted.
For this routed ULA destination, selecting the interface's link-local source internally is inappropriate.
The scan should identify the reachable open port without requiring an explicit -S workaround.
Possible implementation cause
A review of current upstream source appears consistent with the observed behaviour:
When -e is supplied without an explicit source address, Nmap calls devname2ipaddr():
https://github.com/nmap/nmap/blob/master/nmap.cc#L1704-L1710
devname2ipaddr() returns the first address matching the interface and address family. It does not consider the destination or IPv6 address scope:
https://github.com/nmap/nmap/blob/master/libnetutil/netutil.cc#L1656-L1671
The raw IPv6 socket send path used on platforms such as macOS skips the constructed IPv6 header and passes the transport payload to sendmsg(). In the inspected function, it does not bind the selected source or supply it through IPV6_PKTINFO:
https://github.com/nmap/nmap/blob/master/libnetutil/netutil.cc#L2718-L2808
The receive filter uses Nmap's internally selected source:
https://github.com/nmap/nmap/blob/master/scan_engine_raw.cc#L854-L874
My interpretation is that macOS selects the appropriate ULA source for the emitted packet, while Nmap retains its link-local source assumption for the checksum and receive filter.
The source review is of current upstream code; I have not audited the installed binary against its exact build source.
Related reports
Nping issue nping --ipv6 (ipv6 mode) don't echo reply packet, alway echo packets Lost(100%). #2032 / PR Github issue #2032 fixed. Source determination added for ipv6. #2033 describe an IPv6 source-determination problem leading to an incorrect BPF filter, but concern a different utility and reproduction:
nping --ipv6 (ipv6 mode) don't echo reply packet, alway echo packets Lost(100%). #2032
Github issue #2032 fixed. Source determination added for ipv6. #2033
Issue Nmap crashes with -e on macOS when DNS resolution is enabled #3441 concerns Homebrew Nmap 7.991 on macOS with -e, but its failure is in DNS/socket binding:
Nmap crashes with -e on macOS when DNS resolution is enabled #3441
This reproduction uses -n, an IPv6 literal, and a raw SYN scan. I have not found an existing report documenting this exact link-local/ULA mismatch.