【问题标题】:Checking if an IP address is in multiple possible subnets检查 IP 地址是否在多个可能的子网中
【发布时间】:2016-12-24 03:02:52
【问题描述】:

我正在解析 Apache 日志并想检查一个 IP 地址是否属于大约 300 个可能的子网列表 (https://github.com/client9/ipcat)。

  • 互联网流量中有很多不同的 IP(我不知道它们属于哪个子网)。
  • 我正在使用 Apache Commons SubnetUtils 包来存储子网。
  • 将来自 300 个子网的所有可能 IP 存储在哈希图中会占用太多内存。
  • 对每个 IP 的每个 SubnetUtil 进行迭代(即使使用 HashMap 缓存用于之前的查找)非常慢。

还有什么我可以在这里做的吗?

【问题讨论】:

  • 你可以用子网构建一个树形结构——它可以一点一点地构建。这会将检查从 300 减少到最多 32(假设 IPv4),但在大多数情况下可能会少得多。
  • 您可以使用 Guava 的 RangeSet 将 IP 范围保存为长整数或整数(注意溢出)。

标签: java networking network-programming ip


【解决方案1】:

您可以使用子网构建树结构 - 它可以一点一点地构建。这会将检查从 300 减少到最多 32(假设 IPv4),但在大多数情况下要少得多。 (因为它要么在几位之后不匹配,要么在子网掩码的平均长度处匹配)

这是一个简单的二叉树实现。您可能想用一些函数来装饰它,以更常见的"a.b.c.d/e" 格式解析子网。

public class SubnetTree {
    private SubnetTree one, zero;
    private boolean terminating;

    public void addSubnet(int net, int bits) {
        if (terminating) {
            // If this node is already terminating, then no need to add
            // subnets that are more specific
            return;
        }
        if (bits > 0) {
            boolean bit = ((net >>> 31) & 1) == 1;
            if (bit) {
                if (one == null) {
                    one = new SubnetTree();
                }
                one.addSubnet(net << 1, bits - 1);
            } else {
                if (zero == null) {
                    zero = new SubnetTree();
                }
                zero.addSubnet(net << 1, bits - 1);
            }
        } else {
            terminating = true;
        }
    }

    public boolean isInRange(int address) {
        if (terminating) {
            return true;
        }
        boolean bit = ((address >>> 31) & 1) == 1;
        if (bit) {
            if (one == null) {
                return false;
            } else {
                return one.isInRange(address << 1);
            }
        } else {
            if (zero == null) {
                return false;
            } else {
                return zero.isInRange(address << 1);
            }
        }
    }
}

对此代码的一个非常简单的测试:

public static void main(String[] args) {
    SubnetTree tree = new SubnetTree();
    tree.add(Integer.parseUnsignedInt("01100110000000000000000000000000", 2), 8);
    System.out.println("true: " + tree.isInRange(Integer.parseUnsignedInt("01100110000000000000100010000101", 2)));
    System.out.println("false: " + tree.isInRange(Integer.parseUnsignedInt("01101110000000000000100010000101", 2)));

    tree.add(Integer.parseUnsignedInt("01001110000000000000000000000000", 2), 6);
    System.out.println("true: " + tree.isInRange(Integer.parseUnsignedInt("01100110000000000000100010000101", 2)));
    System.out.println("false: " + tree.isInRange(Integer.parseUnsignedInt("01101110000000000000100010000101", 2)));
    System.out.println("true: " + tree.isInRange(Integer.parseUnsignedInt("01001110100000000000000000000000", 2)));
    System.out.println("true: " + tree.isInRange(Integer.parseUnsignedInt("01001100100000000000000000111111", 2)));
}

【讨论】:

  • 谢谢你!会玩它
【解决方案2】:

您基本上想为所有子网构建一个 Trie(前缀树)。在其中的搜索将非常快,最坏的情况是最长子网前缀的长度(最终为 32)。在内存方面它也非常有效。

以二进制表示形式考虑您的子网,因此树将存储它。然后,IP 地址也应转换为二进制,并将树用于搜索。到达叶子节点时,检查它是否是有效的子网。

这是一个相当完整的实现/示例:

import java.util.Arrays;

public class CheckIpAdrBelongsToSubnet {
  private static final String [] CIDR_SUBNETS = {"192.5.0.0/16", "10.10.0.0/16"};

  public static void main(String ... ips) {
    BinaryTrieNode root = createBinaryTrieFromCidrs(CIDR_SUBNETS);

    for(String ip :ips) {
      System.out.println(ip + " belongs to a subnet in list: " + ipBelongsToCidrs(ip, root));
    }
  }

  static boolean ipBelongsToCidrs(String ipv4, BinaryTrieNode root) {
    BinaryIpV4 bipv4 = new BinaryIpV4(ipv4);
    BinaryTrieNode current = root;
    for(boolean b : bipv4.getBinIp()) {
      BinaryTrieNode nextCurr = current.getNode(b);
      if(nextCurr == null) {
        return current.isEndOfValidPrefix();
      } else {
        current = nextCurr;
      }
    }
    return false;
  }

  static BinaryTrieNode createBinaryTrieFromCidrs(String [] cidrs) {
    BinaryTrieNode root = new BinaryTrieNode();

    for (String cidr : cidrs) {
      String ipv4 = cidr.split("/")[0];
      int prefixLength = Integer.parseInt(cidr.split("/")[1]);
      BinaryIpV4 bipv4 = new BinaryIpV4(ipv4, prefixLength);

      BinaryTrieNode current = root;
      for(boolean b : bipv4.getBinIp()) {
        BinaryTrieNode nextCurr = current.getNode(b);
        if(nextCurr == null) {
          nextCurr = new BinaryTrieNode();
          current.setNode(b, nextCurr);
        }
        current = nextCurr;
      }
      current.setEndOfValidPrefix(true);
    }

    return root;
  }

  public static class BinaryIpV4 {
    boolean [] binIp;

    BinaryIpV4(String ipv4) {
      translateCidrToBinPrefix(ipv4,32);
    }

    BinaryIpV4(String ipv4, int prefixLength) {
      translateCidrToBinPrefix(ipv4,prefixLength);
    }

    void translateCidrToBinPrefix(String ipv4, int prefixLength) {
      String [] bytes = ipv4.split("\\.");
      binIp = new boolean [prefixLength];
      int idx = 0;

      for(String b : bytes) {
        int by = Integer.parseUnsignedInt(b);
        String binByte = getBinaryString(by);

        for(int i = 0; i < binByte.length() ; i++) {
          binIp[idx++] = binByte.charAt(i)=='1';

          if(idx >= prefixLength) {
            return;
          }
        }
      }
    }

    public boolean[] getBinIp() {
      return binIp;
    }

    String getBinaryString(int b) {
      return String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0');
    }

    public String toString() {
      return Arrays.toString(binIp);
    }
  }

  public static class BinaryTrieNode {
    private boolean isEndOfValidPrefix=false;

    private BinaryTrieNode zeroNode;
    private BinaryTrieNode oneNode;

    public boolean isEndOfValidPrefix() {
      return isEndOfValidPrefix;
    }
    public void setEndOfValidPrefix(boolean isEndOfValidPrefix) {
      this.isEndOfValidPrefix = isEndOfValidPrefix;
    }
    public BinaryTrieNode getNode(boolean b) {
      return b?oneNode:zeroNode;
    }
    public void setNode(boolean b, BinaryTrieNode node) {
      if(b) {
        this.oneNode = node;
      } else {
        this.zeroNode = node;
      }

    }
  }
}

【讨论】:

    【解决方案3】:

    这是我要做的:

    • 提前设置任何子网不得与任何其他子网重叠的策略。
    • 按起始 IP(一次)升序对子网进行排序。
    • 对于每个 IP,对数组执行二进制搜索,返回最后一个小于或等于该 IP 的 IP。
    • 检查那个子网。

    在 IP 字符串初始转换为整数之后,这是 O(log2(n)) 次简单的数值比较,其中 n 为 300(大约为 9)加上一个按位与和一个额外的数值比较。

    要将字符串转换为 C 中的整数,您可以这样做:

    struct in_addr addr;
    int ip = inet_aton(stringAddress, &addr);
    

    对于每个 IP(子网和您正在检查的 IP)。而对于面具:

    uint32_t mask_val = ~((1 << (32 - nbits)) - 1);
    // e.g. a mask of 30 gives you ~[[1 << 2 == 0b100] - 1 = 0b11] = 0b111...100
    

    我认为大多数其他语言都有类似的功能。

    然后你会将它们与:

    if (ip & mask == subnet) { // match }
    

    【讨论】:

    • 已经有两个较早的答案描述了时间复杂度 O(1) 和 Java 中的解决方案。
    • 不,它们不是 O(1)。它们是 O(k),其中 k 是位数。这最终比我描述的要慢得多。
    • 不,它们是 O(1) - O(32) 等于 O(1) - 我们只在它们没有固定上限时才调用 O(n)。
    • 你错过了我的观点,即常数太大了,即使我们假设你永远不会看到小于 /28 的子网,看起来更复杂的 O(log2(n )) 算法可能需要更少的时间,直到您有几亿个子网进行比较。
    猜你喜欢
    • 2012-01-25
    • 2017-09-02
    • 2021-06-20
    • 2010-12-02
    • 2017-05-05
    • 2013-08-22
    • 1970-01-01
    • 2012-04-17
    • 2017-10-30
    相关资源
    最近更新 更多