参考:

实验环境: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:

java udp判断端口是否打开 如何判断udp端口是否打开_系统调用


这是为什么呢?原因在于内核中 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 编译并运行:

java udp判断端口是否打开 如何判断udp端口是否打开_系统调用_02

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 端口是否开放。