Why would you want to write a port scanner?
Writing a port scanner is a great way to learn the basics of the TCP protocol, which is the transport layer used by most internet protocols (including HTTP & SSH).
It’s also a good exercise to learn more about how Ruby network programming works.
Let’s gets started by talking about ports!
What Is a Port?
When we talk about ports what are we really talking about? A port at the O.S. (Operating System) level is just a “file descriptor” associated with a process.
A file descriptor is just a number which is used to reference an open I/O channel, like stdout
(standard output), a network socket or a file.
When the OS receives a TCP/IP packet it looks at the destination port & tries to find a process which is listening on this port.
Then if there is a listening process the packet is delivered to it.
Port Ranges
There are 65.535 ports available in total, but in practice not all of them are used regularly.
Ports can be divided into 3 groups:
Range | Name | Examples |
---|---|---|
1-1023 |
Well-known ports | 22 (SSH), 80 (HTTP), 443 (HTTPS) |
1024-49151 |
Registered ports | 3306 (MySQL), 5432 (PostgreSQL) |
49152-65535 |
Ephemeral ports | Chrome, Firefox |
Ports in the 2nd range are assigned by the IANA (Internet Assigned Numbers Authority).
And the 3rd range is used for “dynamic” or “ephemeral” ports, these are the ports used by the client side of the connection to receive data from the server.
The Basics of TCP Communication
Now you should be more familiar with ports, but we are still not ready to write our port scanner.
First, we need to discuss what it means for a port to be open. Then we will examine the behavior of both an open port & a closed port at the network level so we can differentiate them.
How can you know if a port is open?
An open port just means that there is an application listening on the other end & that we have access to it (not blocked by a firewall).
Let’s see how a new TCP connection is initiated.
A new TCP connection is started with a SYN
packet. This packet symbolizes the start of a new connection.
There are three possible outcomes:
- The server answers with a
SYN/ACK
– this means it’s ready to establish a connection - The server answers with a
RST
packet – this means that the connection is rejected - The server doesn’t answer at all (some firewalls implement this as the DROP policy)
If the client receives a SYN/ACK
packet then it can finish establishing the connection by sending an ACK
packet.
This is called the “three-way handshake“.
Note: A great way to watch this happening is by using a networking tool like tcpdump or wireshark. These tools allow you to see every packet that is coming in & out of your system.
Here is an example connection from tshark (wireshark’s command-line interface):
Now that you have a basic understanding of how a TCP connection is established we can write a simple port scanner.
Let’s Write a Port Scanner!
The simplest way to write our scanner is to open a new TCP connection using TCPSocket
& then rely on the fact that a rejected connection will raise the Errno::ECONNREFUSED
exception.
Here is the code:
require 'socket' PORT = ARGV[0] || 22 HOST = ARGV[1] || 'localhost' begin socket = TCPSocket.new(HOST, PORT) status = "open" rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT status = "closed" end puts "Port #{PORT} is #{status}."
This code has some limitations:
We can only scan one port at a time. Often you want to scan a range of ports to get a better idea of what services are exposed on a specific host.
Another limitation with this code is that a non-responding port (the third of the outcomes I mentioned earlier) will make you wait for about 20 seconds until the connection times out.
This can be solved with a combination of connect_nonblock
, IO.select
& Socket
(instead of TCPSocket
, which initiates a connection as soon as you create the object).
Example:
require 'socket' TIMEOUT = 2 def scan_port(port) socket = Socket.new(:INET, :STREAM) remote_addr = Socket.sockaddr_in(port, 'www.example.com') begin socket.connect_nonblock(remote_addr) rescue Errno::EINPROGRESS end _, sockets, _ = IO.select(nil, [socket], nil, TIMEOUT) if sockets p "Port #{port} is open" else # Port is closed end end PORT_LIST = [21,22,23,25,53,80,443,3306,8080] threads = [] PORT_LIST.each { |i| threads << Thread.new { scan_port(i) } } threads.each(&:join)
The IO.select
call will wait until the socket is ready to receive data (which means it's open) or until TIMEOUT
time has passed, after which we can assume that the port is either closed or ignoring connection requests.
So, this is all great as a learning exercise, but if you need a proper port scanner you should use something like nmap.
Nmap is open source & has been under active development for over 15 years.
This is what nmap output looks like:
I know that's a lot of info, so to help you remember write a comment with 2 new things you learned from this post 🙂
Summary
In this post you learned what a port is, the different port ranges available, how a TCP connection is initiated (the three-way handshake) & how to write a basic port scanner using Ruby.
If you enjoyed this post don't forget to share it so more people can enjoy it 🙂
This is a good program to try after learning ruby. Thanks for sharing.
Thanks for reading! 🙂
I really enjoy reading your posts 🙂 Thank you for sharing, Jesus!
Thanks for your kind comments, Martin 🙂
Cool, i like this.
Thank you 🙂
First things firsts: article is nice in general, has nice recap about networking and code samples. Thanks. 🙂
Now the suggestions for improvement:
0) You’re talking about exactly TCP, not “TCP/IP”. 🙂
1) Given the title I was expecting to see the actual port-scanner implemented in Ruby, not just script to check one port at a time. 🙂
Given that you alredy use IO.select you could try to show how multiple sockets can be dealed with, so it will be an actual (even if very very simple and crude) port scanner.
I, for instance, felt deceived to some degree – it’s like you would say “how to implement DVCS in Ruby” in title/link on RubyFlow and then inside the article showed how to check the checksum/calculate diff of single file and said “don’t implement DVCS, use git instead”.
Already existing tools are good and nice (and nmap is awesome), but I thought that purpose of an article wasn’t just to mention them, but show simple and crude implementations you can learn something from.
3) It would also be nice to add useful links like http://ruby-doc.org/core-2.3.3/IO.html#method-c-select and https://stackoverflow.com/questions/6165735/understanding-io-select-when-reading-socket-in-ruby
Thanks a lot for your feedback! 🙂