网络穿透与音视频技术(5)——NAT映射检测和常见网络穿越方法论(NAT检测实践2)
(接上文《网络穿透与音视频技术(4)——NAT映射检测和常见网络穿越方法论(NAT检测实践1)》)
2.3、检测过程实战——客户端
2.2.3、主要代码——IP获取工具类
这里注意一个问题:很多情况我们的客户端会有多个IP,但实际上我们只需要基于一个IP进行NAT检测。这里笔者给出一个工具类,可以为开发人员从客户端的多个IP下,返回一个满足条件的可用的IP。
package testCoordinate.utils; import java.io.IOException; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.util.ArrayList; import java.util.Comparator; import java.util.Enumeration; import java.util.List; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 本进程JVM的内网IP地址<br> * 工具内,不允许进行继承和实例化操作 * @author yinwenjie */ public final class NetIpUtils { /** * 日志 */ private final static Logger LOGGER = LoggerFactory.getLogger(NetIpUtils.class); private NetIpUtils() {} /** * 获取当前操作系统的一个非环路IP * @return */ public static String getNLoopAddress() { List<InetAddress> localAddresses = getLocalAddress(); if(localAddresses == null || localAddresses.isEmpty()) { return null; } return localAddresses.get(0).getHostAddress(); } /** * 获取当前操作系统的一个内网可用IP,也就是说网段为一下范围的IP,C类网段优先<br> * A类 10.0.0.0-10.255.255.255 <br> * B类 172.16.0.0-172.31.255.255 <br> * C类 192.168.0.0-192.168.255.255 <br> * @return 如果没有任何内网IP,则返回空 */ public static String getIntranetAddress() { String intranetAddress = null; List<String> ips = new ArrayList<>(); List<InetAddress> localAddresses = getLocalAddress(); if(localAddresses == null || localAddresses.isEmpty()) { return null; } localAddresses.stream().forEach(item -> ips.add(item.getHostAddress())); // 开始进行选择 if(ips.isEmpty()) return null; // C类地址优先 ips.sort(new Comparator<String>() { @Override public int compare(String o1, String o2) { return o2.compareTo(o1); } }); for (String ip : ips) { if(isInnerIP(ip)) return ip; } return intranetAddress; } /** * 获取当前操作系统的多个可用的内网IP(),也就是说网段为一下范围的IP<br> * A类 10.0.0.0-10.255.255.255 <br> * B类 172.16.0.0-172.31.255.255 <br> * C类 192.168.0.0-192.168.255.255 <br> * @return 如果没有任何内网IP,则返回空 */ public static String[] getIntranetAddresses() { List<InetAddr 20000 ess> localAddresses = getLocalAddress(); if(localAddresses == null || localAddresses.isEmpty()) { return null; } for (int index = 0 ; index < localAddresses.size() ; index++) { InetAddress ip = localAddresses.get(index); if(isInnerIP(ip.getHostAddress())) { localAddresses.remove(index); index--; } } List<String> ips = new ArrayList<>(); localAddresses.stream().forEach(item -> ips.add(item.getHostAddress())); return ips.toArray(new String[]{}); } /** * 该工具方法用于给调用者一个本地内网地址,并且这个内网地址是至少可以连接targetAddresses中的某一个目标源地址的 * @param targetAddresses * @return 如果没有满足要求的内网地址,则返回false */ @SuppressWarnings("rawtypes") public static String getIntranetAddress(String[] targetAddresses) { if(targetAddresses == null || targetAddresses.length == 0) { return null; } try { for (Enumeration ifaces = NetworkInterface.getNetworkInterfaces(); ifaces.hasMoreElements();) { NetworkInterface iface = (NetworkInterface) ifaces.nextElement(); if(iface.isVirtual()) { continue; } boolean needCheck = false; InetAddress inetAddr = null; CHECK:for (Enumeration inetAddrs = iface.getInetAddresses(); inetAddrs.hasMoreElements();) { inetAddr = (InetAddress) inetAddrs.nextElement(); // 排除loopback类型地址 if (!inetAddr.isLoopbackAddress() && inetAddr.isSiteLocalAddress()) { needCheck = true; break CHECK; } } // 如果条件成立,则不需要进行连通性测试 if(!needCheck || inetAddr == null) { continue; } for (String targetAddress : targetAddresses) { if(InetAddress.getByName(targetAddress).isReachable(iface, 0, 1000)) { return inetAddr.getHostAddress(); } } } } catch (IOException e) { LOGGER.error(e.getMessage() , e); return null; } return null; } /** * @return */ @SuppressWarnings("rawtypes") private static List<InetAddress> getLocalAddress() { List<InetAddress> localAddresses = new ArrayList<>(); try { for (Enumeration ifaces = NetworkInterface.getNetworkInterfaces(); ifaces.hasMoreElements();) { NetworkInterface iface = (NetworkInterface) ifaces.nextElement(); if(iface.isVirtual()) { continue; } for (Enumeration inetAddrs = iface.getInetAddresses(); inetAddrs.hasMoreElements();) { InetAddress inetAddr = (InetAddress) inetAddrs.nextElement(); // 排除loopback类型地址 if (!inetAddr.isLoopbackAddress() && inetAddr.isSiteLocalAddress()) { localAddresses.add(inetAddr); } } } } catch (SocketException e) { LOGGER.error(e.getMessage() , e); return null; } return localAddresses; } /** * 判定一个给定的IP是否为保留的内网IP段 * @param ipAddress * @return */ public static boolean isInnerIP(String ipAddress) { // 如果是一个不符合规范的IP地址,就不用判断了(来个简单的) String patternRegx = "\\d{1,3}(\\.\\d{1,3}){3}"; if(ipAddress == null) return false; if(!Pattern.matches(patternRegx, ipAddress)) return false; /** * 私有IP: * A类 10.0.0.0-10.255.255.255 * B类 172.16.0.0-172.31.255.255 * C类 192.168.0.0-192.168.255.255 */ boolean isInnerIp = false; long ipNum = getIpNum(ipAddress); long aBegin = getIpNum("10.0.0.0"); long aEnd = getIpNum("10.255.255.255"); long bBegin = getIpNum("172.16.0.0"); long bEnd = getIpNum("172.31.255.255"); long cBegin = getIpNum("192.168.0.0"); long cEnd = getIpNum("192.168.255.255"); isInnerIp = isInner(ipNum, aBegin, aEnd) || isInner(ipNum, bBegin, bEnd) || isInner(ipNum, cBegin, cEnd); return isInnerIp; } private static long getIpNum(String ipAddress) { String[] ip = ipAddress.split("\\."); int a = (Integer.parseInt(ip[0]) << 24) & 0xFFFFFFFF; int b = (Integer.parseInt(ip[1]) << 16) & 0xFFFFFFFF; int c = (Integer.parseInt(ip[2]) << 8) & 0xFFFFFFFF; int d = Integer.parseInt(ip[3]) & 0xFFFFFFFF; return a + b + c + d; } private static boolean isInner(long userIp, long begin, long end) { return (userIp >= begin) && (userIp <= end); } }
以上工具不但可以用在NAT检测的客户端准备过程,实际上还可以用在很多技术场景下(例如判定指定的IP信息是否是一个规范的内网地址),读者可以根据情况直接进行使用。
2.2.4、主要检测思路
这之前的文章中已经进行了介绍,NAT映射实现方式的检测顺序为,首先检查客户端和服务端的网络连接之间是否至少存在一级NAT设备,如果答案是肯定的那么进行Symmetric NAT检测,如果不是Symmetric NAT那么接着进行Full Cone NAT检测,如果不是Full Cone NAT则最后进行Address Restricted Cone NAT/Port Restricted Cone NAT 检测。在整个检测过程中,服务器端只是起到一个辅助作用,主要的判断逻辑还是在客户端进行。基于这样的检测原理,检测客户端程序的设计思路如下图所示:
如说图所示,监测客户端将请求发送和检测信息接收分别可做成两个线程,主控制线程负责控制整个监测顺序和检测节奏——通过阻塞队列向检测请求发送线程推送信息,并且在当前阶段的检测结果还没有得到响应(或者等待超时)之前进行阻塞等待。主控制线程还在每个阶段接收到响应结果后,进行NAT类型的确认。
2.2.5、主要检测代码
- 以下代码是请求发送线程:
/** * 检测请求发送线程 * @author yinwenjie */ private static class CheckRequestTask implements Runnable { private BlockingQueue<JSONObject> messageQueue; private String serverIp; private Integer serverPort; private DatagramChannel udpChannel; public CheckRequestTask(String serverIp ,Integer serverPort ,BlockingQueue<JSONObject> messageQueue , DatagramChannel udpChannel) { this.serverIp = serverIp; this.serverPort = serverPort; this.messageQueue = messageQueue; this.udpChannel = udpChannel; } @Override public void run() { while(true) { try { doHandle(); } catch(Exception e) { LOGGER.error(e.getMessage() , e); } } } /** * 进行发送 * @throws IOException */ private void doHandle() throws IOException { JSONObject jsonObject; try { jsonObject = messageQueue.take(); } catch (InterruptedException e) { LOGGER.error(e.getMessage() , e); return; } // 准备发送,根据不同的type,使用不同的channel进行发送 String jsonContext = jsonObject.toJSONString(); byte[] jsonBytes = jsonContext.getBytes(); // 发送 LOGGER.info("客户端向检测服务[" + serverIp + ":" + serverPort + "]发送检测请求===:" + jsonContext); synchronized (CheckClient.class) { ByteBuffer conentBytes = ByteBuffer.allocateDirect(jsonBytes.length); try { udpChannel.connect(new InetSocketAddress(serverIp, serverPort)); conentBytes.put(jsonBytes); conentBytes.flip(); udpChannel.write(conentBytes); } finally { conentBytes.clear(); udpChannel.disconnect(); } } } }
以上代码和第三方线程通信的机制就是messageQueue可阻塞队列。
- 以下代码是检测信息接收线程:
/** * 检测信息接收线程 * @author yinwenjie */ private static class CheckResponseTask implements Runnable { private Selector selector; private BlockingQueue<JSONObject> responseMessagesQueue; public CheckResponseTask(Selector selector ,BlockingQueue<JSONObject> responseMessagesQueue ) { this.selector = selector; this.responseMessagesQueue = responseMessagesQueue; } @Override public void run() { /* * 1、建立UDP Channel的接收接听 * 2、解析接收到的数据报中的内容 * 3、将接收到的信息发送到响应队列中 * */ while(true) { try { doHandle(); } catch(IOException e) { LOGGER.error(e.getMessage() , e); } } } private void doHandle() throws IOException { // 1、============= ByteBuffer bb = ByteBuffer.allocateDirect(2048); selector.select(); Iterator<SelectionKey> keys = selector.selectedKeys().iterator(); while(keys.hasNext()) { SelectionKey sk = keys.next(); keys.remove(); if(sk.isReadable()) { DatagramChannel curdc = (DatagramChannel) sk.channel(); try { curdc.receive(bb); } catch(Exception e) { LOGGER.warn(e.getMessage() , e); continue; } bb.flip(); byte[] peerbs = new byte[bb.limit()]; for(int i=0;i<bb.limit();i++){ peerbs[i]=bb.get(i); } // 2、============= String receStr = new String(peerbs); JSONObject requestObject = null; Integer type = null; try { requestObject = JSONObject.parseObject(receStr); if(requestObject == null) { continue; } type = requestObject.getInteger("type"); if(type == null) { continue; } } catch(Exception e) { LOGGER.error(e.getMessage() , e); } finally { bb.clear(); } // 3、=============== String targetIp = requestObject.getString("targetIp"); Integer targetPort = requestObject.getInteger("resoucePort"); LOGGER.info("=========接收到检测结果,来自于服务器[" + targetIp + ":" + targetPort + "] " + receStr); try { this.responseMessagesQueue.put(requestObject); } catch (InterruptedException e) { LOGGER.error(e.getMessage() , e); } } } } }
同样,检测信息接收线程也是通过可阻塞队列和其它第三方线程实现交互
- 以下代码是主控制线程:
/** * NAT映射实现方式检测程序——客户端 * @author yinwenjie */ public class CheckClient { private static Logger LOGGER = LoggerFactory.getLogger(CheckClient.class); public static void main(String[] args) throws Exception { /** * 当前的检查类型type: * 1、检测是否至少有一级NAT设备 * 2、Symmetric NAT检测 * 3、Full Cone NAT检测 * 4、Address Restricted Cone NAT/Port Restricted Cone NAT 检测 */ String serverIp1 = args[0]; String serverPort1Value = args[1]; Integer serverPort1 = Integer.parseInt(serverPort1Value); String serverIp2 = args[2]; String serverPort2Value = args[3]; Integer serverPort2 = Integer.parseInt(serverPort2Value); // 这是客户端的IP 和 端口信息。\\ 这里的目标IP可以进行调整 String clientIp = NetIpUtils.getIntranetAddress(new String[]{"61.139.2.69"}); String clientPortValue = args[4]; Integer clientPort = Integer.parseInt(clientPortValue); // 建立UDP连接和监听 Selector selector = Selector.open(); DatagramChannel udpChannel = DatagramChannel.open(); udpChannel.configureBlocking(false); udpChannel.socket().bind(new InetSocketAddress(clientIp , clientPort)); udpChannel.register(selector, SelectionKey.OP_READ); /* * 1、使用type = 1的标记,进行“是否有NAT设备的检测” * 2、使用type = 2的标记,进行Symmetric NAT检测 * 3、使用type = 3的标记,进行Full Cone NAT检测 * 4、使用type = 4的标记,进行Address Restricted Cone NAT/Port Restricted Cone NAT 检测 * */ // 专门给服务器 IP1 + PORT1发消息的线程 BlockingQueue<JSONObject> requestMessageQueue1 = new LinkedBlockingQueue<>(); LinkedBlockingQueue<JSONObject> responseMessageQueue = new LinkedBlockingQueue<>(); BlockingQueue<JSONObject> requestMessageQueue2 = new LinkedBlockingQueue<>(); CheckRequestTask checkRequestTask1 = new CheckRequestTask(serverIp1 , serverPort1 , requestMessageQueue1 , udpChannel); Thread checkRequestThread1 = new Thread(checkRequestTask1); checkRequestThread1.start(); // 专门给服务器 IP2 + PORT2发消息的线程 CheckRequestTask checkRequestTask2 = new CheckRequestTask(serverIp2 , serverPort2 , requestMessageQueue2 , udpChannel); Thread checkRequestThread2 = new Thread(checkRequestTask2); checkRequestThread2.start(); CheckResponseTask checkResonanceTask = new CheckResponseTask(selector, responseMessageQueue); Thread checkResonanceThread = new Thread(checkResonanceTask); checkResonanceThread.start(); // 1、以下是检查一============================ // 要求客户端发送type == 1的检查请求(3次) Integer currentType = 1; JSONObject currentResult = checkHandle(currentType, requestMessageQueue1, responseMessageQueue, checkResonanceThread); // 对结果进行判定—— Validate.notNull(currentResult , "网络超时或者本地处理异常,导致检测失败"); String resouceIp = currentResult.getString("resouceIp"); Integer resoucePort = currentResult.getInteger("resoucePort"); // 如果条件成立,说明两个节点间client 到 server没有任何NAT设备 if(StringUtils.equals(clientIp, resouceIp) && clientPort.intValue() == resoucePort.intValue()) { LOGGER.warn("client和指定的server之间没有任何NAT设备,检查过程终止!"); return; } // 2、以下是检查二============================ currentType = 2; JSONObject currentResult1 = checkHandle(currentType, requestMessageQueue1, responseMessageQueue, checkResonanceThread); Validate.notNull(currentResult1 , "网络超时或者本地处理异常,导致检测失败"); String resouceIp1 = currentResult1.getString("resouceIp"); Integer resoucePort1 = currentResult1.getInteger("resoucePort"); JSONObject currentResult2 = checkHandle(currentType, requestMessageQueue2, responseMessageQueue, checkResonanceThread); Validate.notNull(currentResult2 , "网络超时或者本地处理异常,导致检测失败"); String resouceIp2 = currentResult2.getString("resouceIp"); Integer resoucePort2 = currentResult2.getInteger("resoucePort"); // 如果条件成立,说明是Symmetric NAT if(!StringUtils.equals(resouceIp1, resouceIp2) || resoucePort1.intValue() != resoucePort2.intValue()) { LOGGER.info("检查到Symmetric NAT"); return; } // 3、以下是检查三============================ currentType = 3; currentResult = checkHandle(currentType, requestMessageQueue1, responseMessageQueue, checkResonanceThread); if(currentResult != null) { LOGGER.info("检查到Full Cone NAT"); return; } // 4、以下是检查四============================ currentType = 4; currentResult = checkHandle(currentType, requestMessageQueue1, responseMessageQueue, checkResonanceThread); if(currentResult == null) { LOGGER.info("检查到Port Restricted Cone NAT"); } else { LOGGER.info("检查到Address Restricted Cone NAT"); } } /** * 检测类型1、2、3、4通用的网络发包和收报过程 * @param currentType * @param requestMessageQueue * @param responseMessageQueue * @param checkResonanceThread * @return */ private static JSONObject checkHandle(Integer currentType ,BlockingQueue<JSONObject> requestMessageQueue , LinkedBlockingQueue<JSONObject> responseMessageQueue , Thread checkResonanceThread) { JSONObject currentResult = null; try { for(int index = 0 ; index < 3 ; index++) { JSONObject message = new JSONObject(); message.put("type", currentType); message.put("ack", false); requestMessageQueue.put(message); } // 等待和获取服务器响应信息 for(int index = 0 ; index < 3 ; index++) { // 不用等待队列中的消息,有就取,没有就不取 JSONObject responseMessage = responseMessageQueue.poll(); if(responseMessage != null) { Integer responseType = responseMessage.getInteger("type"); if(responseType.intValue() == currentType.intValue()) { currentResult = responseMessage; } else if(responseType.intValue() <= currentType.intValue()) { index--; continue; } } synchronized (checkResonanceThread) { checkResonanceThread.wait(1000); } } } catch(InterruptedException e) { LOGGER.error(e.getMessage() , e); } return currentResult; } // ......... // 发送线程和接受线程作为CheckClient的子类存在于这里 // ......... }
===============================================================
(后文将会以上放出的代码进行一些补充说明,然后介绍几种现成的NAT检测程序和常见的网络穿透方法论)
- 下一代网络中多媒体业务穿越NAT和防火墙技术的研究
- 80端口被屏蔽解决方法,80端口穿透之NAT端口映射技术
- 网络中检测共享(NAT)技术分析
- 详解常见漏洞扫描器及网络扫描技术(图)
- 20145321 《网络对抗技术》 Web安全基础实践
- 野外火灾烟雾视频检测技术研究及应用(数据集,代码及论文)
- 计算机网络技术教程-董文磊-专题视频课程
- [p2p]UDP用打洞技术穿透NAT的原理与实现(附源代码 转)
- 视频监控技术 迎来网络“多媒体”时代
- 网络---NAT技术与代理服务器调研
- 20145336 张子扬 《网络对抗技术》免杀原理与实践
- 观止云技术实践| 可追溯日志:视频云时代的新运维大胸器
- P2P网络中UDP穿越NAT的原理
- 网络分布视频技术与盈利性视频站点技术
- p2p网络中的NAT穿透技术----常见NAT穿越解决方案
- 20145203盖泽双 《网络对抗技术》实践八:Web基础
- 关于网络中如何检测共享(NAT)的原理
- 音视频网络传输技术在安防系统中的应用