【Java】InetAddress.isReachable()”失效“的原因探究

背景

在某些场景下,我们可能需要在Java中判断到某个主机的网络是否连通,比如我们的系统中可能有业务需要录入一些主机信息,此时为了更好的用户体验,我们可能会在前端页面上提供一个拨测按钮,让用户可以在输入主机地址之后进行连通性检验,来判断我们的系统和目标主机是否网络可达,同时也能一定程度上保证用户输入的主机地址有效性。

这只讨论使用最简单且通用的方式判断,如果我们清楚目标主机/应用有其他可用的连通性测试接口,那么使用这种更准确的接口当然是更好的解决方案。比如若要测数据库连通性,我们可以尝试建立一个数据库连接来判断,这样更加准确

提到这种简单的连通性测试,有计算机基础的同学肯定会想到ping命令,如果我们借助ping命令来检测目的主机的网络连通性,则Java代码可以这样写:

java 复制代码
public static void main(String[] args) throws Exception {
    String ip = "192.168.121.136";
    linuxPing(ip);
}

/**
 * Linux平台下的Ping,使用Runtime来执行命令。Windows中ping参数不太一样,需要对应调整
 */
public static void linuxPing(String ip) throws IOException, InterruptedException {
    Process exec = Runtime.getRuntime().exec("ping -c 4 " + ip);
    BufferedReader reader = new BufferedReader(new InputStreamReader(exec.getInputStream()));
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
        //解析每行输出,判断最终结果
    }
}

上述实现方式确实也是一种方案,但需要考虑几个跨平台带来的问题:

  1. Windows和Linux下的ping命令选项参数不一致,如指定次数时Windows中用-n选项,而Linux中用-c选项。需要判断平台,并使用对应的ping命令
  2. IPv6和IPv4使用的ping命令选项可能不一致(如Linux中ping命令在使用IPv6地址时应该为ping -6 xxx,Windows中可不指定),因此在实现时需要区分不同类型的地址
  3. 不同平台下ping命令的输出格式也不一定相同,如果使用命令的输出来解析结果,可能需要适配不同的格式

当然,这种常用功能想必早已经有开源成熟的解决方案,我们自己去实现属实有些许费力不讨好的意味。这时我们会发现,Java中早就给我们提供了InetAddress.isReachable()方法来完成这个任务,于是我们自信地写出以下代码:

java 复制代码
public static void main(String[] args) throws Exception {
    String ip;
    if (args != null && args.length > 0) {
        ip = args[0];
    } else {
        ip = "192.168.121.136";
    }
    InetAddress address = null;
    try {
        address = InetAddress.getByName(ip);
        if (address.isReachable(5 * 1000)) {
            System.out.println(ip + " is reachable");
        } else {
            System.out.println(ip + " is not reachable");
        }
    } catch (IOException e) {
        System.out.println("exception: " + e.getMessage());
    }
}

上述代码使用了Java标准库方法,跨平台、IPv4/IPv6区分等问题我们就通通不用考虑了,只需要把异常处理一下即可。但代码上线时,问题又双叒叕来了------明明自测还好好的,怎么上线全部连通性测试都失败了??

现象

当我们使用上述代码进行测试时,会发现一种奇怪的现象------使用ping命令能ping通,但用InetAddress.isReachable()却老是不行

查阅网上的资料,有的回答中提到了一个点,大概意思是:

使用isReachable()时,如果应用的权限不足,可能会导致isReachable()检测连通性失败,始终返回false

确实,我们的应用程序通常是不会以root等超级用户身份执行的,上图测试失败时我们就是以普通用户longqinx执行的,那换成root试试呢?

果然!当我们切换到root用户时,isReachable()开始正常工作了;再次尝试用普通用户longqinx执行时,发现isReachable()又失败了...

注:上述示例中, sudo -u username command 表示以username用户身份执行command命令

问题原因

在网络上查了一圈儿资料之后,多数人都提到了权限问题和防火墙问题,但却几乎无人再去深入研究这个问题的根本原因所在。我心中的两个问题始终没有得到解答:

  1. isReachable()到底是干了啥才需要高级权限?
  2. 又是哪里和防火墙扯上了关系?

源代码才是最终的答案!于是我开始跟踪InetAddress.isReachable()的调用链,发现其最终调用了一个名为java.net.Inet4AddressImpl#isReachable0的Native方法

继续跟进源码,最终在OpenJDK的源码中找到了这个isReachable0的实现逻辑:

源码位置:github.com/openjdk/jdk...

c 复制代码
/*
 * Class:     java_net_Inet4AddressImpl
 * Method:    isReachable0
 * Signature: ([bI[bI)Z
 */
JNIEXPORT jboolean JNICALL
Java_java_net_Inet4AddressImpl_isReachable0(JNIEnv *env, jobject this,
                                            jbyteArray addrArray, jint timeout,
                                            jbyteArray ifArray, jint ttl)
{
    //.....省略......

    // Let's try to create a RAW socket to send ICMP packets.
    // This usually requires "root" privileges, so it's likely to fail.
    fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if (fd == -1) {
        return tcp_ping4(env, &sa, netif, timeout, ttl);
    } else {
        // It didn't fail, so we can use ICMP_ECHO requests.
        return ping4(env, fd, &sa, netif, timeout, ttl);
    }
}

/**
 * ping implementation using tcp port 7 (echo)
 */
static jboolean
tcp_ping4(JNIEnv *env, SOCKETADDRESS *sa, SOCKETADDRESS *netif, jint timeout, jint ttl);

/**
 * ping implementation.
 * Send an ICMP_ECHO_REQUEST packet every second until either the timeout
 * expires or an answer is received.
 * Returns true if an ECHO_REPLY is received, false otherwise.
 */
static jboolean
ping4(JNIEnv *env, jint fd, SOCKETADDRESS *sa, SOCKETADDRESS *netif, jint timeout, jint ttl)

看到上述代码就十分明了了,注释也十分清晰。isReachable0这个Native方法会先尝试创建一个发送ICMP包的SOCK_RAW类型的socket,而创建这种socket需要用户有足够的权限,比如root权限,否则创建socket就会失败(返回-1)

若用户有足够的权限,则socket创建成功,此时通过ping4()函数来进行连通性测试,有兴趣的读者可以看这个函数的实现,其本质上就是socket编程发送ICMP包来检测,原理和ping命令类似

而当用户权限不足创建socket失败后,源码中开始走另一条路,即调用tcp_ping4()函数来进行连通性测试。此函数本质上是建立一个socket连接到目标主机上的echo服务 ,若echo服务有回应则认为是连通的。这里建立的socket是SOCK_STREAM类型,即普通的面向连接的socket,普通用户就有权限创建

echo服务是一种特殊服务,工作在端口 7 上,它只是简单地将收到的东西返回给发送者,通过这个特性就能判断目的主机网络是否连通。echo协议可参考RFC 862 - Echo Protocol (ietf.org)

说到这里,我们再回头看一开始的测试代码。既然在程序权限不足的情况下底层实现会通过echo协议来判断连通性,那为什么这个测试代码还是失败了呢?有经验的同学可能已经猜到了,正是防火墙在作祟

从上图可以看出,我们使用普通用户权限也能正常进行连通性测试了,但前提是目标主机开放了echo服务所在的7端口

总结

通过上文的测试和分析,可以得出下述结论(Linux平台下):

  1. Java中InetAddress.isReachable()方法底层有两种机制来判断连通性,一种是使用ICMP报文,在程序有足够权限时使用;另一种则是使用echo协议,在程序权限不足以建立原生socket发送ICMP报文时使用。
  2. 当程序权限不足时会使用echo协议来进行连通性测试,此时需要目标主机端口 7 为开放状态才有意义。端口 7 未开放时也会测试失败

基于上述结论,这里给出使用InetAddress.isReachable()时的一些注意事项:

  1. 判断程序上线时是否有足够的权限,若是以普通用户权限运行则不建议使用InetAddress.isReachable()进行连通性测试
  2. 若权限不足但又非要使用InetAddress.isReachable()来进行连通性测试,则需要考虑被测试的目标主机是否会正常开放端口 7。若能保证被测的目标主机端口 7 都是开放的,那也可以使用此方法
  3. 默认情况下,笔者测试的CentOS7和Ubuntu 22.04系统中端口 7 都是默认关闭状态,使用时需要谨慎

----未经授权,请勿转载----

相关推荐
码农101号7 分钟前
Linux中容器文件操作和数据卷使用以及目录挂载
linux·运维·服务器
我是一只代码狗16 分钟前
springboot中使用线程池
java·spring boot·后端
PanZonghui25 分钟前
Centos项目部署之Nginx 的安装与卸载
linux·nginx
hello早上好29 分钟前
JDK 代理原理
java·spring boot·spring
PanZonghui31 分钟前
Centos项目部署之安装数据库MySQL8
linux·后端·mysql
PanZonghui33 分钟前
Centos项目部署之运行SpringBoot打包后的jar文件
linux·spring boot
PanZonghui33 分钟前
Centos项目部署之Java安装与配置
java·linux
程序员弘羽1 小时前
Linux进程管理:从基础到实战
linux·运维·服务器
PanZonghui1 小时前
Centos项目部署之常用操作命令
linux
JeffersonZU1 小时前
Linux/Unix进程概念及基本操作(PID、内存布局、虚拟内存、环境变量、fork、exit、wait、exec、system)
linux·c语言·unix·gnu