【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 都是默认关闭状态,使用时需要谨慎

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

相关推荐
qq_433618441 小时前
shell 编程(五)
linux·运维·服务器
是小崔啊1 小时前
开源轮子 - EasyExcel02(深入实践)
java·开源·excel
myNameGL2 小时前
linux安装idea
java·ide·intellij-idea
青春男大2 小时前
java栈--数据结构
java·开发语言·数据结构·学习·eclipse
HaiFan.2 小时前
SpringBoot 事务
java·数据库·spring boot·sql·mysql
我要学编程(ಥ_ಥ)3 小时前
一文详解“二叉树中的深搜“在算法中的应用
java·数据结构·算法·leetcode·深度优先
music0ant3 小时前
Idea 添加tomcat 并发布到tomcat
java·tomcat·intellij-idea
计算机徐师兄3 小时前
Java基于SSM框架的无中介租房系统小程序【附源码、文档】
java·微信小程序·小程序·无中介租房系统小程序·java无中介租房系统小程序·无中介租房微信小程序
源码哥_博纳软云3 小时前
JAVA智慧养老养老护理帮忙代办陪诊陪护小程序APP源码
java·开发语言·微信小程序·小程序·微信公众平台
广而不精zhu小白4 小时前
CentOS Stream 9 挂载Windows共享FTP文件夹
linux·windows·centos