Scapy Host Discovery & Port Scanning: TCP SYN Ping, Traceroute & More
This guide explains how to use Scapy for host discovery (TCP SYN/ACK, UDP, ARP, ICMP pings), various port‑scanning techniques (SYN, FIN, NULL, Xmas, UDP) and traceroute methods (ICMP, TCP, DNS), providing complete code examples and practical insights.
Host Discovery
TCP SYN Ping
Send an empty TCP packet with only the SYN flag set.
A SYN/ACK or RST response indicates the host is up and running.
<code>>> ans,unans=sr(IP(dst="60.205.177.0/28")/TCP(dport=80,flags="S"))
Begin emission:
Finished sending 16 packets.
Received 92 packets, got 9 answers, remaining 7 packets
>>> ans.summary(lambda s:s[1].sprintf("%IP.src% is alive"))
60.205.177.1 is alive
60.205.177.2 is alive
60.205.177.4 is alive
60.205.177.6 is alive
60.205.177.7 is alive
60.205.177.8 is alive
60.205.177.11 is alive
60.205.177.12 is alive
60.205.177.14 is alive</code>TCP ACK Ping
Send an empty TCP packet with only the ACK flag set.
An unsolicited ACK should be answered with RST, indicating a live host.
Both SYN‑ping and ACK‑ping are useful because most stateless firewalls do not filter unsolicited ACK packets.
<code>>> ans, unans = sr(IP(dst='60.205.177.90-105')/TCP(dport=80, flags='A'))
Begin emission:
Finished sending 16 packets.
Received 173 packets, got 7 answers, remaining 9 packets
>>> ans.summary(lambda s:s[1].sprintf("{IP: %IP.src% is alive}"))
60.205.177.91 is alive
60.205.177.94 is alive
60.205.177.95 is alive
60.205.177.97 is alive
60.205.177.100 is alive
60.205.177.101 is alive
60.205.177.102 is alive</code>UDP Ping
Send a UDP packet to a chosen port (payload optional); protocol‑specific payloads improve effectiveness.
Select ports that are likely closed; open UDP ports may ignore empty packets.
An ICMP "port unreachable" response shows the host is up.
<code>>> ans, unans = sr(IP(dst='60.205.177.100-254')/UDP(dport=90),timeout=0.1)
Begin emission:
Finished sending 155 packets.
Received 18 packets, got 11 answers, remaining 144 packets
>>> ans.summary(lambda s:s[1].sprintf("%IP.src% is unreachable"))
60.205.177.106 is unreachable
60.205.177.108 is unreachable
60.205.177.107 is unreachable
60.205.177.111 is unreachable
60.205.177.125 is unreachable
60.205.177.172 is unreachable
60.205.177.191 is unreachable
60.205.177.203 is unreachable
60.205.177.224 is unreachable
60.205.177.242 is unreachable
60.205.177.244 is unreachable</code>ARP Ping
Use ARP Ping to discover live hosts on the same LAN.
Faster and more reliable because it operates at layer 2.
ARP is the backbone protocol for all layer‑2 communications.
In IPv6, ARP is replaced by NDP, which provides address resolution and neighbor discovery.
<code>>> ans,unans=srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst="172.17.51.0/24"),timeout=2)
Begin emission:
Finished sending 256 packets.
Received 190 packets, got 162 answers, remaining 94 packets
>>> ans.summary(lambda r: r[0].sprintf("%Ether.src% %ARP.pdst%"))
00:16:3e:0c:d1:ad 172.17.51.0
00:16:3e:0c:d1:ad 172.17.51.1
00:16:3e:0c:d1:ad 172.17.51.2
00:16:3e:0c:d1:ad 172.17.51.3
00:16:3e:0c:d1:ad 172.17.51.4
00:16:3e:0c:d1:ad 172.17.51.5
00:16:3e:0c:d1:ad 172.17.51.6
00:16:3e:0c:d1:ad 172.17.51.7</code>ICMP Ping
ICMP scanning uses the ubiquitous _ping_ program to send standard packets.
Send an ICMP type 8 (echo request); a type 0 (echo reply) indicates the host is alive.
Many hosts and firewalls block these packets, making basic ICMP scans unreliable.
ICMP also supports timestamp and address‑mask requests to reveal host availability.
<code>>> ans,unans=sr(IP(dst="60.205.177.168-180")/ICMP())
>>> ans.summary(lambda s:s[0].sprintf("{IP: %IP.dst% is alive}"))
60.205.177.168 is alive
60.205.177.169 is alive
60.205.177.171 is alive
60.205.177.172 is alive
60.205.177.175 is alive
60.205.177.174 is alive
60.205.177.176 is alive
60.205.177.179 is alive
60.205.177.178 is alive
60.205.177.180 is alive</code>Service Discovery (Port Scanning)
TCP Connect Scan
Example of a captured three‑way handshake:
<code>192.168.2.1.35555 > 192.168.2.12.4444: Flags [S] seq=12345
192.168.2.12.4444 > 192.168.2.1.35555: Flags [S.], seq=9998 ack=12346
192.168.2.1.35555 > 192.168.2.12.4444: Flags [.] seq=12346 ack=9999</code>IP and port numbers are separated by a dot; '.' denotes ACK, 'S' denotes SYN, and '[S.]' denotes SYN‑ACK.
Crafting a Three‑Way Handshake in Scapy
Step 1 – Send SYN from client to listening server
Create an IP header with source and destination addresses.
Create a TCP header with a random source port, the server’s listening port, SYN flag, and an initial sequence number.
<code>ip=IP(src="192.168.2.53", dst="60.205.177.168")
syn_packet = TCP(sport=1500, dport=80, flags="S", seq=100)</code>Step 2 – Listen for the server’s SYN‑ACK response
Store the server’s response.
Extract the server’s TCP sequence number and add 1.
<code>synack_packet = sr1(ip/syn_packet)
my_ack = synack_packet.seq + 1</code>Step 3 – Send ACK to confirm the connection
The IP header uses the same source and destination as the initial SYN.
The TCP header mirrors the SYN’s source and destination ports, sets the ACK flag, and uses the incremented sequence number.
<code>ack_packet = TCP(sport=1500, dport=80, flags="A", seq=101, ack=my_ack)
send(ip/ack_packet)</code>Full script:
<code>#!/usr/bin/python
from scapy.all import *
# Build payload
get='GET / HTTP/1.0\n\n'
# Set source and destination IPs
ip=IP(src="192.168.2.53", dst="60.205.177.168")
# Random source port
port=RandNum(1024,65535)
# Build SYN packet
SYN=ip/TCP(sport=port, dport=80, flags="S", seq=42)
# Send SYN and receive SYN‑ACK
SYNACK=sr1(SYN)
# Build ACK packet with payload
ACK=ip/TCP(sport=SYNACK.dport, dport=80, flags="A", seq=SYNACK.ack, ack=SYNACK.seq+1)/get
# Send ACK and print response
reply,error=sr(ACK)
print(reply.show())</code>SYN Scan
SYN scanning (half‑open scan) determines port state without completing the TCP handshake. An open port replies with SYN‑ACK, after which the scanner sends RST. A closed port replies with RST directly. Sending many SYNs without completing connections can cause a SYN flood attack.
Using Scapy to perform a SYN scan on a single host and port:
<code>>> syn_packet = IP(dst='60.205.177.168')/TCP(dport=22,flags='S')
>>> rsp=sr1(syn_packet)
Begin emission:
Finished sending 1 packets.
Received 3 packets, got 1 answers, remaining 0 packets
>>> rsp.sprintf("%IP.src% %TCP.sport% %TCP.flags%")
'60.205.177.168 ssh SA'</code>SYN Scan on Multiple Ports
<code>>> ans,unans=sr(IP(dst="60.205.177.168")/TCP(dport=(20,22),flags="S"))
Begin emission:
Finished sending 3 packets.
Received 7 packets, got 3 answers, remaining 0 packets
>>> ans.summary(lambda s:s[1].sprintf("%TCP.sport% %TCP.flags%" ))
ftp_data RA
ftp RA
ssh SA</code>SYN Scan on Multiple Hosts and Ports
Use
make_tableto build a matrix where the X‑axis is target IP, Y‑axis is target port, and the cell contains the TCP flag.
<code>>> ans,unans = sr(IP(dst=["60.205.177.168-170"])/TCP(dport=[20,22,80],flags="S"))
Begin emission:
Finished sending 9 packets.
Received 251 packets, got 4 answers, remaining 5 packets
>>> ans.make_table(lambda s: (s[0].dst, s[0].dport, s[1].sprintf("%TCP.flags%")))
60.205.177.168 60.205.177.169
20 RA -
22 SA -
80 SA SA</code>FIN Scan
The client sends a TCP packet with the FIN flag. No response indicates an open port; a RST response indicates a closed port.
<code>>> fin_packet = IP(dst='60.205.177.168')/TCP(dport=4444,flags='F')
>>> resp = sr1(fin_packet)
Begin emission:
Finished to send 1 packets.
Received 0 packets, got 0 answers, remaining 1 packets</code>Closed port example:
<code>>> fin_packet = IP(dst='60.205.177.168')/TCP(dport=4399,flags='F')
>>> resp = sr1(fin_packet)
>>> resp.sprintf('%TCP.flags%')
'RA'</code>NULL Scan
A NULL scan sends a TCP packet with no flags set. An RST response means the port is closed; no response means the port is open. ICMP type 3 codes 1,2,3,9,10,13 indicate the port is filtered.
<code>>> null_scan_resp = sr1(IP(dst="60.205.177.168")/TCP(dport=4399,flags=""),timeout=1)
>>> null_scan_resp.sprintf('%TCP.flags%')
'RA'</code>Xmas Scan
An Xmas scan sends a TCP packet with FIN, URG, and PUSH flags. No response indicates an open port; an RST indicates a closed port. ICMP type 3 codes 1,2,3,9,10,13 indicate filtering.
<code>>> xmas_scan_resp=sr1(IP(dst="60.205.177.168")/TCP(dport=4399,flags="FPU"),timeout=1)
Begin emission:
Finished sending 1 packets.
Received 2 packets, got 1 answers, remaining 0 packets
>>> xmas_scan_resp.sprintf('%TCP.flags%')
'RA'</code>UDP Scan
UDP scanning is common for detecting DNS, SNMP, and DHCP services. An open port replies with a UDP packet; an ICMP "port unreachable" (type 3, code 3) indicates a closed port.
<code>>> udp_scan=sr1(IP(dst="60.205.177.168")/UDP(dport=53),timeout=1)
</code>Traceroute
Traceroute relies on the IP TTL field. Each router decrements TTL; when TTL reaches zero, the router returns an ICMP "time exceeded" message.
Unix tools use UDP, Windows
tracertuses ICMP, and Linux
tcptracerouteuses TCP.
Traceroute with ICMP
<code>>> ans,unans=sr(IP(dst="49.232.152.189",ttl=(1,10))/ICMP())
Begin emission:
Finished sending 10 packets.
Received 112 packets, got 7 answers, remaining 3 packets
>>> ans.summary(lambda s:s[1].sprintf("%IP.src%"))
10.36.76.142
10.54.138.21
10.36.76.13
45.112.216.134
103.216.40.18
9.102.250.221
10.102.251.214</code>Traceroute with TCP
<code>>> ans,unans=sr(IP(dst="baidu.com",ttl=(1,10))/TCP(dport=53,flags="S"))
Begin emission:
Finished sending 10 packets.
Received 31 packets, got 9 answers, remaining 1 packets
>>> ans.summary(lambda s:s[1].sprintf("%IP.src% {ICMP:%ICMP.type%}"))
10.36.76.142 time-exceeded
10.36.76.13 time-exceeded
10.102.252.130 time-exceeded
117.49.35.150 time-exceeded
10.102.34.237 time-exceeded
111.13.123.150 time-exceeded
218.206.88.22 time-exceeded
39.156.67.73 time-exceeded
39.156.27.1 time-exceeded</code>Scapy also provides a built‑in
traceroute()function that combines these techniques.
<code>>> traceroute("baidu.com")
Begin emission:
Finished sending 30 packets.
Received 24 packets, got 24 answers, remaining 6 packets
220.181.38.148:tcp80
2 10.36.76.13 11
3 10.102.252.34 11
4 117.49.35.138 11
5 116.251.112.185 11
6 36.110.217.9 11
7 36.110.246.201 11
8 220.181.17.150 11
14 220.181.38.148 SA
15 220.181.38.148 SA
... (continues)</code>Traceroute with DNS
Specify a full packet in the
l4argument to perform DNS‑based traceroute.
<code>>> ans,unans=traceroute("60.205.177.168",l4=UDP(sport=RandShort())/DNS(qd=DNSQR(qname="thesprawl.org")))
Begin emission:
Finished sending 30 packets.
Received 21 packets, got 4 answers, remaining 26 packets
60.205.177.168:udp53
1 10.2.0.1 11
2 114.242.29.1 11
4 125.33.185.114 11
5 61.49.143.2 11</code>Ops Development Stories
Maintained by a like‑minded team, covering both operations and development. Topics span Linux ops, DevOps toolchain, Kubernetes containerization, monitoring, log collection, network security, and Python or Go development. Team members: Qiao Ke, wanger, Dong Ge, Su Xin, Hua Zai, Zheng Ge, Teacher Xia.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.