您的位置:首页 > 理论基础 > 计算机网络

网络穿透与音视频技术(5)——NAT映射检测和常见网络穿越方法论(NAT检测实践2)

2018-10-15 10:48 633 查看
版权声明:欢迎转载,但是看在我辛勤劳动的份上,请注明来源:http://blog.csdn.net/yinwenjie(未经允许严禁用于商业用途!) https://blog.csdn.net/yinwenjie/article/details/82943799

(接上文《网络穿透与音视频技术(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检测程序和常见的网络穿透方法论)

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: