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

  • 相关阅读:
    IDEA激活
    Spring JDBC
    数据库连接池
    JDBC
    10个很实用Linux命令,千万不要错过
    Linux 下如何使用 alias 命令
    Linux 下如何使用 fc 命令
    Linux 下如何修改密码有效期?
    利用 tee 命令调试shell脚本中的管道
    ps 命令显示不完整的问题
  • 原文地址:https://www.cnblogs.com/liulaolaiu/p/11744226.html
Copyright © 2020-2023  润新知