Information Security 22 min read

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.

Ops Development Stories
Ops Development Stories
Ops Development Stories
Master Host Discovery & Port Scanning with Scapy: TCP SYN Ping to Traceroute

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

sr1

to send and receive a packet.

Use

sprintf

to 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_table

to 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>
traceroutenetwork securityport scanningScapyhost discovery
Ops Development Stories
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.