Tại sao bạn muốn viết một port scanner?
Viết port scanner là một cách tuyệt vời để học cơ bản của giao thức TCP, là lớp vận chuyển được dùng bởi hầu hết các giao thức internet (bao gồm HTTP và SSH).
Đây cũng là bài tập tốt để tìm hiểu sâu hơn về cách lập trình mạng trong Ruby hoạt động.
Hãy bắt đầu bằng cách nói về port!
Port là gì?
Khi chúng ta nói về port, chúng ta thực sự đang nói về gì? Ở cấp độ HĐH (Hệ điều hành), một port chỉ là một "file descriptor" (bộ mô tả tệp) liên kết với một tiến trình.
Một file descriptor chỉ là một số được dùng để tham chiếu một kênh I/O mở, như stdout (đầu ra chuẩn), một network socket hoặc một tệp.
Khi HĐH nhận được gói tin TCP/IP, nó nhìn vào port đích và cố gắng tìm một tiến trình đang lắng nghe trên port này.
Sau đó, nếu có tiến trình lắng nghe, gói tin sẽ được chuyển giao cho nó.
Các dải Port
Tổng cộng có 65.535 port có sẵn, nhưng trong thực tế không phải tất cả đều được dùng thường xuyên.
Port có thể chia thành 3 nhóm:
| Dải | Tên | Ví dụ |
|---|---|---|
1-1023 |
Well-known ports (Port phổ biến) | 22 (SSH), 80 (HTTP), 443 (HTTPS) |
1024-49151 |
Registered ports (Port đã đăng ký) | 3306 (MySQL), 5432 (PostgreSQL) |
49152-65535 |
Ephemeral ports (Port tạm thời) | Chrome, Firefox |
Các port trong dải thứ 2 được gán bởi IANA (Internet Assigned Numbers Authority).
Và dải thứ 3 được dùng cho các port "động" hoặc "ephemeral", đây là các port phía client dùng để nhận dữ liệu từ server.
Cơ bản của giao tiếp TCP
Giờ bạn nên quen với port hơn, nhưng chúng ta vẫn chưa sẵn sàng viết port scanner.
Trước tiên, chúng ta cần thảo luận xem có nghĩa là gì khi một port mở. Sau đó, chúng ta sẽ kiểm tra hành vi của cả port mở và port đóng ở cấp độ mạng để có thể phân biệt chúng.
Làm thế nào bạn biết một port đang mở?
Một port mở chỉ có nghĩa là có một ứng dụng đang lắng nghe ở đầu kia và chúng ta có quyền truy cập vào nó (không bị chặn bởi tường lửa).
Hãy xem cách một kết nối TCP mới được khởi tạo.

Một kết nối TCP mới bắt đầu bằng gói tin SYN. Gói tin này tượng trưng cho sự bắt đầu của một kết nối mới.
Có ba kết quả có thể xảy ra:
- Server trả lời với gói tin
SYN/ACK– nghĩa là nó sẵn sàng thiết lập kết nối - Server trả lời với gói tin
RST– nghĩa là kết nối bị từ chối - Server không trả lời gì cả (một số tường lửa triển khai chính sách DROP)
Nếu client nhận được gói tin SYN/ACK, nó có thể hoàn tất việc thiết lập kết nối bằng cách gửi gói tin ACK.
Điều này được gọi là "three-way handshake" (bắt tay ba bước).
Lưu ý: Cách tuyệt vời để quan sát điều này xảy ra là dùng công cụ mạng như tcpdump hoặc wireshark. Các công cụ này cho phép bạn thấy mọi gói tin đi vào và ra khỏi hệ thống của bạn.
Dưới đây là ví dụ kết nối từ tshark (giao diện dòng lệnh của wireshark):

Giờ bạn đã có hiểu biết cơ bản về cách kết nối TCP được thiết lập, chúng ta có thể viết một port scanner đơn giản.
Hãy viết Port Scanner!
Cách đơn giản nhất để viết scanner là mở một kết nối TCP mới dùng TCPSocket và dựa vào việc kết nối bị từ chối sẽ ném ngoại lệ Errno::ECONNREFUSED.
Đây là mã:
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}."
Mã này có một số hạn chế:
Chúng ta chỉ có thể quét một port tại một thời điểm. Thường bạn muốn quét một dải port để có ý tưởng tốt hơn về dịch vụ nào đang exposed trên một host cụ thể.
Một hạn chế khác là port không phản hồi (kết quả thứ ba tôi nhắc ở trên) sẽ khiến bạn chờ khoảng 20 giây cho đến khi kết nối timeout.
Điều này có thể giải quyết bằng kết hợp connect_nonblock, IO.select và Socket (thay vì TCPSocket, vốn khởi tạo kết nối ngay khi bạn tạo đối tượng).
Ví dụ:
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)
Lời gọi IO.select sẽ chờ cho đến khi socket sẵn sàng nhận dữ liệu (nghĩa là nó mở) hoặc cho đến khi thời gian TIMEOUT trôi qua, sau đó chúng ta có thể giả định port đó hoặc đóng hoặc đang bỏ qua yêu cầu kết nối.
Vậy, tất cả điều này tuyệt vời như bài tập học tập, nhưng nếu bạn cần một port scanner thích hợp, bạn nên dùng thứ như nmap.
Nmap là mã nguồn mở và đã được phát triển tích cực hơn 15 năm.
Đây là giao diện đầu ra của nmap:

Tôi biết đó là nhiều thông tin, vì vậy để giúp bạn nhớ, hãy viết bình luận với 2 điều mới bạn học được từ bài viết này 🙂
Tóm tắt
Trong bài viết này, bạn đã học port là gì, các dải port khác nhau, cách kết nối TCP được khởi tạo (three-way handshake) và cách viết port scanner cơ bản bằng Ruby.
Nếu bạn thích bài viết này, đừng quên chia sẻ để nhiều người hơn có thể tận hưởng 🙂