6

I'm looking for an easy way to programmatically extract the private IPv4 address(es) of a computer.

Something similar to this question, but restricted to private IPs.

As an example, I can extract all the IPv4 addresses with the following command:

ifconfig | grep 'inet addr' | cut -d ':' -f 2 | awk '{ print $1 }'

Example output:

6.11.71.78
10.0.2.15
127.0.0.1

In a similar way, I'd like to get just IPs in the private address space. So, referring to the same example, the output should be:

10.0.2.15
  • I wrote the solution to that Q. What specifically do you want that that answer doesn't already provide? – slm Nov 5 '13 at 8:47
  • I've updated the question with more details. – Emyl Nov 5 '13 at 8:59
  • How do you define private address(es) I would have guessed 127.0.0.1 would be the private one. – Anthon Nov 5 '13 at 9:11
  • @Anthon I'd like to follow RFC 1918: en.wikipedia.org/wiki/… – Emyl Nov 5 '13 at 9:13
  • See my answer, gives you what you want. – slm Nov 5 '13 at 9:18
12

Anything in the private IP space will always start with one of three IP address blocks.

  • 24-bit block - 10.X.X.X
  • 20-bit block - 172.16.X.X - 172.31.X.X
  • 16-bit block - 192.168.X.X

So just grep for the above types of IP addresses.

$ ifconfig | grep 'inet addr' | cut -d ':' -f 2 | awk '{ print $1 }' | \
      grep -E '^(192\.168|10\.|172\.1[6789]\.|172\.2[0-9]\.|172\.3[01]\.)'
192.168.1.20

Details

The grep I'm using makes use of regular expressions. In this case we're looking for the following patterns:

  • 192.168
  • 10.
  • 172.1[6789].
  • 172.2[0-9].
  • 172.3[01].

Additionally we're being explicit in only matching numbers which start with one of these patterns. The anchor (^) is providing this capability to us.

More Examples

If we add the following lines to a file just to test the grep out.

$ cat afile 
192.168.0.1
10.11.15.3
1.23.3.4
172.16.2.4

We can then test it like so:

$ cat afile | grep -E '^(192\.168|10\.|172\.1[6789]\.|172\.2[0-9]\.|172\.3[01]\.)'
192.168.0.1
10.11.15.3
172.16.2.4
  • 1
    Class B isn't just one, it includes addresses from 172.16.0.0 to 172.31.255.255 – Emyl Nov 5 '13 at 9:19
  • Private IP spaces are 10.0.0.0/8 (ie. 10.0.0.0 => 10.255.255.255), 172.16.0.0/12 (ie. 172.16.0.0 => 172.31.255.255) ad 192.168.0.0/16 (ie. 192.168.0.0 => 192.168.255.255), so your regex won't match every private IP. – ssssteffff Nov 5 '13 at 9:20
  • 1
    I'm aware of Class B but never had a need to use it until today, let me adjust the regex. – slm Nov 5 '13 at 9:21
  • See updates, fixed regex – slm Nov 5 '13 at 9:23
  • @slm, don't call networks by the obselete class names. Classful networking was deprecated back in the late 90s. – Zoredache Nov 5 '13 at 17:34
1

IPv4 was created at a time when 32-bit systems were prevalent. The IPv4 dotted decimal address can be stored into a 32-bit unsigned integer, and bitwise operations are efficiently performed by network hardware. A bitmask for the 172.16.0.0/12 CIDR can be formed from a single left-shift, and checked against an address with a single bitwise-and.

There are three 'private' network address ranges defined by RFC-1918.

  • CIDR/8, (A) single large network, (24-bit, 16M) address range at 10.x.y.z/8
  • CIDR/12, (B) 16 contiguous networks (20-bit, 1M) address range at 172.16+x.y.z/12, where x in [0..15]
  • CIDR/16, (C) 256 contiguous networks (16-bit, 64K) address range at 192.168.y.z/16

Also, for carrier network subdivision,

  • CIDR/10, (A) single large network, (24-bit, 16M) address range at 100.64+x.y.z/10, where x in [0..63]

And for link-local addresses,

  • CIDR/16, (B) a single network (16-bit, 64K) address range at 169.254.y.z/16

With a language that supports bitwise operations, you can convert a dotted decimal address into an integer easily,

//assume x[0],x[1],x[2],x[3] are the parts of a dotted ip address
unsigned int ipv4 = (( (( (x[0]<<8) |x[1])<<8) |x[2])<<8) |x[3]

Suppose you have defined constants for the above listed addresses,

CIDR8 = (( (( (10<<8) |0xff)<<8) |0xff)<<8) |0xff
CIDR12 = (( (( (172<<8) |16 |0xf)<<8) |0xff)<<8) |0xff
CIDR16 = (( (( (192<<8) |168)<<8) |0xff)<<8) |0xff
CIDR10 = (( (( (100<<8) |64 |0x3f)<<8) |0xff)<<8) |0xff
CIDRLL = (( (( (169<<8) |254)<<8) |0xff)<<8) |0xff

Checking whether your ipv4 address is one of these addresses is simple,

ipv4 == (ipv4 & CIDR8)  //10.0.0.0/8
ipv4 == (ipv4 & CIDR12) //172.16.0.0/12
ipv4 == (ipv4 & CIDR16) //192.168.0.0/16
ipv4 == (ipv4 & CIDR10) //100.64.0.0/10
ipv4 == (ipv4 & CIDRLL) //169.254.0.0/16

Rather than check for 16 different 172.16.0.0/12 networks, one can use the above bitmask approach to directly check whether an ipv4 address is part of one of these private (NAT) networks. Choosing perl (python or ruby also work), instead of shell or awk, and using a single bit-wise and operation cuts down on the work considerably.

sub isprivate
{
    my($inet) = @_;
    if( $inet =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/ ) {
        if( $1==10 ) { return 10; }
        if( $1==172 && (($2 & 0x1f) == $2) ) { return 172; }
        if( $1==192 && ($2==168) ) { return 192; }
    }
    return 0;
};
sub iscarrier
{
    my($inet) = @_;
    if( $inet =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/ ) {
        if( $1==100 && (($2 & 0x7f) == $2) ) { return 100; }
    }
    return 0;
};
sub islinklocal
{
    my($inet) = @_;
    if( $inet =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/ ) {
        if( $1==169 && ($2==254) ) { return 169; }
    }
    return 0;
};

How do you want to classify addresses?

sub ipaddr
{
    my($inet) = @_;
    {
        if( isprivate($inet)>0 ) { $kind = "private"; }
        elsif( isloop($inet)>0 ) { $kind = "loopback"; }
        elsif( iscarrier($inet)>0 ) { $kind = "carrier"; }
        elsif( islinklocal($inet)>0 ) { $kind = "linklocal"; }
        else { $kind = ""; }
        print "$iface: $inet $netmask $broadcast ($flagsdesc) $kind\n";
    }
};

Run ifconfig from within a perl script,

$found = 0;
open($fh,"/sbin/ifconfig|");
while($line=<$fh>)
{
    chomp($line); $line =~ s/^\s+//;
    if( $line =~ /(\w+):\s+flags=(\d+)\s*\<(.*)\>\s+mtu\s+(\d+)\b/ ) {
        if( $found ) { ipaddr($inet); }
        $found = 1;
        ($iface,$flags,$flagsdesc,$mtu) = ($1,$2,$3,$4);
    }
    if( $line =~ /inet\s+(\d+\.\d+\.\d+\.\d+)\b/ ) {
        ($inet,$netmask,$broadcast) = ($1,"","");
        if( $line =~ /netmask\s+([\d+\.]+)\b/ ) { ($netmask) = ($1); }
        if( $line =~ /broadcast\s+([\d\.]+)\b/ ) { ($broadcast) = ($1); }
    }
}
if( $found ) { ipaddr($inet); }
0

The output (one line per IP) can be filtered with the following script:

#!/bin/sh
PATTERN='^10\.' #  10.0.0.0/8
PATTERN+='|^192\.168\.'  # 192.168.0.0/16
PATTERN+='|^169\.254\.' # not strictly private range, but link local
for i in $(seq 16 31) ; do # 172.16.0.0/12
    PATTERN+="|^172\.$i\." 
done
egrep "$PATTERN"
exit 0

Usage e.g.:

ifconfig | grep 'inet addr' | cut -d ':' -f 2 | awk '{ print $1 }' | ./filter_private_ips
0

Show private IPs

ip -o addr show | \
  grep -v 'inet6' | \
  grep -v 'scope host' | \
  awk '{print $4}' | \
  cut -d '/' -f 1 | \
  grep -E '^(192\.168|10\.|172\.1[6789]\.|172\.2[0-9]\.|172\.3[01]\.)'

Show public IPs

ip -o addr show | \
  grep -v 'inet6' | \
  grep -v 'scope host' | \
  awk '{print $4}' | \
  cut -d '/' -f 1 | \
  grep -vE '^(192\.168|10\.|172\.1[6789]\.|172\.2[0-9]\.|172\.3[01]\.)'

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.