最近要给一个扫描器(Java 写的)添加代理检测的插件。

思路很简单,尝试使用待检测的 IP 和端口作为代理,访问 http://ipinfo.io/ip/ 之类的可以返回用户 IP 的网站。
如果返回的 IP 和自己本机不一样,就判断为代理。

不过也遇到了不少坑。

刚开始直接向端口发送 GET http://ipinfo.io/ip/ HTTP/1.1\r\n\r\n 检查代理,速度虽然很快,但这种方式部分 HTTP 代理不支持,而且显然对 Socks 代理不适用。

后来换成 HttpURLConnection,代码类似:

    String checkUrl = "http://ipinfo.io/ip/";
    Proxy proxy = new Proxy(Proxy.Type.HTTP, addr);
    URL url = new URL(checkUrl);
    uc = (HttpURLConnection)url.openConnection(proxy);
    uc.setConnectTimeout(3000);
    uc.connect();

然后跑了一会儿发现有卡住不动的情况,经小伙伴提醒加上了 uc.setReadTimeout(3000) 解决。
这里 setReadTimeout 的意思就是 "If the timeout expires before there is data available for read, a java.net.SocketTimeoutException is raised"。

搞定超时之后扫了一堆端口,扫出不少代理——然而大部分是用来翻墙的。
以百度为例,223.252.xxx.xxx:12345 是百度公司的 IP,但代理出口 IP 220.130.xxx.xxx 却属于台湾。
怎么判断是不是目标公司的内网代理呢?

如果有百度的外网 IP 段列表的话,直接查查代理出口 IP 在不在列表里就行,然而我们肯定不可能有完整的 IP 列表。去 https://www.ipip.net/ 查出口 IP 是不是属于百度的?很多都查不出来。

于是只能采取迂回的办法,如果出口 IP 和检查的 IP 是一样的,那肯定是内网代理,级别定为高危。如果不一样,那可能是用来翻墙的代理,调低危险级别。

最后上线扫描时又出了个问题,代理扫描器还是卡住了。已经设置超时了为啥也会卡住?

查看 log 怀疑是类似 Slowloris DOS 的情况。比如检查的端口返回了畸形的 HTTP Header,或是一直发送数据,但每次只发几个字节,维持 HTTP 连接,使 HttpURLConnection 无法关闭。

但这种情况可能性不大,查了下发现有人提到过在使用代理的情况下 setConnectTimeout 没用,可是至今没有人解答。

找出造成扫描器卡住的 IP 在本地测试却没问题,给其他小伙伴测试了下也一切正常,想到扫描器环境用的是 OpenJDK 1.6,最终只能猜测是 OpenJDK 的 bug。

纠结半天后决定把 HttpURLConnection 换成 HttpClient,HttpClient 想要支持 SOCKS 代理的话要重写一下 SOCKS 工厂类:

public static String connectSocksProxy(String checkHost, String proxyHost, int proxyPort, int timeout) throws Exception{
        String result;
        Registry<ConnectionSocketFactory> reg = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", new MyConnectionSocketFactory()).build();
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(reg);
        CloseableHttpClient httpclient = HttpClients.custom().setConnectionManager(cm).build();
        try {
            InetSocketAddress socksaddr = new InetSocketAddress(proxyHost, proxyPort);
            RequestConfig config = RequestConfig.custom().setSocketTimeout(timeout).setConnectTimeout(timeout)
                    .setConnectionRequestTimeout(timeout).build();
            HttpClientContext context = HttpClientContext.create();
            context.setAttribute("socks.address", socksaddr);
            context.setRequestConfig(config);

            HttpHost target = new HttpHost(checkHost, 80, "http");
            HttpGet request = new HttpGet("/ip/");

            logger.info("Executing request " + request + " to " + target + " via SOCKS proxy " + socksaddr);

            result = getResponse(httpclient, target, request, context);
        } finally {
            httpclient.close();
        }
        return result;
    }

    static class MyConnectionSocketFactory extends PlainConnectionSocketFactory {
        @Override
        public Socket createSocket(final HttpContext context) throws IOException {
            InetSocketAddress socksaddr = (InetSocketAddress) context.getAttribute("socks.address");
            Proxy proxy = new Proxy(Proxy.Type.SOCKS, socksaddr);
            return new Socket(proxy);
        }

        @Override
        public Socket connectSocket(int connectTimeout, Socket socket, HttpHost host, InetSocketAddress remoteAddress,
                                    InetSocketAddress localAddress, HttpContext context) throws IOException {
            // Convert address to unresolved
            InetSocketAddress unresolvedRemote = InetSocketAddress
                    .createUnresolved(host.getHostName(), remoteAddress.getPort());
            return super.connectSocket(connectTimeout, socket, host, unresolvedRemote, localAddress, context);
        }
    }

总结:人生苦短,快用 Python …