udp 檢測對端端口是否開放


參考:

實驗環境:os: centos8.5 / kernel: 4.18.0 / gcc: 8.5.0 / arch: x86-64
示例內核代碼版本:5.15.5

1. 概述

本篇文章主要記錄如何檢測對端某個端口上是否提供了 udp 服務。

2. 如何檢測

2.1 tcp 端口開放檢測

對於 tcp,只需要寫一個 tcp 客戶端,阻塞調用 connect() 函數來判斷返回值:

  • 如果目的 host 或 port 沒有回應,那么第一個 syn 包將超時(經歷重傳),返回 -1(ECONNREFUSED)
  • 如果目的 host 對應端口沒有提供 tcp 服務,則會回復 rst,返回 -1(ECONNREFUSED)

2.2 udp 端口開放檢測

2.2.2 icmp unreachable

icmp 不可達消息分為 host 不可達和 port 不可達,host 不可達一般是路由器回復的,port 不可達一般是主機回復的。且一個 udp 應用層網絡服務也可以手動回復 host/port unreachable icmp 報文回去。
但是路由器並不一定會回復 icmp host unreachable 報文,很多情況下發往一個不存在主機的 udp 報文都會石沉大海,沒有任何回應。

2.2.2 connect() 系統調用

我們知道 udp 不是一個面向連接的協議,沒有三次握手的過程,但是我們依然可以調用 connect() 函數:

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

在 udp 中調用 connect() 函數,實際上內核只是將第二個參數 addr 指代的目的 host 和 port 記錄下來,形成了獨一無二的網絡四元組,但是網絡上什么都不會發生。所以 udp 無法利用 connect() 檢測端口是否開放。

2.2.3 sendto() 系統調用

send() 系統調用返回的是發送到網絡中的字節數,返回值與對端的回應無關。

2.2.4 sendto() + recvfrom()

在不調用 connect() 的前提下,udp 發送數據必須調用 sendto() 來指出目標主機的 host 和 port,這個時候如果對端回應了 icmp unreachable(port 或 host),recvfrom() 系統調用也將一直阻塞,沒有任何返回:

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define DEST_PORT 3000
#define DSET_IP_ADDRESS  "127.0.0.1"

int main() {
  // 建立udp socket
  int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
  if (sock_fd < 0) {
    perror("socket");
    exit(1);
  }

  // 設置目標 address
  struct sockaddr_in addr_serv;
  int len;
  memset(&addr_serv, 0, sizeof(addr_serv));
  addr_serv.sin_family = AF_INET;
  addr_serv.sin_addr.s_addr = inet_addr(DSET_IP_ADDRESS);
  addr_serv.sin_port = htons(DEST_PORT);
  len = sizeof(addr_serv);

  char send_buf[20] = "hey, who are you?";
  char recv_buf[20];
  printf("client send: %s\n", send_buf);
  
  int send_num = sendto(sock_fd, send_buf, strlen(send_buf), 0, (struct sockaddr *)&addr_serv, len);
  if (send_num < 0) {
    printf("sendto error no: %d %d %s\n", send_num, errno, strerror(errno));
    exit(1);
  }

  int recv_num = recvfrom(sock_fd, recv_buf, sizeof(recv_buf), 0,
      (struct sockaddr *)&addr_serv, (socklen_t *)&len);
  if (recv_num < 0) {
    printf("recvfrom error no: %d %d %s\n", recv_num, errno, strerror(errno));
    exit(1);
  }

  recv_buf[recv_num] = '\0';
  printf("client receive %d bytes: %s\n", recv_num, recv_buf);

  close(sock_fd);
  return 0;
}

上述代碼使用 gcc test.c -o udp 編譯,運行后代碼將一直阻塞在 recvfrom() 系統調用中,抓包能看到對端回復了 ICMP port unreachable:

這是為什么呢?原因在於內核中 udp 收到 icmp 報文后的處理函數 --->linux-5.15.5/net/ipv4/udp.c::__udp4_lib_err():

/*
 * This routine is called by the ICMP module when it gets some
 * sort of error condition.  If err < 0 then the socket should
 * be closed and the error returned to the user.  If err > 0
 * it's just the icmp type << 8 | icmp code.
 * Header points to the ip header of the error packet. We move
 * on past this. Then (as it used to claim before adjustment)
 * header points to the first 8 bytes of the udp header.  We need
 * to find the appropriate port.
 */
int __udp4_lib_err(struct sk_buff *skb, u32 info, struct udp_table *udptable)
{
	// ...
        // 忽略前面的代碼(主要是設置錯誤碼)

        // 這里,如果 inet->recverr 為 0,那么會進入 if 語句
	if (!inet->recverr) {
                // 這里,如果 udp 不是處於 TCP_ESTABLISHED 狀態(調用過 connect()),那么,進入 out
		if (!harderr || sk->sk_state != TCP_ESTABLISHED)
			goto out;
	} else
		ip_icmp_error(sk, skb, err, uh->dest, info, (u8 *)(uh+1));

	sk->sk_err = err;
        // 繼續向應用層報告 icmp 錯誤
	sk_error_report(sk);

        // 直接退出,不向應用層報告 icmp 錯誤
out:
	return 0;
}

如上代碼,所以,我們可以設置 inet->recverr 不為 0,或者將 udp 狀態設置為 TCP_ESTABLISHED,都能在應用層收到 icmp unreachable 錯誤反饋:

  • 設置 inet->recverr 為 1,調用 setsockopt() 函數設置 IP_RECVERR 參數為 1
  • 將 udp 狀態設置為 TCP_ESTABLISHED,在 send() 前調用 connect()

2.2.5 icmp unreachable 正確的檢測方式

經過上一節的討論,我們可以有如下代碼:

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define DEST_PORT 3000
#define DSET_IP_ADDRESS  "127.0.0.1"

int main() {
  // 建立udp socket
  int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
  if (sock_fd < 0) {
    perror("socket");
    exit(1);
  }

  // 設置目標 address
  struct sockaddr_in addr_serv;
  int len;
  memset(&addr_serv, 0, sizeof(addr_serv));
  addr_serv.sin_family = AF_INET;
  addr_serv.sin_addr.s_addr = inet_addr(DSET_IP_ADDRESS);
  addr_serv.sin_port = htons(DEST_PORT);
  len = sizeof(addr_serv);

  char send_buf[20] = "hey, who are you?";
  char recv_buf[20];
  printf("client send: %s\n", send_buf);

#if 1
  int ret = connect(sock_fd, (struct sockaddr *)&addr_serv, len);
  if (ret < 0) {
    printf("connect error no: %d %d %s\n", ret, errno, strerror(errno));
    exit(1);
  }
#elif 0
  int val = 1;
  (void)setsockopt(sock_fd, IPPROTO_IP, IP_RECVERR , &val, sizeof(int));
#endif
  
  int send_num = sendto(sock_fd, send_buf, strlen(send_buf), 0, (struct sockaddr *)&addr_serv, len);
  if (send_num < 0) {
    printf("sendto error no: %d %d %s\n", send_num, errno, strerror(errno));
    exit(1);
  }

  int recv_num = recvfrom(sock_fd, recv_buf, sizeof(recv_buf), 0,
      (struct sockaddr *)&addr_serv, (socklen_t *)&len);
  if (recv_num < 0) {
    printf("recvfrom error no: %d %d %s\n", recv_num, errno, strerror(errno));
    exit(1);
  }

  recv_buf[recv_num] = '\0';
  printf("client receive %d bytes: %s\n", recv_num, recv_buf);

  close(sock_fd);
  return 0;
}

使用 gcc test.c -o udp 編譯並運行:

2.3 可用性的問題

實際上上面的程序只能檢測 udp 端口沒有開放(收到 icmp unreachable),如果 udp 端口開放了,或者 udp 消息石沉大海,則會面臨如下問題:

  • 如果調用是阻塞的,udp 服務不做任何回應,那么 recv()/recvfrom() 不返回,因為沒有 icmp unreachable 或其它任何消息
  • 如果調用是阻塞的,udp 消息石沉大海,那么 recv()/recvfrom() 不返回,因為沒有 icmp unreachable 消息
  • 如果調用是非阻塞的,那么 recv()/recvfrom() 將立即返回,並收到 -1(EAGAIN) 錯誤,這樣也無法判斷 udp 端口是否開放

如果確定能收到 icmp unreachable(host/port) 的前提下,一種解決方法是在阻塞模式下,設置 socket 接收數據超時時間:

int setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv);

如果 recv()/recvfrom() 返回 -1(EAGAIN),說明 udp 端口是開放的,如果返回 -1(ECONNREFUSED),說明收到了 icmp unreachable(host 或者 port 不可達)。
但是要注意,正常情況下 -1(EAGAIN) 無法區分是 udp 消息石沉大海還是因為 udp 服務故意什么都不回應。因此個人認為沒有完美的方法檢測 udp 端口是否開放。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM