Sitemap

Python Network/Port Scanner

25 min readOct 2, 2026

--

Press enter or click to view image in full size

So, I woke up and had a thought about using Python to build a network scanner, but I haven’t used Python for a project in a while, and this would be two birds with one arrow: I get to revise Python and also learn more about actually building a scanner. So, deciding to build a scanner, I wondered what the things and questions I would have to solve would be before I could actually build the scanner itself. I went into Claude, gave it my whole detailed plan for the project, and asked it the same question, and it gave me questions that would teach me the whole step-by-step process.

My first question was what module would be used to build this. I have had some experience with ‘scapy’ for school projects, so that was my first thought, but as per the questions that Claude gave me, it seemed like checking out a built-in Python module, “Socket,” was the correct option.

I used Python’s official documentation and the following links to learn more about the Socket module:

https://docs.python.org/3/library/socket.html

https://www.pythonsheets.com/notes/network/python-socket.html#get-network-interfaces

https://www.w3schools.com/python/ref_module_socket.asp

After going through these documents, I learned a bit of the background about the module. Now let’s check the questions given by Claude and get to solving them one by one.

Level 1 — Socket basics (just connecting)

  1. What’s my IP? — Use the socket module to get your own machine’s hostname and local IP address and print them.

So, checking what my hostname is was very straightforward; as in the document I referenced above, it gave me the socket.gethostname() method.

import socket

name = socket.gethostname()
print (name)

This was plenty to get the hostname of my device; getting the IP address was a little tricky. I tried to get it by looking up my hostname, basically using socket.gethostbyname(socket.gethostname()). Here, we know that socket.gethostname() gets you the hostname for my device, as we already know from above, and socket.gethostbyname() gets me the IP address for that host by reverse DNS lookup. Now, the issue was that when I tried this, I got the loopback IP address, and while that wasn’t a failure, it definitely wasn’t the answer we were looking for. So, I started researching again and found this Stack Overflow thread: https://stackoverflow.com/questions/166506/finding-local-ip-addresses-using-pythons-stdlib

Here in the comments, I found that my previous attempt, where I passed my hostname into the reverse DNS lookup method, likely failed because I was running Linux on Windows Subsystem for Linux (WSL), and it couldn’t find my local IP address. In the same Stack Overflow thread, I also found a possible solution for my problem. It gave me an idea to open a socket connection to a random IP or domain, and from there we could check which of my sockets (interfaces) was used for that connection and what the IP address for that socket was.

import socket

name = socket.gethostname()
print (name)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ipadd = s.getsockname()[0]
print (ipadd)

Here, socket.AF_INET defines that I am going to use an IPv4 address, and socket.DGRAM defines that I am going to make a connection to a UDP port. Now, as we know, UDP doesn’t have a handshake process; it will just send data to the defined IP address on the mentioned port and be done with it.

For the next line in the code, s.connect((“8.8.8.8”, 80)), I defined that I am connecting to Google’s DNS IP on port 80. I had to use two brackets here, as s.connect only takes one argument, but I have to pass the IP address and also the port number, which required me to use a tuple instead. So, the first bracket is for the method itself, and the second bracket is for the tuple.

Press enter or click to view image in full size

Similarly, I used s.getsockname() to get the IP address of the interface that was used to make this connection, but realized that this also results in printing the port number my device used for this connection. So, just like when we only print one particular item from the list, I chose to only print the first item, which is the IP address; hence, I used [0]. It’s like print (list[0]), where only the first item from the list would be printed.

2. Single connection test — Ask the user for a hostname (like “google.com”) and a port number (like 80). Try to open a socket connection to it and print whether it succeeded or failed.

I felt like this was simple since we had already made a connection in the previous question, so I just copied the old code and went on adding a few lines to it.

import socket

hostname = input("Enter a hostname: ")
port = int(input ("Enter a port: "))
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(3.0)
s.connect((hostname, port))
print ("success")

Here, all that is added from the previous question was the timeout part: s.settimeout(3.0) would wait for three seconds to make a connection and then disconnect. After actually running this, I realized all the connections were positive, and maybe there was an error in my code. I also realized that I am using UDP, which is connectionless, meaning it sends the data and then doesn’t really care if it reached the destination or not. So, I switched to using TCP instead.

import socket

hostname = input("Enter a hostname: ")
port = int(input ("Enter a port: "))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3.0)
s.connect((hostname, port))
print ("success")

This code legitimately checks whether the connection was made or not. If the connection was successful, it would print success, but if it failed, it would give me an error and stop the code. An example is below.

Press enter or click to view image in full size

Here, you can see that when I set a port too high, it gave me an error saying that the port has to be between 0–65535. Now I didn’t want these errors and wanted them handled properly, so I checked the possible errors in Python’s official documentation and updated the code for error handling accordingly

import socket

hostname = input("Enter a hostname: ")
port = int(input ("Enter a port: "))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3.0)

try:
s.connect((hostname, port))
print ("The connection was successful")
except OverflowError:
print("Port must be between 0 and 65535")
except socket.gaierror:
print("Could not resolve hostname")
except ConnectionRefusedError:
print("Connection actively refused (port closed)")
except socket.timeout:
print("No response — connection timed out (port filtered or host unreachable)")
s.close()

Now this shows when the connection was successful, and if it fails, it prints why it failed instead of giving us an error.

Press enter or click to view image in full size

I tested it again, and while testing, I saw that there were some errors that might still go through that I didn’t anticipate, so I added another line. The screenshot above shows the error we got before and after the fix.

import socket

hostname = input("Enter a hostname: ")
port = int(input ("Enter a port: "))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3.0)

try:
s.connect((hostname, port))
print ("The connection was sucessful")
except OverflowError:
print("Port must be between 0 and 65535")
except socket.gaierror:
print("Could not resolve hostname")
except ConnectionRefusedError:
print("Connection actively refused (port closed)")
except socket.timeout:
print("No response — connection timed out (port filtered or host unreachable)")
except socket.error as e:
print ("The error was ", e )
finally:
s.close()

3. Resolve a domain to an IP — Ask the user for a domain name (like “example.com”) and use socket to resolve and print its IP address.

This question turned out to be straightforward because of the mistake I had already made in the first question; I just needed to use socket.gethostbyname() and pass the name of the host there, with error handling.

Here I realized that this might actually be teaching me, as I could use what I learnt previously here, and it was helping me grow lol.

import socket
hostname = input("Enter a hostname: ")

try:
ipadd = socket.gethostbyname(hostname)
print (ipadd)
except (socket.gaierror, OverflowError, ConnectionRefusedError) as e:
print ("The lookup failed because ", e)

All I did was what we had already done in the first and second question: I let the user input the hostname and did a reverse DNS lookup to get the IP address for that hostname, and also used try/except for error handling in case users passed something random in the hostname section.

Press enter or click to view image in full size

4. Grab a banner — Connect to a known open port (try port 80 on a website) and print whatever data the server sends back immediately after connecting (this is called a “banner” — many services announce themselves).

For this question, I had to research again, and I went through these three links

https://stackoverflow.com/questions/17024212/grab-banner-of-different-ports-with-python

https://medium.com/@rajesh.p3807/building-a-simple-python-banner-grabbing-script-a-step-by-step-guide-6538f2d26804

https://ballingt.com/banner-grabbing-with-python/

I only opened another method that would check anything that is received from the connection and print it, and it also has error handling, as shown below. I did limit the receiving bytes to 1024, as I believed that for a test run this would be enough.

import socket

hostname = input("Enter a hostname: ")
port = int(input ("Enter a port: "))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3.0)
try:
s.connect((hostname, port))
banner = s.recv(1024)
print (banner)
except socket.error as e:
print("The error is ", e)
s.close()

Since I was still testing and wanted relevant answers so I could update my code accordingly, I ran a Metasploitable server on my device using VirtualBox and ran the program to banner grab against my Metasploitable. During the process I checked different ports such as 21,22,23,25 and 80. It worked fine for most of these, but while trying to connect to port 80, it showed a timeout error, and when connecting to telnet, it showed me raw bytes, as evident below.

Press enter or click to view image in full size

So, I researched a bit more, and apparently, unlike most ports, telnet doesn’t respond in plain text, so when banner-grabbing telnet, such a response is normal. Similarly, for HTTP, we need to send/request data to be able to receive anything, so let’s make some changes in the code to ensure that this works better now.

import socket

hostname = input("Enter a hostname: ")
port = int(input ("Enter a port: "))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3.0)
try:
s.connect((hostname, port))
s.send(b'GET /\n\n')
banner = s.recv(1024)
print (banner)
except socket.error as e:
print("The error is ", e)
s.close()

I added sending a GET request in the code and tried banner grabbing on port 80 again, and it worked as intended for the most part.

Press enter or click to view image in full size

Since we were still getting issues when connecting to HTTP, I changed the script a bit more and realized that there are some important things to add when requesting a banner through port 80: one is the method, which we are using GET method; next is the version of HTTP we want to use. So, I changed the script to add an HTTP version. We also needed the hostname of the website, so I added the

import socket

hostname = input("Enter a hostname: ")
port = int(input ("Enter a port: "))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3.0)
try:
s.connect((hostname, port))
request = f"GET / HTTP/1.1 \r\n Host: {hostname}\r\n\r\n"
s.send(request.encode())
banner = s.recv(1024)
print (banner)
except socket.error as e:
print("The error is ", e)
s.close()
Press enter or click to view image in full size
Press enter or click to view image in full size

Here you can see what it looks like and that I am still getting an error. So, to figure out what mistake I am making, I checked by sending an actual request to an HTTP website.

Press enter or click to view image in full size

Here you can see that there is more information being sent by a browser when accessing a website, so let’s copy something similar to that in ours as well. Before doing so, I checked with Claude, and it basically told me that extra spaces were where the problem was and to remove extra spaces, as Python reads the extra spaces also as actual characters, which can cause errors.

import socket

hostname = input("Enter a hostname: ")
port = int(input ("Enter a port: "))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3.0)
try:
s.connect((hostname, port))
request = f"GET / HTTP/1.1\r\nHost: {hostname}\r\n\r\n"
s.send(request.encode())
banner = s.recv(1024)
print (repr(banner))
except socket.error as e:
print("The error is ", e)
s.close()

Now let’s compare the result before and after below

Press enter or click to view image in full size

There you can see my request, and inside the red box in the black highlight, the response we saw in the reply within python, we restricted the response to 1024 bits, so we only saw a limited part of the reply below. In Wireshark, you can see the whole thing.

Press enter or click to view image in full size

FYI, I forgot to mention this above, but in the response, I added print(repr(banner)). The repr here stands for “representation”, so that the answer is printed in its raw form. As in, \r\n would also be seen.

Level 2 — Checking multiple things (loops + sockets together)

5. Port range checker — Ask the user for a hostname and a range of ports (e.g., 1–100). Loop through each port and print whether it’s open or closed.

6. import socket
7.
8. hostname = input("Enter a hostname: ")
9. start = int(input ("Enter the starting port: "))
10. end = int(input ("Enter the ending port: "))
11.
12. for x in range(start, end):
13. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
14. s.settimeout(1.0)
15. result = s.connect_ex((hostname, x))
16. if result == 0:
17. print ("The port",x, "is open")
18. s.close()
19. else:
20. print ("The port", x, "is closed")
21. s.close()

This took me a while, but in the end, it was pretty straightforward. Above, you can see the code that works; it will print whether the port is opened or closed properly.

Initially, the mistake I made was that I put the s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) part and the s.settimeout(1.0) part outside the loop. Basically, what that did was that only the first port was checked, and all the others errored out because apparently a socket created can only be used once and then it closes, so for each new connection we need a new socket to open; hence why I had to put the socket declaration and timeout inside the loop.

Here are the results

Press enter or click to view image in full size

6. Timeout handling — Take your port checker from #5 and add a timeout, so a single slow/unresponsive port doesn’t freeze the whole scan for ages. Print how long the full scan took.

7. import socket
8. import datetime
9. hostname = input("Enter a hostname: ")
10.
11. initial_time = datetime.datetime.now()
12.
13. for x in range(0, 65535):
14. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
15. s.settimeout(0.2)
16. result = s.connect_ex((hostname, x))
17. if result == 0:
18. print ("The port",x, "is open")
19. s.close()
20. else:
21. pass
22. s.close()
23. final_time = datetime.datetime.now()
24. scan_time = final_time - initial_time
25. print("The total time taken was: ", scan_time)

This one was just an addition to the last question; I removed the printing “that the port is closed” and put pass in there so the loop keeps going and we only get the open ports. Also, to check how long it took, I imported the datetime module and checked the time before the loop started and after it was done and subtracted them from each other to get how long it actually ran. While importing, we could have used “from datetime import datetime” instead of just “import datetime”; that way, we wouldn’t have to write datetime twice when calling it later.

Press enter or click to view image in full size

7. Common ports list — Instead of scanning every port in a range, build a Python list/dict of well-known ports (21-FTP, 22-SSH, 80-HTTP, 443-HTTPS, 445-SMB, 3389-RDP) and scan just those, printing the service name next to each result.

Get Luffytaroouu’s stories in your inbox

Join Medium for free to get updates from this writer.

This again was similar to the last question; we only needed to add a dictionary that defined what service was running on which port and run a for loop that would go through each value in the dictionary and then print it with the value on the key as shown below.

import socket
import datetime
hostname = input("Enter a hostname: ")
ports_to_scan = {"FTP":"21", "SSH":"22", "SMTP":"25", "HTTP":"80", "HTTPS":"443", "SMB":"445", "RDP":"3389"}
initial_time = datetime.datetime.now()

for value in ports_to_scan.values():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(0.2)
result = s.connect_ex((hostname, int(value)))
if result == 0:
print ("The port",value, "is open")
s.close()
else:
pass
s.close()
final_time = datetime.datetime.now()
scan_time = final_time - initial_time
print("The total time taken was: ", scan_time)
Press enter or click to view image in full size

I did this first and realized that I only got the value, not the key. So, since while printing I also need the key lets change the code a little bit.

import socket
import datetime
hostname = input("Enter a hostname: ")
ports_to_scan = {"FTP":"21", "SSH":"22", "SMTP":"25", "HTTP":"80", "HTTPS":"443", "SMB":"445", "RDP":"3389"}
initial_time = datetime.datetime.now()

for key,value in ports_to_scan.items():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(0.2)
result = s.connect_ex((hostname, int(value)))
if result == 0:
print ("The port",value, "is open. It runs following service: ", key)
s.close()
else:
pass
s.close()
final_time = datetime.datetime.now()
scan_time = final_time - initial_time
print("The total time taken was: ", scan_time)

That prints the port and what is running on the port as well, just like we initially wanted

Press enter or click to view image in full size

Level 3 — Command-line arguments (using sys)

8. Take input from the command line instead of input() — Use sys.argv to let the user run your script like python scanner.py google.com 80 instead of being prompted interactively. Print out what arguments were received.

This might have been one of the most confusing exercises that I did till now. I had no idea how sys.argv worked, but it turns out it was similar to how I use PowerShell and basically just passing in arguments when we are running the file.

Here is the script I used for the exercise that worked. (Note: sys.argv[0] is the name of the file, and from sys.argv[1], we can put in which parameters we want and where we want them accordingly. Similarly, the arguments we put in using sys.argv are strings, but as we know port numbers are integers, so make sure to convert that to int.

import sys
import socket
import datetime

hostname = sys.argv[1]
port = sys.argv[2]
initial_time = datetime.datetime.now()
print("The host that the user wants scanned is ", sys.argv[1])
print("The port that the user wants scanned is ", sys.argv[2])

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3.0)
try:
s.connect((hostname, int(port)))
request = f"GET / HTTP/1.1\r\nHost: {hostname}\r\n\r\n"
s.send(request.encode())
banner = s.recv(2056)
print (repr(banner))
except socket.error as e:
print("The error is ", e)
s.close()

Here is my result

Press enter or click to view image in full size

9. Argument validation — Extend #8 so if the user forgets to provide a hostname or port, your script prints a helpful usage message (“Usage: python scanner.py <host> <port>”) and exits cleanly instead of crashing.

10. import sys
11. import socket
12. import datetime
13. if len(sys.argv) == 3:
14. hostname = sys.argv[1]
15. port = sys.argv[2]
16. initial_time = datetime.datetime.now()
17. print("The host that the user wants scanned is ", sys.argv[1])
18. print("The port that the user wants scanned is ", sys.argv[2])
19.
20. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
21. s.settimeout(3.0)
22. try:
23. s.connect((hostname, int(port)))
24. request = f"GET / HTTP/1.1\r\nHost: {hostname}\r\n\r\n"
25. s.send(request.encode())
26. banner = s.recv(2056)
27. print (repr(banner))
28. except socket.error as e:
29. print("The error is ", e)
30. s.close()
31. elif len(sys.argv) < 3:
32. print ("You passed too few arguments, put in the ip address and portnumber you want scanned")
33. else:
34. print ("You passed too many arguments, put in the ip address and portnumber you want scanned")

All I had to do for this task was put a loop to check if the number of arguments we were expecting was present or not, so I counted the sys.argv list using len() and checked if it had 3 items; if it did, then the code would run; else it would print a message accordingly.

Press enter or click to view image in full size

10. Multiple ports via command line — Let the user pass a comma-separated list of ports (python scanner.py google.com 22,80,443) and scan all of them.

For this exercise, we need to take multiple ports in, which is not hard. We just add commas in the command line and put in more ports, so what changes? We know that when we insert anything from the command line, it goes in as a string. So, we have to break that string into something like a list so we can make it work, and then we could use a for loop as we did before and scan each port. For this purpose, we use the “.split()” method. This method is used to break down a string into multiple sections, and we can choose where to split the string. Since we determined that we will be inputting multiple strings using commas, let’s do the same here. So, for sys.argv[2], where we get the port, we will use .split(“,”) to split them by commas. Sys.argv[2].split(“,”).

The following is the code.

import sys
import socket
import datetime
if len(sys.argv) == 3:
hostname = sys.argv[1]
port = sys.argv[2].split(",")
initial_time = datetime.datetime.now()
print("The host that the user wants scanned is ", sys.argv[1])
print("The port that the user wants scanned is ", sys.argv[2])

for x in port:

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3.0)
try:
s.connect((hostname, int(x)))
request = f"GET / HTTP/1.1\r\nHost: {hostname}\r\n\r\n"
s.send(request.encode())
banner = s.recv(2056)
print (repr(banner))
except socket.error as e:
print("The error is: ", e)
s.close()
elif len(sys.argv) < 3:
print ("You passed too few arguments, put in the ip address and portnumber you want scanned")
else:
print ("You passed too many arguments, put in the ip address and portnumber you want scanned")

Result

Press enter or click to view image in full size

Level 4 — Speed and structure (this is where it starts feeling “real”)

11. Threading for speed — Your Level 2 scanner checks ports one at a time, which is slow for large ranges. Learn threading (or concurrent.futures) to check multiple ports simultaneously and compare the time difference against your sequential version from #6.

Ughh, this was a tough one. I had to research threading and concurrent.futures, and honestly went with concurrent.futures just because I found some documentation and it seemed more interesting to learn. These were some of the links I took reference from

https://alexwlchan.net/2019/adventures-with-concurrent-futures/

https://www.packetswitch.co.uk/what-is-concurrent-futures-and-how-can-it-boost-your-python-performance/

https://docs.python.org/3/library/concurrent.futures.html
https://medium.com/@smrati.katiyar/introduction-to-concurrent-futures-in-python-009fe1d4592c

https://www.pythonsheets.com/notes/concurrency/python-futures.html

I made a function out of the scanning part so that we could call it multiple times (cause again the whole purpose of this is to run multiple scans at the same time; the previous loops only did one at a time). The following is the code:

import sys
import socket
import datetime
import concurrent.futures

def scan_port(hostname, port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3.0)
try:
s.connect((hostname, port))
request = f"GET / HTTP/1.1\r\nHost: {hostname}\r\n\r\n"
s.send(request.encode())
banner = s.recv(2056)
print(f"Port {port}:", repr(banner))
except socket.error as e:
print(f"Port {port}: error -", e)
finally:
s.close()

if len(sys.argv) == 3:
hostname = sys.argv[1]
ports = range(0,65535)

print("The host that the user wants scanned is ", hostname)
print("The ports that the user wants scanned are ", ports)

initial_time = datetime.datetime.now()

with concurrent.futures.ThreadPoolExecutor() as executor:
for port in ports:
executor.submit(scan_port, hostname, port)

final_time = datetime.datetime.now()
print("The total time taken was: ", final_time - initial_time)

elif len(sys.argv) < 3:
print("You passed too few arguments, put in the ip address and port number you want scanned")
else:
print("You passed too many arguments, put in the ip address and port number you want scanned")

Here, everything else is the same; it takes a hostname from the command line and scans the port from 0–65535, so basically all ports. I also put the time-checking format back in for this, as I also wanted to compare it with the previous exercise where we scanned all ports and wanted to check the difference in time. Initially, when I ran the script, I realized that since I was also printing the errors, there were a lotta errors, so I removed the error-printing part and used pass there so only the connected ports would be printed.

You can see underneath how it looked initially. We can see that the time taken has significantly reduced; previously, when we ran the program sequentially, it took around 4 minutes; now it takes around one.

Press enter or click to view image in full size

I changed the program as follows

import sys
import socket
import datetime
import concurrent.futures

def scan_port(hostname, port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3.0)
try:
s.connect((hostname, port))
request = f"GET / HTTP/1.1\r\nHost: {hostname}\r\n\r\n"
s.send(request.encode())
banner = s.recv(2056)
print(f"Port {port}:", repr(banner))
except socket.error as e:
pass
finally:
s.close()

if len(sys.argv) == 3:
hostname = sys.argv[1]
ports = range(0,65535)

print("The host that the user wants scanned is ", hostname)
print("The ports that the user wants scanned are ", ports)

initial_time = datetime.datetime.now()

with concurrent.futures.ThreadPoolExecutor() as executor:
for port in ports:
executor.submit(scan_port, hostname, port)

final_time = datetime.datetime.now()
print("The total time taken was: ", final_time - initial_time)

elif len(sys.argv) < 3:
print("You passed too few arguments, put in the ip address and port number you want scanned")
else:
print("You passed too many arguments, put in the ip address and port number you want scanned")

Also realized that since we weren’t using multiple ports given by the user instead of scanning all possible ports, I removed the if statement cause len(sys.argv) wouldn’t be equal to 3 anymore. I also realized that since, in one of the tasks, we removed the connect_ex method and tried to banner grab for HTTP, a lot of ports that aren’t chatty stopped responding, so basically when I used this program to scan, this is the result I got.

Press enter or click to view image in full size
Press enter or click to view image in full size

Now, based on the results, I knew that it wasn’t showing all the open ports, as I am running the scan against my Metasploitable. Then, when I looked into it, I realized that because I was only sending the HTTP-encoded request, most other protocols saw that and ignored it and without the .connect_ex, we couldn’t confirm if the port was open or not; we assumed the port was closed, so I went back and changed the script to the following

import sys
import datetime
import concurrent.futures
import socket

def scanningPorts(hostname, port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5.0)
result = s.connect_ex((hostname, port))

if result == 0:
print(f"The port numbered: {port} is open")
if port == 80:
try:
request = f"GET / HTTP/1.1\r\nHost: {hostname}\r\n\r\n"
s.send(request.encode())
banner = s.recv(2056)
print(f"Port {port} banner:", repr(banner))
except socket.error as e:
print(f"Port {port}: connected, but banner grab failed -", e)

s.close()

hostname = sys.argv[1]
ports = range(0, 65535)

print("The host that the user wants scanned is ", hostname)
print("The ports that the user wants scanned are ", ports)

initial_time = datetime.datetime.now()

with concurrent.futures.ThreadPoolExecutor() as executor:
for port in ports:
executor.submit(scanningPorts, hostname, port)

final_time = datetime.datetime.now()
print("The total time taken was: ", final_time - initial_time)

Now, with an exception made for HTTP, the scan worked properly. Here are the results

Press enter or click to view image in full size

12. Organize into functions/classes — Refactor everything so far into clean, reusable functions (scan_port(host, port), scan_range(host, start, end)) rather than one long script

For this, I thought about it and wanted to combine what we have done before. Until now, I have been changing the script in case I want to check a range of ports, a particular port, or a list of ports, but now I want to combine these and use conditional statements. So basically, when we input a single port from the command line, only that port gets scanned. If we want a list of ports, we can comma-separate and input a list of ports, and just like before, it will get split by commas, and each of those ports gets scanned. Furthermore, for a range of ports, I used the same logic; I split it with a hyphen, then put it in the range(start,end) function.

Here is the code for it

import sys
import datetime
import concurrent.futures
import socket

def scanningPorts(hostname, port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3.0)
result = s.connect_ex((hostname, port))

if result == 0:
print(f"The port numbered: {port} is open")
if port == 80:
try:
request = f"GET / HTTP/1.1\r\nHost: {hostname}\r\n\r\n"
s.send(request.encode())
banner = s.recv(2056)
print(f"Port {port} banner:", repr(banner))
except socket.error as e:
print(f"Port {port}: connected, but banner grab failed -", e)
s.close()

if len(sys.argv) != 3:
print("Usage: python scanner.py <host> <port | port1,port2,... | start-end>")
sys.exit()

hostname = sys.argv[1]
port_arg = sys.argv[2]

if "," in port_arg:
ports = [int(p) for p in port_arg.split(",")]
elif "-" in port_arg:
start, end = port_arg.split("-")
ports = range(int(start), int(end) + 1)
else:
ports = [int(port_arg)]

print("The host that the user wants scanned is ", hostname)
print("The ports that the user wants scanned are ", list(ports))

initial_time = datetime.datetime.now()

with concurrent.futures.ThreadPoolExecutor() as executor:
for port in ports:
executor.submit(scanningPorts, hostname, port)

final_time = datetime.datetime.now()
print("The total time taken was: ", final_time - initial_time)

Here are the results

Press enter or click to view image in full size
Press enter or click to view image in full size

Honestly, I think it is working beautifully at this point.

Level 5 — Real network scanning concepts

13. Host discovery (ping sweep) — Before scanning ports on a single host, learn to check which hosts on a subnet are even alive — this usually means either shelling out to the system ping command via subprocess, or building a simple ICMP check.

Till now, we were running the scan against a particular IP address, or hostname; now let’s run the scan against a list/range of ip addresses

import sys
import subprocess
import socket
import datetime
import concurrent.futures
import ipaddress

def parse_range(spec, convert):
"""Parse a 'single', 'a,b,c', or 'start-end' spec into a list, using `convert` to turn each piece into its real type."""
if "," in spec:
return [convert(p) for p in spec.split(",")]
elif "-" in spec:
start_str, end_str = spec.split("-")
start = convert(start_str)
end = convert(end_str)
return [convert(i) for i in range(int(start), int(end) + 1)]
else:
return [convert(spec)]

def ping_host(ip):
result = subprocess.run(
["ping", "-c", "1", "-W", "1", str(ip)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
return result.returncode == 0

def scan_port(hostname, port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1.0)
result = s.connect_ex((hostname, port))
s.close()
return result == 0

if len(sys.argv) != 3:
print("Usage: python sweep.py <ip | ip1,ip2,... | start_ip-end_ip> <port | port1,port2,... | start-end>")
sys.exit()

ip_arg = sys.argv[1]
port_arg = sys.argv[2]

targets = parse_range(ip_arg, ipaddress.ip_address)
ports = parse_range(port_arg, int)

print(f"Scanning {len(targets)} host(s) across {len(ports)} port(s)...")

initial_time = datetime.datetime.now()

with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor:
ping_futures = {executor.submit(ping_host, ip): ip for ip in targets}
alive_hosts = []
for future in concurrent.futures.as_completed(ping_futures):
ip = ping_futures[future]
if future.result():
alive_hosts.append(ip)

print(f"\n{len(alive_hosts)} of {len(targets)} hosts are alive:")
for ip in sorted(alive_hosts, key=lambda ip: int(ip)):
print(" ", ip)

with concurrent.futures.ThreadPoolExecutor(max_workers=100) as executor:
port_futures = {}
for ip in alive_hosts:
for port in ports:
future = executor.submit(scan_port, str(ip), port)
port_futures[future] = (ip, port)

print("\nOpen ports:")
for future in concurrent.futures.as_completed(port_futures):
ip, port = port_futures[future]
if future.result():
print(f" {ip}:{port} is open")

final_time = datetime.datetime.now()
print("\nTotal time taken:", final_time - initial_time)

Until now I had two almost identical pieces of code doing the same kind of parsing — one for ports (splitting on commas or a dash) back in exercise #12, and I needed the same logic for IPs now too. Instead of just copy-pasting the same if/elif/else block twice with minor changes, I wanted to pull that logic out into one function I could reuse for both. So, I built this section:

def parse_range(spec, convert):
if "," in spec:
return [convert(p) for p in spec.split(",")]
elif "-" in spec:
start_str, end_str = spec.split("-")
start = convert(start_str)
end = convert(end_str)
return [convert(i) for i in range(int(start), int(end) + 1)]
else:
return [convert(spec)]

The part that was genuinely new to me here was the convert parameter. I had never passed a function in as an argument before, the way you’d pass a string or a number. But apparently in Python, a function is just an object like anything else, so you can hand one to another function without calling it, and have it get called later, inside. So, when I call it like this:

targets = parse_range(ip_arg, ipaddress.ip_address)
ports = parse_range(port_arg, int)

The same parse_range function handles both IPs and ports, just by swapping out what convert points to. Inside the function wherever I wrote convert(p), it’s really calling whichever function got passed in, ipaddress.ip_address(p) in one case, int(p) in the other, without parse_range itself needing to know or care which one it’s working with.

For actually splitting an IP range like 192.168.1.220–192.168.1.230, I had to figure out a new trick, since IPs don’t increment like plain numbers the way ports do. I found out that the ipaddress module lets you convert any IP address into its underlying raw integer with int(), since under the hood every IPv4 address really is just a 32-bit number. So, I could do int(start) and int(end), range() between those two plain integers exactly like I already knew how to do with ports, and then convert each resulting number back into a proper IP with ipaddress.ip_address(i) again.

Once I had this one shared function, the rest of the script followed the same two-stage structure I built in the ping sweep exercise before this one — first ping every target IP to see which ones are actually alive, using a ThreadPoolExecutor, and only once that’s completely finished, move on to port-scanning just the hosts that responded:

with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor:
ping_futures = {executor.submit(ping_host, ip): ip for ip in targets}
alive_hosts = []
for future in concurrent.futures.as_completed(ping_futures):
ip = ping_futures[future]
if future.result():
alive_hosts.append(ip)

I used a dictionary here instead of a plain list of futures because ping_host only returns True or False; it doesn’t return which IP it was testing. So, the dictionary maps each Future back to the IP it belongs to, meaning once a future finishes I can look up ping_futures[future] to find out which IP that result is actually for.

The port-scanning stage then only runs against alive_hosts, nested in a loop over every port for every alive IP:

with concurrent.futures.ThreadPoolExecutor(max_workers=100) as executor:
port_futures = {}
for ip in alive_hosts:
for port in ports:
future = executor.submit(scan_port, str(ip), port)
port_futures[future] = (ip, port)

I kept these as two separate ThreadPoolExecutor blocks on purpose, rather than trying to combine everything into one pass. The reasoning is that I need to know which hosts are alive before I even start port-scanning them, there’s no point spending time and connections scanning ports on a host that’s already dead. So, the ping sweep has to fully finish (its block has to close) before the port-scanning stage even starts.

I tested it with a small range covering my Metasploitable box plus some IPs around it, along with a handful of ports:

python learningtoScanner.py 192.168.1.220–192.168.1.230 21,22,80,445

Press enter or click to view image in full size

python learningtoScanner.py 192.168.1.1,192.168.1.223 0–1024

Press enter or click to view image in full size

I realized that the tests were sometimes inconsistent, possibly because we had only been sending one packet and waiting for one second for each device. Instead, I changed that part to sending 4 packets and waiting 2 seconds, and the results got more consistent.

["ping", "-c", "4", "-W", "2", str(ip)],

P.S. For this last test, I had already turned off my Metasploitable, so that won’t show up in this.

Press enter or click to view image in full size

--

--