How can I check if an ip is in a network in Python?

Viewed 181799

Given an ip address (say 192.168.0.1), how do I check if it's in a network (say 192.168.0.0/24) in Python?

Are there general tools in Python for ip address manipulation? Stuff like host lookups, ip adddress to int, network address with netmask to int and so on? Hopefully in the standard Python library for 2.5.

29 Answers

Using ipaddress (in the stdlib since 3.3, at PyPi for 2.6/2.7):

>>> import ipaddress
>>> ipaddress.ip_address('192.168.0.1') in ipaddress.ip_network('192.168.0.0/24')
True

If you want to evaluate a lot of IP addresses this way, you'll probably want to calculate the netmask upfront, like

n = ipaddress.ip_network('192.0.0.0/16')
netw = int(n.network_address)
mask = int(n.netmask)

Then, for each address, calculate the binary representation with one of

a = int(ipaddress.ip_address('192.0.43.10'))
a = struct.unpack('!I', socket.inet_pton(socket.AF_INET, '192.0.43.10'))[0]
a = struct.unpack('!I', socket.inet_aton('192.0.43.10'))[0]  # IPv4 only

Finally, you can simply check:

in_network = (a & mask) == netw

I like to use netaddr for that:

from netaddr import CIDR, IP

if IP("192.168.0.1") in CIDR("192.168.0.0/24"):
    print "Yay!"

As arno_v pointed out in the comments, new version of netaddr does it like this:

from netaddr import IPNetwork, IPAddress
if IPAddress("192.168.0.1") in IPNetwork("192.168.0.0/24"):
    print "Yay!"

For python3

import ipaddress
ipaddress.IPv4Address('192.168.1.1') in ipaddress.IPv4Network('192.168.0.0/24')
ipaddress.IPv4Address('192.168.1.1') in ipaddress.IPv4Network('192.168.0.0/16')

Output :

False
True

This article shows you can do it with socket and struct modules without too much extra effort. I added a little to the article as follows:

import socket,struct

def makeMask(n):
    "return a mask of n bits as a long integer"
    return (2L<<n-1) - 1

def dottedQuadToNum(ip):
    "convert decimal dotted quad string to long integer"
    return struct.unpack('L',socket.inet_aton(ip))[0]

def networkMask(ip,bits):
    "Convert a network address to a long integer" 
    return dottedQuadToNum(ip) & makeMask(bits)

def addressInNetwork(ip,net):
   "Is an address in a network"
   return ip & net == net

address = dottedQuadToNum("192.168.1.1")
networka = networkMask("10.0.0.0",24)
networkb = networkMask("192.168.0.0",24)
print (address,networka,networkb)
print addressInNetwork(address,networka)
print addressInNetwork(address,networkb)

This outputs:

False
True

If you just want a single function that takes strings it would look like this:

import socket,struct

def addressInNetwork(ip,net):
   "Is an address in a network"
   ipaddr = struct.unpack('L',socket.inet_aton(ip))[0]
   netaddr,bits = net.split('/')
   netmask = struct.unpack('L',socket.inet_aton(netaddr))[0] & ((2L<<int(bits)-1) - 1)
   return ipaddr & netmask == netmask

Using Python >= 3.7 ipaddress:

import ipaddress

address = ipaddress.ip_address("192.168.0.1")
network = ipaddress.ip_network("192.168.0.0/16")

print(network.supernet_of(ipaddress.ip_network(f"{address}/{address.max_prefixlen}")))

Explanation

You can think of an IP Address as a Network with the largest possible netmask (/32 for IPv4, /128 for IPv6)

Checking whether 192.168.0.1 is in 192.168.0.0/16 is essentially the same as checking whether 192.168.0.1/32 is a subnet of 192.168.0.0/16

This code is working for me on Linux x86. I haven't really given any thought to endianess issues, but I have tested it against the "ipaddr" module using over 200K IP addresses tested against 8 different network strings, and the results of ipaddr are the same as this code.

def addressInNetwork(ip, net):
   import socket,struct
   ipaddr = int(''.join([ '%02x' % int(x) for x in ip.split('.') ]), 16)
   netstr, bits = net.split('/')
   netaddr = int(''.join([ '%02x' % int(x) for x in netstr.split('.') ]), 16)
   mask = (0xffffffff << (32 - int(bits))) & 0xffffffff
   return (ipaddr & mask) == (netaddr & mask)

Example:

>>> print addressInNetwork('10.9.8.7', '10.9.1.0/16')
True
>>> print addressInNetwork('10.9.8.7', '10.9.1.0/24')
False

Relying on the "struct" module can cause problems with endian-ness and type sizes, and just isn't needed. Nor is socket.inet_aton(). Python works very well with dotted-quad IP addresses:

def ip_to_u32(ip):
  return int(''.join('%02x' % int(d) for d in ip.split('.')), 16)

I need to do IP matching on each socket accept() call, against a whole set of allowable source networks, so I precompute masks and networks, as integers:

SNS_SOURCES = [
  # US-EAST-1
  '207.171.167.101',
  '207.171.167.25',
  '207.171.167.26',
  '207.171.172.6',
  '54.239.98.0/24',
  '54.240.217.16/29',
  '54.240.217.8/29',
  '54.240.217.64/28',
  '54.240.217.80/29',
  '72.21.196.64/29',
  '72.21.198.64/29',
  '72.21.198.72',
  '72.21.217.0/24',
  ]

def build_masks():
  masks = [ ]
  for cidr in SNS_SOURCES:
    if '/' in cidr:
      netstr, bits = cidr.split('/')
      mask = (0xffffffff << (32 - int(bits))) & 0xffffffff
      net = ip_to_u32(netstr) & mask
    else:
      mask = 0xffffffff
      net = ip_to_u32(cidr)
    masks.append((mask, net))
  return masks

Then I can quickly see if a given IP is within one of those networks:

ip = ip_to_u32(ipstr)
for mask, net in cached_masks:
  if ip & mask == net:
    # matched!
    break
else:
  raise BadClientIP(ipstr)

No module imports needed, and the code is very fast at matching.

As of Python 3.7, you can use subnet_of and supernet_of helper methods, which are part of the standard library:

To just test against a single IP, you can just use the subnet mask /32 which means "only this IP address" as a subnet, or you can pass the IP address to IPv4Nework or IPv6Nework constructors and they will return a subnet value for you.

So for your example:

from ipaddress import IPv4Network, IPv4Address

# Store IP Address as variable
>>> myip = IPv4Address('192.168.0.1')
>>> myip
IPv4Address('192.168.0.1')

# This treats the IP as a subnet
>>> myip_subnet = IPv4Network(myip)
>>> myip_subnet
IPv4Network('192.168.0.1/32')

# The other subnet to test membership against
>>> other_subnet = IPv4Network('192.168.0.0/24')
>>> other_subnet
IPv4Network('192.168.0.0/24')

# Now we can test
>>> myip_subnet.subnet_of(other_subnet)
True

Are there general tools in Python for ip address manipulation? Stuff like host lookups, ip adddress to int, network address with netmask to int and so on? Hopefully in the standard Python library for 2.5.

In Python 3, there's the ipaddress module which has tools for IPv4 and IPv6 manipulation. You can convert them to an int, by casting, i.e. int(IPv4Address('192.168.0.1')). Lots of other useful functions in the ipaddress module for hosts, etc.

#This works properly without the weird byte by byte handling
def addressInNetwork(ip,net):
    '''Is an address in a network'''
    # Convert addresses to host order, so shifts actually make sense
    ip = struct.unpack('>L',socket.inet_aton(ip))[0]
    netaddr,bits = net.split('/')
    netaddr = struct.unpack('>L',socket.inet_aton(netaddr))[0]
    # Must shift left an all ones value, /32 = zero shift, /0 = 32 shift left
    netmask = (0xffffffff << (32-int(bits))) & 0xffffffff
    # There's no need to mask the network address, as long as its a proper network address
    return (ip & netmask) == netaddr 

I don't know of anything in the standard library, but PySubnetTree is a Python library that will do subnet matching.

If you do not want to import other modules you could go with:

def ip_matches_network(self, network, ip):
    """
    '{:08b}'.format(254): Converts 254 in a string of its binary representation

    ip_bits[:net_mask] == net_ip_bits[:net_mask]: compare the ip bit streams

    :param network: string like '192.168.33.0/24'
    :param ip: string like '192.168.33.1'
    :return: if ip matches network
    """
    net_ip, net_mask = network.split('/')
    net_mask = int(net_mask)
    ip_bits = ''.join('{:08b}'.format(int(x)) for x in ip.split('.'))
    net_ip_bits = ''.join('{:08b}'.format(int(x)) for x in net_ip.split('.'))
    # example: net_mask=24 -> compare strings at position 0 to 23
    return ip_bits[:net_mask] == net_ip_bits[:net_mask]

I tried one subset of proposed solutions in these answers.. with no success, I finally adapted and fixed the proposed code and wrote my fixed function.

I tested it and works at least on little endian architectures--e.g.x86-- if anyone likes to try on a big endian architecture, please give me feedback.

IP2Int code comes from this post, the other method is a fully (for my test cases) working fix of previous proposals in this question.

The code:

def IP2Int(ip):
    o = map(int, ip.split('.'))
    res = (16777216 * o[0]) + (65536 * o[1]) + (256 * o[2]) + o[3]
    return res


def addressInNetwork(ip, net_n_bits):
    ipaddr = IP2Int(ip)
    net, bits = net_n_bits.split('/')
    netaddr = IP2Int(net)
    bits_num = int(bits)
    netmask = ((1L << bits_num) - 1) << (32 - bits_num)
    return ipaddr & netmask == netaddr & netmask

Hope useful,

Here is the solution using netaddr package

from netaddr import IPNetwork, IPAddress


def network_has_ip(network, ip):

    if not isinstance(network, IPNetwork):
        raise Exception("network parameter must be {0} instance".format(IPNetwork.__name__))

    if not isinstance(ip, IPAddress):
        raise Exception("ip parameter must be {0} instance".format(IPAddress.__name__))

    return (network.cidr.ip.value & network.netmask.value) == (ip.value & network.netmask.value)

To avoid having builtin or third party modules change their syntax over time, I created my own that does this. I'm using this as an importable module. I hope this helps someone:


def subnet_lookup(subnet: str, netmask: str, ip_address: str):
    """
    :param subnet: subnet to test against (as string)
    :param netmask: mask of subnet
    :param ip_address: ip to test against subnet and mask

    :return True if a match; False if not a match

    Steps:

    1) convert entire subnet into one binary word
    2) convert entire mask into one binary word
    3) determine bcast from comparing subnet and mask
    4) convert entire ip_address into one binary word
    5) convert entire subnet into decimal
    6) convert entire bcast into decimal
    7) convert entire ip_address into decimal
    8) determine if ip_address falls between subnet and bcast using range(); returns True if yes, False if no
    """

    def convert_whole_to_bin(whole):
        ip_dec_list = whole.split(".")
        ip_bin_str = ""

        for ip in ip_dec_list:
            binary = dec_to_bin(int(ip))
            ip_bin_str += binary

        return ip_bin_str

    def dec_to_bin(decimal_octet: int):
        binary = bin(decimal_octet).replace("0b", "")

        return binary.rjust(8, '0')

    def split_binary_into_list(binary_octet: str):
        bin_list = []
        for s in binary_octet:
            bin_list.append(s)

        return bin_list

    def determine_bcast(subnet, netmask):
        subnet_split = split_binary_into_list(subnet)
        netmask_split = split_binary_into_list(netmask)
        bcast_list = []

        for subnet, mask in zip(subnet_split, netmask_split):
            if mask != '0':
                bcast_list.append(subnet)

            else:
                bcast_list.append('1')

        bcast_bin = "".join(bcast_list)

        return bcast_bin

    def bin_to_dec(binary_single_word: str):
        decimal = int(binary_single_word, 2)

        return decimal

    def subnet_lookup(ip_address, subnet, bcast):

        return ip_address in range(subnet, bcast + 1)

    # 1) convert entire subnet into one binary word
    subnet_single_bin = convert_whole_to_bin(whole=subnet)

    # 2) convert entire mask into one binary word
    mask_single_bin = convert_whole_to_bin(whole=netmask)

    # 3) determine bcast from comparing subnet and mask
    bcast_single_bin = determine_bcast(subnet=subnet_single_bin, netmask=mask_single_bin)

    # 4) convert entire ip_address into one binary word
    ip_address_single_bin = convert_whole_to_bin(whole=ip_address)

    # 5) convert entire subnet into decimal
    subnet_single_dec = bin_to_dec(binary_single_word=subnet_single_bin)

    # 6) convert entire bcast into decimal
    bcast_single_dec = bin_to_dec(binary_single_word=bcast_single_bin)

    # 7) convert entire ip_address into decimal
    ip_address_single_dec = bin_to_dec(binary_single_word=ip_address_single_bin)

    # 8) determine if ip_address falls between subnet and bcast; returns True if yes, False if no
    lookup_result = subnet_lookup(ip_address=ip_address_single_dec, subnet=subnet_single_dec, bcast=bcast_single_dec)

    return lookup_result


# Testing:

subnet = "172.16.0.0"
netmask = "255.255.0.0"
ip_address = "172.16.255.255"

result = subnet_lookup(subnet=subnet, netmask=netmask, ip_address=ip_address)

print(result)
Related