【问题标题】:Can't connect to IPv6-only host from java无法从 java 连接到仅限 IPv6 的主机
【发布时间】:2015-05-20 03:52:11
【问题描述】:

我有一些仅支持 IPv6 的主机。我可以成功地对其执行 curl 请求 通过卷曲

$ curl -I my.ip.v6.only.host
HTTP/1.1 200 OK

但是当我试图从 java 中获取它时,我遇到了一个错误:

HttpGet httpget = new HttpGet("http://my.ip.v6.only.host");
CloseableHttpResponse response = httpclient.execute(httpget);    

堆栈跟踪:

INFO: I/O exception (java.net.NoRouteToHostException) caught when processing request to {}->http://my.ip.v6.only.host: No route to host
Mar 17, 2015 7:42:23 PM org.apache.http.impl.execchain.RetryExec execute
INFO: Retrying request to {}->http://my.ip.v6.only.host
java.net.NoRouteToHostException: No route to host
    at java.net.PlainSocketImpl.socketConnect(Native Method)
    at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:339)
    at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:200)
    at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:182)
    at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
    at java.net.Socket.connect(Socket.java:579)
    at org.apache.http.conn.socket.PlainConnectionSocketFactory.connectSocket(PlainConnectionSocketFactory.java:72)
    at org.apache.http.impl.conn.HttpClientConnectionOperator.connect(HttpClientConnectionOperator.java:123)
    at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:318)
    at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:363)
    at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:219)
    at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:195)
    at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:86)
    at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:108)
    at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:184)
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82)
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:106)
    at MainTest.main(MainTest.java:25)

java v1.7.0_65 和 v1.8.0_40,MacOS 10.10.2 出现问题。在以前版本的 MacOS 10.9.5 上运行良好。

发生了什么事?主机如何可能被curl 访问而无法从java 访问。

另外,我尝试过使用-Djava.net.preferIPv6Addresses=true-Djava.net.preferIPv4Stack=false,但没有用。

UPD 在 OpenJDK 中发现了一个相关的错误,JDK-8015415

UPD 2 当我尝试使用有线连接而不是 wifi 时,它帮助了我。很奇怪。

【问题讨论】:

  • 您使用的是什么版本的 Apache Http Client?
  • org.apache.httpcomponents:httpclient:4.3.5
  • 尝试过正常的 Java NIO 请求了吗?我似乎无法在 Linux box atm 上重现此问题。
  • @MichaelHampton,当然是在线托管,因为它可以通过 curl 获得。

标签: java httpclient osx-yosemite ipv6


【解决方案1】:

AirDrop + Java 配合可能会出现问题。

简答 - 尝试:

$ sudo ifconfig awdl0 down

调查以下问题(感谢 Sergey Shinderuk):

我们在java中有这样的代码可以重现:

import java.net.Socket;

public class Test {
    public static void main(String[] args) throws Exception {
        new Socket("2a02:6b8::3", 80); // ya.ru
    }
}

当我们使用 WiFi 时,得到异常:java.net.NoRouteToHostException: No route to host

虽然使用 telnet 一切正常:

$ telnet 2a02:6b8::3 80
Trying 2a02:6b8::3...
Connected to www.yandex.ru.
Escape character is '^]'.
^C

当我们关闭 wifi 并使用有线连接时 - 一切正常。但是,如果我们使用有线连接,但打开了 wifi - 这个 java 代码将无法工作。这很奇怪。

我们需要在 java 和 telnet 之间比较 connect(2) 的参数。

$ sudo dtrace -qn 'syscall::connect:entry { print(*(struct sockaddr_in6 *)copyin(arg1, arg2)) }' -c './telnet 2a02:6b8::3 80'

struct sockaddr_in6 {
    __uint8_t sin6_len = 0x1c
    sa_family_t sin6_family = 0x1e
    in_port_t sin6_port = 0x5000
    __uint32_t sin6_flowinfo = 0
    struct in6_addr sin6_addr = {
        union __u6_addr = {
            __uint8_t [16] __u6_addr8 = [ 0x2a, 0x2, 0x6, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x3 ]
            __uint16_t [8] __u6_addr16 = [ 0x22a, 0xb806, 0, 0, 0, 0, 0, 0x300 ]
            __uint32_t [4] __u6_addr32 = [ 0xb806022a, 0, 0, 0x3000000 ]
        }
    }
    __uint32_t sin6_scope_id = 0
}

您可以看到我们将connect(2) 的第二个参数打印为struct sockaddr_in6。您还可以看到所有预期信息:AF_INET6、端口 80 和 ipv6-address。

请注意:我们推出了./telnet,而不是telnet - dtrace 不能 使用 Apple 签名的系统二进制文件。所以我们应该复制它。

java 也一样:

$ sudo dtrace -qn 'syscall::connect:entry { print(*(struct sockaddr_in6 *)copyin(arg1, arg2)) }' -c '/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/bin/java Test'
[...]
struct sockaddr_in6 {
    __uint8_t sin6_len = 0
    sa_family_t sin6_family = 0x1e
    in_port_t sin6_port = 0x5000
    __uint32_t sin6_flowinfo = 0
    struct in6_addr sin6_addr = {
        union __u6_addr = {
            __uint8_t [16] __u6_addr8 = [ 0x2a, 0x2, 0x6, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x3 ]
            __uint16_t [8] __u6_addr16 = [ 0x22a, 0xb806, 0, 0, 0, 0, 0, 0x300 ]
            __uint32_t [4] __u6_addr32 = [ 0xb806022a, 0, 0, 0x3000000 ]
        }
    }
    __uint32_t sin6_scope_id = 0x8
}

正如我们所见,主要区别在于 telnet 发送 sin6_len == 0 而 java - sin6_scope_id = 0x8。主要问题正是sin6_scope_id。 telnet 和 curl 发送 scope_id == 0,但 java - 0x8。而当我们使用有线连接时,java会发送scope_id == 0xb

为了清楚起见,我们尝试使用 telnet 重现 scope_id 的问题。 使用 WiFi 做:

$ telnet 2a02:6b8::3%0 80
Trying 2a02:6b8::3...
Connected to www.yandex.ru.

$ telnet 2a02:6b8::3%8 80
Trying 2a02:6b8::3...
telnet: connect to address 2a02:6b8::3: No route to host
telnet: Unable to connect to remote host

$ telnet 2a02:6b8::3%b 80
Trying 2a02:6b8::3...
Connected to www.yandex.ru.

所以 telnet 可以连接到0xb,但不能连接到0x8

似乎这段代码对于java的正确位置是: http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/8fe85977d5a6/src/solaris/native/java/net/net_util_md.c#l105

我们已经看到scope_id 填充了私有字段 java.net.NetworkInterface.defaultIndex 的值,其中包含一些默认接口的索引。

我们可以用代码打印所有索引:

import java.lang.reflect.Field;
import java.net.NetworkInterface;
import java.util.Collections;
import java.util.List;

public class Test {
    public static void main(String[] args) throws Exception {
        List<NetworkInterface> netins = Collections.list(NetworkInterface.getNetworkInterfaces());
        for (NetworkInterface netin : netins) {
            System.out.println(netin + " " + netin.getIndex());
        }

        Field f = NetworkInterface.class.getDeclaredField("defaultIndex");
        f.setAccessible(true);
        System.out.println("defaultIndex = " + f.get(NetworkInterface.class));
    }
}

在无线网络上:

$ java Netif
name:awdl0 (awdl0) 8
name:en0 (en0) 4
name:lo0 (lo0) 1
defaultIndex = 8

有线

$ java Netif
name:en4 (en4) 11
name:lo0 (lo0) 1
defaultIndex = 11

有线+wifi

$ java Netif
name:awdl0 (awdl0) 8
name:en4 (en4) 11
name:en0 (en0) 4
name:lo0 (lo0) 1
defaultIndex = 8

wifi连接时,defaultIndex == 8,默认接口是awdl0。

所以我们只是

$ sudo ifconfig awdl0 down

Java 代码可以工作。

还有:

【讨论】:

  • 很棒的答案,谢谢!不幸的是,在某些情况下,降低 awdl0 是不够的。在我的 MacBook 上,我的 defaultIndex 指向 en5,完整列表为:en5、utun2、utun1、utun0、awdl0、en0、lo0;其中 en0 是必须使用的。相当痛苦。有趣的是,Java 已经知道如何正确地做到这一点,如果您使用SocketChannel.open(inet6SockAddress).socket(),您将获得一个工作套接字。当然,有 的 Java 代码使用标准的 Socket API,所以这对于 Mac 上的 IPv6 来说似乎是个大问题。
【解决方案2】:

这个补丁的作者是https://github.com/snaury

解释:

需要用otool打开libnet.dylib,找到_setDefaultScopeID符号:

otool -tv -p _setDefaultScopeID libnet.dylib

在这里你可以找到与 0 和条件跳转的比较:

000000000000b882    cmpb    $0x1e, 0x1(%r14)
000000000000b887    jne 0xb8aa
000000000000b889    cmpl    $0x0, 0x18(%r14)
000000000000b88e    jne 0xb8aa

您需要用任何十六进制编辑器将条件跳转替换为无条件跳转:

000000000000b882    cmpb    $0x1e, 0x1(%r14)
000000000000b887    jne 0xb8aa
000000000000b889    cmpl    $0x0, 0x18(%r14)
000000000000b88e    jmp 0xb8aa

JNE == 75 1a
JMP == eb 1a

或者使用这一行命令:

otool -tv -p _setDefaultScopeID libnet.dylib | awk '/cmpl.*\$0x0/ {print $1}' | python -c 'exec """\nwith open("libnet.dylib", "r+b") as fd:\n    fd.seek(int(raw_input(), 16) + 5)\n    fd.write(chr(235))\n"""'

【讨论】:

  • 非常感谢单行!应该提到 libnet.dylib 应该在类似于/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/libnet.dylib的jre路径中
【解决方案3】:

有线连接对我也有帮助。

$ java -version java version "1.8.0_25" Java(TM) SE Runtime Environment (build 1.8.0_25-b17) Java HotSpot(TM) 64-Bit Server VM (build 25.25-b02, mixed mode)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-20
    • 2014-01-25
    • 2011-03-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多