最近要给一个扫描器(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 …