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

版权声明:欢迎转载,但是看在我辛勤劳动的份上,请注明来源: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<InetAddress> 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检测程序和常见的网络穿透方法论)

猜你喜欢

转载自blog.csdn.net/yinwenjie/article/details/82943799