Track 10 · Lesson 6 of 6
Build a subnet calculator
Build a shell script that turns an address and prefix into the network, broadcast, host range and host count, including the /31 and /32 cases most calculators get wrong.
This lesson builds the same calculator as the subnet tool, so you can do the whole job from the shell without opening it. By the end you will have a script called subnetcalc.sh that takes 192.168.1.42/24 and prints the network, the broadcast address, the mask, the wildcard, the first and last usable host, and how many addresses there are.
You build it up a few lines at a time, running it after every step, and the finished script sits in one block at the end so you can copy the whole thing at once.
What you'll be able to do
- See which subnet your own phone is on.
- Turn a dotted quad into a single number and back.
- Build a subnet mask from a prefix length.
- Get the network with a bitwise AND and the broadcast with an OR.
- Handle a /31 and a /32 correctly, which most calculators do not.
- Reject a mask that is not a run of ones followed by zeros.
Step 1: Look at your own subnet first
Before calculating anything, look at what you are actually on.
ip -o -4 addr show
1: lo inet 127.0.0.1/8 scope global dynamic
2: wlan0 inet 192.168.1.24/24 scope global dynamic
192.168.1.24/24 is your address and the prefix. The prefix says how many of the 32 bits are fixed for the network. A /24 fixes 24 and leaves 8 for hosts, which is two to the eight, which is 256 addresses. Every number the finished script prints comes out of that one idea.
If ip is missing, pkg install iproute2 provides it.
Step 2: An address is really one number
Four numbers between zero and 255, each worth one byte. Multiply each by its place value and add them up and you have a single integer.
cat > step2.sh <<'EOF'
ip_to_int() {
printf '%s' "$1" | awk -F. '{ printf "%.0f", ($1 * 16777216) + ($2 * 65536) + ($3 * 256) + $4 }'
}
for a in 192.168.1.42 10.0.0.5 255.255.255.0; do
printf '%-16s -> %s\n' "$a" "$(ip_to_int "$a")"
done
EOF
bash step2.sh
192.168.1.42 -> 3232235818
10.0.0.5 -> 167772165
255.255.255.0 -> 4294967040
-F. splits the input on dots, so $1 to $4 are the four octets. The %.0f matters: awk does its arithmetic in floating point, and printing an integer that large without it gives you scientific notation. 256, 65536 and 16777216 are the place values of the four bytes.
Step 3: And back again
The reverse is a shift and a mask per byte.
cat > step3.sh <<'EOF'
int_to_ip() {
local n=$1
printf '%d.%d.%d.%d' $(( (n >> 24) & 255 )) $(( (n >> 16) & 255 )) $(( (n >> 8) & 255 )) $(( n & 255 ))
}
for n in 3232235818 4294967295 0; do
printf '%-12s -> %s\n' "$n" "$(int_to_ip "$n")"
done
EOF
bash step3.sh
3232235818 -> 192.168.1.42
4294967295 -> 255.255.255.255
0 -> 0.0.0.0
n >> 24 shifts the top byte down into the low bits, and & 255 keeps only that byte. Do it four times with a different shift each time and the four octets fall out in order.
Step 4: The mask, from a prefix
A mask is a run of ones followed by a run of zeros. A /24 is twenty-four ones and eight zeros.
cat > step4.sh <<'EOF'
mask_from_prefix() {
printf '%d' $(( (0xffffffff << (32 - $1)) & 0xffffffff ))
}
int_to_ip() {
local n=$1
printf '%d.%d.%d.%d' $(( (n >> 24) & 255 )) $(( (n >> 16) & 255 )) $(( (n >> 8) & 255 )) $(( n & 255 ))
}
for p in 24 12 31 32 0; do
printf '/%-3s -> %s\n' "$p" "$(int_to_ip "$(mask_from_prefix "$p")")"
done
EOF
bash step4.sh
/24 -> 255.255.255.0
/12 -> 255.240.0.0
/31 -> 255.255.255.254
/32 -> 255.255.255.255
/0 -> 0.0.0.0
Start with all thirty-two bits set, shift left by the number of host bits, and you have the mask. The & 0xffffffff at the end is doing real work: shell arithmetic on 64-bit integers would otherwise leave the top bits set, and the mask would be wrong.
Now 255.240.0.0 stops looking arbitrary. It is twelve ones and twenty zeros, and twelve is the prefix length.
Step 5: Network with an AND, broadcast with an OR
The network address has every host bit set to zero, so it is the address ANDed with the mask. The broadcast address has every host bit set to one, so it is the network ORed with the inverse of the mask.
cat > step5.sh <<'EOF'
ip_to_int() { printf '%s' "$1" | awk -F. '{ printf "%.0f", ($1 * 16777216) + ($2 * 65536) + ($3 * 256) + $4 }'; }
int_to_ip() { local n=$1; printf '%d.%d.%d.%d' $(( (n >> 24) & 255 )) $(( (n >> 16) & 255 )) $(( (n >> 8) & 255 )) $(( n & 255 )); }
mask_from_prefix() { printf '%d' $(( (0xffffffff << (32 - $1)) & 0xffffffff )); }
n=$(ip_to_int 192.168.1.42)
m=$(mask_from_prefix 24)
net=$(( n & m ))
bc=$(( net | (~m & 0xffffffff) ))
printf 'address %s\n' "$(int_to_ip "$n")"
printf 'netmask %s\n' "$(int_to_ip "$m")"
printf 'network %s\n' "$(int_to_ip "$net")"
printf 'broadcast %s\n' "$(int_to_ip "$bc")"
EOF
bash step5.sh
address 192.168.1.42
netmask 255.255.255.0
network 192.168.1.0
broadcast 192.168.1.255
~m flips every bit, turning the mask into its inverse, which is the wildcard mask. ANDing the address with the mask clears the host bits and leaves the network. ORing the network with the wildcard sets them all and gives the broadcast.
Step 6: How many hosts, and the two special cases
The count is two to the power of the host bits. Then two addresses come off, because the network and broadcast addresses cannot be given to a machine. Except when they can.
cat > step6.sh <<'EOF'
ip_to_int() { printf '%s' "$1" | awk -F. '{ printf "%.0f", ($1 * 16777216) + ($2 * 65536) + ($3 * 256) + $4 }'; }
int_to_ip() { local n=$1; printf '%d.%d.%d.%d' $(( (n >> 24) & 255 )) $(( (n >> 16) & 255 )) $(( (n >> 8) & 255 )) $(( n & 255 )); }
mask_from_prefix() { printf '%d' $(( (0xffffffff << (32 - $1)) & 0xffffffff )); }
for spec in 192.168.1.42/24 10.0.0.5/31 192.0.2.9/32; do
addr=${spec%%/*}
prefix=${spec#*/}
n=$(ip_to_int "$addr")
m=$(mask_from_prefix "$prefix")
net=$(( n & m ))
bc=$(( net | (~m & 0xffffffff) ))
total=$(( 2 ** (32 - prefix) ))
if [ "$prefix" -eq 32 ]; then
usable=1; first=$n; last=$n
elif [ "$prefix" -eq 31 ]; then
usable=2; first=$net; last=$bc
else
usable=$(( total - 2 )); first=$(( net + 1 )); last=$(( bc - 1 ))
fi
printf '%-18s network %-15s broadcast %-15s total %-8s usable %-6s first %-15s last %s\n' \
"$spec" "$(int_to_ip "$net")" "$(int_to_ip "$bc")" "$total" "$usable" "$(int_to_ip "$first")" "$(int_to_ip "$last")"
done
EOF
bash step6.sh
192.168.1.42/24 network 192.168.1.0 broadcast 192.168.1.255 total 256 usable 254 first 192.168.1.1 last 192.168.1.254
10.0.0.5/31 network 10.0.0.4 broadcast 10.0.0.5 total 2 usable 2 first 10.0.0.4 last 10.0.0.5
192.0.2.9/32 network 192.0.2.9 broadcast 192.0.2.9 total 1 usable 1 first 192.0.2.9 last 192.0.2.9
Look at the /31. It has two addresses and both of them are usable, because there is no room to reserve a network or a broadcast address. RFC 3021 defines this, and it is exactly what you want on a point-to-point link between two routers. The /32 is a single address, and it is both the network and the host. Many calculators subtract two from a /31 and cheerfully report zero usable hosts, which is wrong.
Step 7: Accept a mask as well as a prefix
People write 255.255.255.0 as often as they write /24, so accept both. Going from a mask back to a prefix is a check as well as a conversion, because not every four numbers are a valid mask.
cat > step7.sh <<'EOF'
ip_to_int() { printf '%s' "$1" | awk -F. '{ printf "%.0f", ($1 * 16777216) + ($2 * 65536) + ($3 * 256) + $4 }'; }
prefix_from_mask() {
local m=$1
if [ "$m" -eq 0 ]; then
printf '0'
return 0
fi
if [ $(( m + (m & -m) )) -ne 4294967296 ]; then
return 1
fi
local wild=$(( (~m) & 0xffffffff ))
local size=$(( wild + 1 )) p=32
while [ "$size" -gt 1 ]; do
size=$(( size / 2 ))
p=$(( p - 1 ))
done
printf '%d' "$p"
}
for mask in 255.255.255.0 255.240.0.0 255.0.255.0; do
printf '%-16s -> ' "$mask"
if p=$(prefix_from_mask "$(ip_to_int "$mask")"); then
echo "/$p"
else
echo "not a run of ones then zeros"
fi
done
EOF
bash step7.sh
255.255.255.0 -> /24
255.240.0.0 -> /12
255.0.255.0 -> not a run of ones then zeros
The validity check is the interesting line. For a real mask, adding the mask to its own lowest set bit carries all the way out and lands exactly on 2^32. For a mask with a hole in it, like 255.0.255.0, the carry stops short and the sum is smaller, so the test fails. m & -m is the trick for isolating the lowest set bit.
Step 8: Arguments, usage and errors
A calculator that prints a stack trace when you mistype an address is not finished. Accept ADDR/PREFIX, ADDR PREFIX and ADDR NETMASK, and complain clearly otherwise.
cat > step8.sh <<'EOF'
prog=${0##*/}
valid_ip() {
awk -F. '
NF != 4 { exit 1 }
{
for (i = 1; i <= 4; i++) {
if ($i !~ /^[0-9]+$/) exit 1
if ($i + 0 > 255) exit 1
}
}
' <<< "$1"
}
case "$1" in
*/*) addr=${1%%/*}; prefix_arg=${1#*/} ;;
*) addr=$1; prefix_arg=${2:-} ;;
esac
if ! valid_ip "$addr"; then
printf '%s: not a valid IPv4 address: %s\n' "$prog" "$addr" >&2
exit 1
fi
case "$prefix_arg" in
*[!0-9]*|'') printf '%s: prefix must be a number from 0 to 32: %s\n' "$prog" "$prefix_arg" >&2; exit 1 ;;
*) if [ "$prefix_arg" -gt 32 ]; then printf '%s: prefix must be 0 to 32\n' "$prog" >&2; exit 1; fi ;;
esac
printf 'address %s, prefix /%s\n' "$addr" "$prefix_arg"
EOF
bash step8.sh 999.1.1.1/24; echo " exit $?"
bash step8.sh 192.168.1.1/33; echo " exit $?"
step8.sh: not a valid IPv4 address: 999.1.1.1
exit 1
step8.sh: prefix must be a number from 0 to 32: 33
exit 1
valid_ip checks two things the eye misses: that there are exactly four parts, and that each one is digits only and no larger than 255. The $i !~ /^[0-9]+$/ test is what stops 1.2.3.4x being accepted.
The whole script
Save this as subnetcalc.sh and make it executable with chmod +x subnetcalc.sh. Nothing is left out.
#!/usr/bin/env bash
# subnetcalc.sh - work out the figures for an IPv4 subnet.
set -u
prog=${0##*/}
usage() {
cat <<'USAGE'
Usage: subnetcalc.sh ADDRESS/PREFIX
subnetcalc.sh ADDRESS PREFIX
subnetcalc.sh ADDRESS NETMASK
Examples:
subnetcalc.sh 192.168.1.42/24
subnetcalc.sh 192.168.1.42 24
subnetcalc.sh 192.168.1.42 255.255.255.0
ADDRESS is an IPv4 address. PREFIX is 0 to 32. NETMASK is a dotted
mask such as 255.255.255.0.
USAGE
}
# Turn a dotted quad into one 32 bit integer.
ip_to_int() {
printf '%s' "$1" | awk -F. '{ printf "%.0f", ($1 * 16777216) + ($2 * 65536) + ($3 * 256) + $4 }'
}
# Turn one 32 bit integer back into a dotted quad.
int_to_ip() {
local n=$1
printf '%d.%d.%d.%d' $(( (n >> 24) & 255 )) $(( (n >> 16) & 255 )) $(( (n >> 8) & 255 )) $(( n & 255 ))
}
# True when the argument is four numbers, each 0 to 255.
valid_ip() {
awk -F. '
NF != 4 { exit 1 }
{
for (i = 1; i <= 4; i++) {
if ($i !~ /^[0-9]+$/) exit 1
if ($i + 0 > 255) exit 1
}
}
' <<< "$1"
}
# Turn a prefix length into a mask, as an integer.
mask_from_prefix() {
printf '%d' $(( (0xffffffff << (32 - $1)) & 0xffffffff ))
}
# Turn a mask into a prefix length, or fail if the mask has a hole.
prefix_from_mask() {
local m=$1
if [ "$m" -eq 0 ]; then
printf '0'
return 0
fi
if [ $(( m + (m & -m) )) -ne 4294967296 ]; then
return 1
fi
local wild=$(( (~m) & 0xffffffff ))
local size=$(( wild + 1 )) p=32
while [ "$size" -gt 1 ]; do
size=$(( size / 2 ))
p=$(( p - 1 ))
done
printf '%d' "$p"
}
if [ $# -eq 0 ]; then
usage >&2
exit 2
fi
case "$1" in
-h|--help)
usage
exit 0
;;
-*)
printf '%s: unknown option %s\n' "$prog" "$1" >&2
printf 'Try %s --help for usage.\n' "$prog" >&2
exit 2
;;
esac
case "$1" in
*/*)
addr=${1%%/*}
prefix_arg=${1#*/}
;;
*)
addr=$1
prefix_arg=${2:-}
;;
esac
if [ -z "$prefix_arg" ]; then
printf '%s: no prefix or netmask given\n' "$prog" >&2
printf 'Try %s --help for usage.\n' "$prog" >&2
exit 2
fi
if ! valid_ip "$addr"; then
printf '%s: not a valid IPv4 address: %s\n' "$prog" "$addr" >&2
exit 1
fi
case "$prefix_arg" in
*.*)
if ! valid_ip "$prefix_arg"; then
printf '%s: not a valid netmask: %s\n' "$prog" "$prefix_arg" >&2
exit 1
fi
m=$(ip_to_int "$prefix_arg")
if ! prefix=$(prefix_from_mask "$m"); then
printf '%s: netmask is not a run of ones then zeros: %s\n' "$prog" "$prefix_arg" >&2
exit 1
fi
;;
*[!0-9]*|'')
printf '%s: prefix must be a number from 0 to 32: %s\n' "$prog" "$prefix_arg" >&2
exit 1
;;
*)
prefix=$prefix_arg
if [ "$prefix" -gt 32 ]; then
printf '%s: prefix must be a number from 0 to 32: %s\n' "$prog" "$prefix_arg" >&2
exit 1
fi
;;
esac
n=$(ip_to_int "$addr")
m=$(mask_from_prefix "$prefix")
net=$(( n & m ))
bc=$(( net | (~m & 0xffffffff) ))
wild=$(( (~m) & 0xffffffff ))
total=$(( 2 ** (32 - prefix) ))
if [ "$prefix" -eq 32 ]; then
usable=1
first=$n
last=$n
elif [ "$prefix" -eq 31 ]; then
usable=2
first=$net
last=$bc
else
usable=$(( total - 2 ))
first=$(( net + 1 ))
last=$(( bc - 1 ))
fi
printf 'address %s\n' "$addr"
printf 'prefix /%d\n' "$prefix"
printf 'network %s\n' "$(int_to_ip "$net")"
printf 'broadcast %s\n' "$(int_to_ip "$bc")"
printf 'netmask %s\n' "$(int_to_ip "$m")"
printf 'wildcard %s\n' "$(int_to_ip "$wild")"
printf 'first host %s\n' "$(int_to_ip "$first")"
printf 'last host %s\n' "$(int_to_ip "$last")"
printf 'addresses %d\n' "$total"
printf 'usable %d\n' "$usable"
Using it
An ordinary home network:
./subnetcalc.sh 192.168.1.42/24
address 192.168.1.42
prefix /24
network 192.168.1.0
broadcast 192.168.1.255
netmask 255.255.255.0
wildcard 0.0.0.255
first host 192.168.1.1
last host 192.168.1.254
addresses 256
usable 254
A /31, where both addresses are usable:
./subnetcalc.sh 10.0.0.5/31
address 10.0.0.5
prefix /31
network 10.0.0.4
broadcast 10.0.0.5
netmask 255.255.255.254
wildcard 0.0.0.1
first host 10.0.0.4
last host 10.0.0.5
addresses 2
usable 2
A /32, a single host:
./subnetcalc.sh 192.0.2.9/32
address 192.0.2.9
prefix /32
network 192.0.2.9
broadcast 192.0.2.9
netmask 255.255.255.255
wildcard 0.0.0.0
first host 192.0.2.9
last host 192.0.2.9
addresses 1
usable 1
A /12, which does not end on a byte boundary:
./subnetcalc.sh 172.16.5.5/12
address 172.16.5.5
prefix /12
network 172.16.0.0
broadcast 172.31.255.255
netmask 255.240.0.0
wildcard 0.15.255.255
first host 172.16.0.1
last host 172.31.255.254
addresses 1048576
usable 1048574
A mask instead of a prefix gives the same answer:
./subnetcalc.sh 192.168.1.42 255.255.255.0
address 192.168.1.42
prefix /24
network 192.168.1.0
And the errors are plain:
./subnetcalc.sh 192.168.1.42 255.0.255.0
subnetcalc.sh: netmask is not a run of ones then zeros: 255.0.255.0
To check the subnet you are on right now, feed it your own address:
./subnetcalc.sh "$(ip -o -4 addr show | awk 'NR==1{split($4,a,"/"); print a[1]"/"a[2]}')"
Quick reference
| Piece | What it does |
|---|---|
ip -o -4 addr show | your own addresses and prefixes, one per line |
awk -F. | split a dotted quad on the dots |
printf '%.0f' | stop awk printing a large integer in scientific notation |
n >> 24 and & 255 | pull one byte out of the number |
0xffffffff << (32 - p) | build a mask from a prefix length |
n & m | the network address |
net | (~m) | the broadcast address |
2 ** (32 - p) | how many addresses the block holds |
m & -m | isolate the lowest set bit, used to validate a mask |
/31 and /32 | the two cases where nothing is reserved |
Next steps
- Networking basics covers the addresses and ports this lesson assumes.
- DNS and addresses is where a name becomes one of these addresses.
- Build a permissions calculator is the same approach applied to a different kind of bit arithmetic.
Test yourself on this
4 questions and 1 task where you run a real command, with the reasoning explained either way.