Master Host Discovery & Port Scanning with Scapy: TCP SYN Ping to Traceroute
This tutorial explains how to perform host discovery and various port‑scanning techniques—including TCP SYN, ACK, UDP, ARP, and ICMP pings—as well as service discovery scans (SYN, FIN, NULL, Xmas, UDP) and traceroute methods using Scapy, complete with code examples and interpretation of results.
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.
<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 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 payload can improve scanning.
Select ports that are likely closed; open UDP ports may ignore empty packets.
An ICMP “port unreachable” reply 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 any layer‑2 communication.
In IPv6 ARP is replaced by NDP, which provides address resolution, duplicate‑address detection, 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) means 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 using tcpdump.
<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>In the capture, “.” denotes ACK, “S” denotes SYN, and “[S.]” denotes SYN‑ACK.
Crafting a three‑way handshake with Scapy
Step 1 – Send SYN from the client to the 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, the 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 – Capture the server’s SYN‑ACK response
Store the response.
Extract the server’s sequence number and add 1.
<code>synack_packet = sr1(ip/syn_packet)
my_ack = synack_packet.seq + 1</code>Step 3 – Send the ACK confirming the server’s response
Reuse the same IP header.
Build a TCP header with the ACK flag, incremented sequence number, and the calculated acknowledgment value.
<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 *
get='GET / HTTP/1.0\n\n'
ip=IP(src="192.168.2.53", dst="60.205.177.168")
port=RandNum(1024,65535)
SYN=ip/TCP(sport=port, dport=80, flags="S", seq=42)
SYNACK=sr1(SYN)
ACK=ip/TCP(sport=SYNACK.dport, dport=80, flags="A", seq=SYNACK.ack, ack=SYNACK.seq+1)/get
reply,error=sr(ACK)
print(reply.show())</code>SYN Scan
SYN scanning (half‑open scan) determines port state without completing a full TCP connection. An open port replies with SYN‑ACK, after which the client sends RST. A closed port replies with RST directly. Sending many SYN packets without completing the handshake can overload the target (SYN flood).
Example with Scapy:
Single host, single port
Use
sr1to send and receive a packet.
Use
sprintfto print fields; “SA” flag indicates an open port, “RA” indicates closed.
<code>syn_packet = IP(dst='60.205.177.168')/TCP(dport=22,flags='S')
rsp=sr1(syn_packet)
rsp.sprintf("%IP.src% %TCP.sport% %TCP.flags%") # '60.205.177.168 ssh SA'</code>Single host, multiple ports
<code>ans,unans=sr(IP(dst="60.205.177.168")/TCP(dport=(20,22),flags="S"))
ans.summary(lambda s:s[1].sprintf("%TCP.sport% %TCP.flags%"))
# ftp_data RA
# ftp RA
# ssh SA</code>Multiple hosts, multiple ports
Use
make_tableto build a matrix of hosts (x‑axis) and ports (y‑axis) with TCP flags as cell values.
<code>ans,unans = sr(IP(dst=["60.205.177.168-170"])/TCP(dport=[20,22,80],flags="S"))
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; an RST response indicates a closed port.
<code>fin_packet = IP(dst='60.205.177.168')/TCP(dport=4444,flags='F')
resp = sr1(fin_packet)</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 TCP packet with no flags set is sent. An RST response means the port is closed; no response means open. Certain ICMP type 3 codes 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
A TCP packet with FIN, URG, and PUSH flags set. No response means open; an RST means closed. Specific ICMP type 3 codes indicate filtering.
<code>xmas_scan_resp = sr1(IP(dst="60.205.177.168")/TCP(dport=4399,flags="FPU"), timeout=1)
xmas_scan_resp.sprintf('%TCP.flags%') # 'RA'</code>UDP Scan
UDP scanning is common for DNS, SNMP, and DHCP services. An open UDP port replies with a UDP packet; an ICMP “port unreachable” reply indicates the port is closed.
<code>udp_scan = sr1(IP(dst="60.205.177.168")/UDP(dport=53), timeout=1)</code>Traceroute
Traceroute exploits the TTL field in the IP header. When TTL reaches 0, the router returns a reply, revealing the hop.
Unix tools use UDP, Windows uses ICMP, Linux’s tcptraceroute uses TCP.
ICMP‑based traceroute
<code>ans,unans=sr(IP(dst="49.232.152.189", ttl=(1,10))/ICMP())
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>TCP‑based traceroute
<code>ans,unans=sr(IP(dst="baidu.com", ttl=(1,10))/TCP(dport=53,flags="S"))
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 performs the same operation.
<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>DNS‑based traceroute
By specifying a full DNS packet as the L4 layer, traceroute can be performed over UDP with DNS queries.
<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.