在Python中检查IP是否在CIDR范围内

12

我知道这里有一些类似的问题,但它们大多要么想找到范围本身(这需要使用一些库,比如stackoverflow说是重复我的问题的示例)并且是用另一种语言编写的。

我有一种方法可以将子网转换为子网中IP范围的开始和结束(好吧,措辞不太好,它就像1.1.1.1/16 -> (1.1.0.0 , 1.1.255.255)

现在我想检查1.1.2.2是否在这个子网中。我能否简单地使用><进行比较?

ip_range = ('1.1.0.0', '1.1.255.255')
if '1.1.2.2' >= ip_range[0] and '1.1.2.2' <= ip_range[1]:
     return True

我测试过它,它能工作,但我不知道它是否对任何IPv4地址都有效。 我认为我只是在比较ASCII顺序,所以这应该始终有效,但是否有任何例外情况呢?


1
可能是如何在Python中检查IP是否在网络中的重复问题,并且答案在https://dev59.com/0mkw5IYBdhLWcg3wrsnn中。 - TessellatingHeckler
7个回答

40

在Python 3.3及更高版本中,您应该使用ipaddress模块

from ipaddress import ip_network, ip_address

net = ip_network("1.1.0.0/16")
print(ip_address("1.1.2.2") in net)    # True

是的,我也在其他帖子中看到过这种方法。不过,我应该提一下,我使用的是Python 2.7版本.....还是谢谢你。 - JChao
4
在安装了“ipaddress” pip软件包之后,这个方法在Python 2.7中也适用于我。 - srkiNZ84

10

在由点分隔的数字列表上进行字符串比较并不可行,因为例如输入1.1.99.99时,测试将会失败,因为'9'只是大于'2'而已。

>>> '1.1.99.99' < '1.1.255.255'
False

因此,您可以通过理解表达式将输入转换为整数元组。

def convert_ipv4(ip):
    return tuple(int(n) for n in ip.split('.'))

注意缺少类型检查,但如果您的输入是正确的IP地址,则可以正常工作。 由于您有一个包含2个IP地址的元组,因此可以创建一个函数,将起始和结束作为参数传递,并通过参数列表将该元组传入,然后使用一个语句返回它(因为Python允许链接比较)。 也许像这样:

def check_ipv4_in(addr, start, end):
    return convert_ipv4(start) < convert_ipv4(addr) < convert_ipv4(end)

试一下。

>>> ip_range = ('1.1.0.0', '1.1.255.255')
>>> check_ipv4_in('1.1.99.99', *ip_range)
True

使用这种方法,您可以懒惰地将其扩展到IPv6,但需要进行十六进制的转换(而不是 int)。


这个很好用。我喜欢它可以轻松过渡到IPv6。 - JChao
仍然错误:check_ipv4_in('1.1.99.499', *ip_range) -> True - Alexey
1
@Alexey 这只是一个无效的IP地址。请阅读我的答案,其中提到“请注意缺乏类型检查,但如果您的输入是正确的IP地址,则没有问题。” - metatoaster

6

您的代码比较字符串,而不是数字。我建议使用元组:

>>> ip_range = [(1,1,0,0), (1,1,255,255)]
>>> testip = (1,1,2,2)
>>> testip > ip_range[0] and testip < ip_range[1]
True
>>> testip = (1,3,1,1)
>>> testip > ip_range[0] and testip < ip_range[1]
False

4

对于 python 2 & 3,使用如下代码:

from ipaddress import ip_network, ip_address

def in_cidr(ip, cidr):
  return ip_address(ip) in ip_network(cidr)

演示


如果您使用的是Python 2.7,请使用以下方式进行安装:

pip install ipaddress 

2

这种方法通常不起作用,因为字符串比较是按照排序顺序进行的,而不是四个字段的数值。例如,'1.1.2.2' > '1.1.128.1' -- 在第5个字符中有关键区别,即 '1' 与 '2'。

如果您想比较这些字段,请尝试将其分成列表:

ip_vals = [int(x) for x in ip_range.split('.')]

现在ip_vals是一个值列表;您可以比较这些列表并获得您想要的结果。


1

IPv4和IPv6 DIY CIDR成员资格和范围

有一个名为ipaddress的模块,提供了所有所需的功能。以下不基于它-仅展示了另一种可能的方法。

构建模块

def ipv4_mask(cidr):
    mask = 2**32 - 2**(32 - int(cidr))
    return (mask >> sh & 0xff for sh in (24, 16, 8, 0))

def ipv6_mask(cidr):
    mask = 2**128 - 2**(128 - int(cidr))
    return (mask >> sh & 0xff for sh in range(120, -1, -8))

def ipv4_bytes(ip):
    return (int(b) for b in ip.split('.'))

def ipv6_bytes(ip):
    words  = ip.split(':')
    filled = False
    for word in words:
        if word:
            yield int(word[:-2] or '0', 16)
            yield int(word[-2:], 16)
        elif filled:
            yield 0
            yield 0
        else:
            filled = True
            for _ in range(9 - len(words)):
                yield 0
                yield 0

除了IPv6字节函数之外,所有基本功能都非常简单。不同的IPv6地址格式需要更多的逻辑来解析,而不是简单的IPv4格式。例如,回环可以表示为::1。或者连续的0可以用相邻的冒号表示,如:aaaa::1111表示aaaa:0:0:0:0:0:0:1111

成员资格检查

要确定IP是否在由IP和CIDR网络掩码位说明符定义的IP范围内,如果将网络掩码应用为其预期的方式(作为掩码),则无需计算起始和结束地址。下面的两个函数是确定IPv4地址是否是CIDR标记的网络IP成员的示例。另一个显示IPv6测试以确定一个子网是否在另一个子网中。

使用上述内容作为构建块,我们可以构建自定义函数用于ipv4或ipv6.:

def ipv4_cidr_member_of(ip1, ip2):
    ip2, m = ip2.split('/')
    return not any((a ^ b) & m 
                   for a, b, m in 
                   zip(ipv4_bytes(ip1), 
                       ipv4_bytes(ip2), 
                       ipv4_mask(m)))

def ipv6_cidr_subnet_of(ip1, ip2):
    ip1, m1 = ip1.split('/')
    ip2, m2 = ip2.split('/')
    return int(m1) >= int(m2) and \
           not any((a ^ b) & m
                   for a, b, m in
                   zip(ipv6_bytes(ip1),
                       ipv6_bytes(ip2),
                       ipv6_mask(m2)))

>>> ipv6_cidr_subnet_of('aaaa:bbbb:cccc:dddd:1100::/72', 
...                     'aaaa:bbbb:cccc:dddd::/64')
True
>>> ipv4_cidr_member_of('11.22.33.44', '11.22.33.0/24')
True
>>> 

采用这种方法,比较通常涉及异或两个IP字节,然后与网络掩码进行与运算。IPv4算法可以通过将以“ipv4_”开头的函数更改为“ipv6_”来简单地转换为IPv6,反之亦然。使用构建块,IPv4或IPv6的算法在此级别上是相同的。

使用构建块,可以创建自定义函数来确定两个CIDR表示的IP地址是否都位于同一网络上,或者一个是否与另一个位于同一网络中 - 这类似于逻辑中的...subnet_of()函数。

范围

请记住,如果您将掩码视为真正的掩码,则不必计算子网的范围以确定成员身份;如果出于任何原因您想要范围,则可以像上面的其他示例一样应用IP和网络掩码来获取它。

>>> def ipv4_cidr_range_bytes(ip):
...     ip, m = ip.split('/')
...     ip    = list(ipv4_bytes(ip))
...     m     = list(ipv4_mask(m))
...     start = [ b &  m         for b, m in zip(ip, m)]
...     end   = [(b | ~m) & 0xff for b, m in zip(ip, m)]
...     return start, end
...     
>>> ipv4_cidr_range_bytes('11.22.34.0/23')
([11, 22, 34, 0], [11, 22, 35, 255])
>>>
>>> # For IPv6, the above function could have been converted to look
>>> # just like it, but let's mix it up for fun with a single pass
>>> # over the data with zip(), then group into bytes objects with map()
>>>
>>> def ipv6_cidr_range_bytes(ip):
...     ip, m = ip.split('/')
...     s, e  = map(lambda *x: bytes(x), 
...                 *((b & m, (b | ~m) & 0xff)
...                   for b, m in zip(ipv6_bytes(ip),
...                                   ipv6_mask(m))))
...     return s, e
...     
>>> ipv6_cidr_range_bytes('aaaa:bbbb:cccc:dddd:1100::/72')
(b'\xaa\xaa\xbb\xbb\xcc\xcc\xdd\xdd\x11\x00\x00\x00\x00\x00\x00\x00', 
 b'\xaa\xaa\xbb\xbb\xcc\xcc\xdd\xdd\x11\xff\xff\xff\xff\xff\xff\xff')

效率

这些函数似乎比使用ipaddress对象和方法稍微快一些:

>>> # Using the ipaddress module:
>>> timeit.timeit("a = ip_network('192.168.1.0/24'); "
                  "b = ip_network('192.168.1.128/30'); "
...               "b.subnet_of(a)", globals=globals(), number=10**4)
0.2772132240352221
>>>
>>> # Using this code:
>>> timeit.timeit("ipv4_cidr_subnet_of('192.168.1.128/30', '192.168.1.0/24')", 
...               globals=globals(), number=10**4)
0.07261682399985148
>>> 

缓存

如果应用程序中存在重复的比较 - 相同的IP经常出现,可以使用functools.lru_cache装饰函数,可能会获得更高的效率:

from functools import lru_cache

@lru_cache
def ipv6_cidr_member_of(ip1, ip2):
    ip1    = ipv6_bytes(ip1)
    ip2, m = ip2.split('/')
    ip2    = ipv6_bytes(ip2)
    m      = ipv6_mask(m)
    return not any((a ^ b) & m for a, b, m in zip(ip1, ip2, m))

这将缓存参数和返回值,因此当在ip2中再次检查是否属于相同的ip1时,缓存会快速返回上次计算的值,函数体不需要重新执行操作。
>>> # Without caching:
>>> timeit.timeit("ipv6_cidr_member_of('aaaa:bbbb:cccc:dddd:11af:23af::',"
...                                   "'aaaa:bbbb:cccc:dddd::/64')",
...               globals=globals(), number=5)
0.00011115199959021993
>>> # 11.115199959021993e-05 <- the above time in sci. notation.
>>>
>>> # With caching (@lru_cach applied).
>>> timeit.timeit("ipv6_cidr_member_of('aaaa:bbbb:cccc:dddd:11af:23af::',"
...                                   "'aaaa:bbbb:cccc:dddd::/64')",
...               globals=globals(), number=5)
4.458599869394675e-05

这个测试只展示了5个循环。缓存命中与未命中的比率越高,效率提升就越大。

0

针对IPv6:

Python3

import ipaddress as ips

ip1=tuple(int(n,16) for n in ips.ip_address("fc00::255:3").exploded.split(':'))
ip2=tuple(int(n,16) for n in ips.ip_address("fc00::255:e").exploded.split(':'))
ip_bet=tuple(int(n,16) for n in ips.ip_address("fc00::255:a").exploded.split(':'))

if ip1 <= ip_bet <= ip2:
    print('Yes')
else:
    print('No')

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接