Networking Fundamentals · beginner · ~10 min
- Read an IPv4 dotted-decimal address and an IPv6 hexadecimal address, and explain what each part means. - Convert between the human-readable form of an address and its underlying bit pattern. - Recognise the RFC 1918 private ranges, loopback, link-local, and other special-purpose addresses on sight. - Decide whether a given address is publicly routable (directly reachable from the internet) or private (reachable only from inside a network). - Apply IPv6 `::` zero-compression rules correctly, both reading and writing compressed addresses.
An IP address is the number that identifies a host at the Internet layer of the network stack. In The TCP/IP and OSI models you saw that data travels down through layers, and that the Internet (network) layer is responsible for getting a packet from one machine to another across networks. The IP address is the label that layer uses to decide where a packet goes. Without it, a router has no idea which of billions of machines should receive your packet.
There are two versions in use today. IPv4 is the older, still-dominant scheme: a 32-bit number written as four decimal octets, like 192.168.1.10. IPv6 is its successor: a 128-bit number written in hexadecimal, like 2001:db8::1. IPv6 exists because 32 bits only allows about 4.3 billion addresses, and the internet ran out.
Not every address is meant to be reachable from anywhere. Some ranges are reserved for private networks (your home or office), some point back at the machine itself (loopback), and some are assigned automatically when normal configuration fails (link-local). Learning to tell these apart at a glance is a foundational networking skill — and the very first question in any security assessment: is this thing even reachable, and from where?
In plain terms first: an IP address is like a postal address for a computer. The terminology — octet, prefix, RFC 1918, loopback, link-local — is just precise vocabulary for the different kinds of "address" and the rules about who can send mail there.
Every packet that crosses a network carries a source and destination IP address. If you cannot read an address, you cannot read a firewall rule, a routing table, a server log, or a packet capture — which means you cannot configure, debug, or defend a network.
The public-versus-private distinction is where this becomes concrete. A public address is globally routable: any machine on the internet can attempt to reach it. A private address only works inside a local network and must be translated (via NAT) before it can talk to the internet at all.
Recon — reconnaissance, the first phase of a security assessment — depends entirely on this. It answers two questions: what is in scope, and what is reachable? Public addresses are directly attackable from the internet; private addresses require a foothold inside the network first. Recognising loopback (127.0.0.1, ::1) and link-local (169.254.x.x, fe80::) addresses on sight saves time and, importantly, keeps you from accidentally probing systems that were never in scope. Reading logs during an incident, you use the same skill in reverse: a login from a public address on the other side of the world means something very different from one that came from 10.0.0.5 on your own LAN.
Definition. An IPv4 address is a 32-bit unsigned integer. For humans it is written as four decimal numbers separated by dots — 192.168.1.10 — where each number is one octet (8 bits).
How it works internally. The dots are only for our eyes. Underneath, the address is a single 32-bit value. Each octet occupies 8 bits, so it ranges from 0 to 255. To get the numeric value, you shift each octet into place:
192.168.1.10 as one 32-bit number
192 168 1 10
11000000 . 10101000 . 00000001 . 00001010
|--byte 3--|--byte 2--|--byte 1--|--byte 0--|
value = (192<<24) | (168<<16) | (1<<8) | 10
= 3232235786
When to use the numeric form. Comparisons, range checks, and subnet math are all far easier on the 32-bit integer than on the string. A firewall deciding whether 192.168.1.10 falls inside 192.168.1.0/24 does it with a single bit-mask on the integer.
When NOT to. Do not do arithmetic on the dotted string with naive string parsing that trusts its input — 256.1.1.1 and 1.2.3 and 0x7f.0.0.1 are all things attackers and buggy tools produce. Validate strictly.
Pitfall. Forgetting that an octet maxes out at 255. 300.1.1.1 is not a valid address, and a parser that silently wraps or truncates it is a bug.
Knowledge check: How many bits are in an IPv4 address, and what is the largest value a single octet can hold?
Definition. RFC 1918 reserves three blocks of IPv4 space for private use. These are never routed on the public internet:
| Block | Range | Size |
|---|---|---|
10.0.0.0/8 |
10.0.0.0 – 10.255.255.255 |
~16.7 million |
172.16.0.0/12 |
172.16.0.0 – 172.31.255.255 |
~1 million |
192.168.0.0/16 |
192.168.0.0 – 192.168.255.255 |
~65 thousand |
Plain explanation. Because these addresses are reused in millions of homes and offices, they cannot be globally unique, so routers on the internet simply drop them. Your laptop on 192.168.1.10 reaches the internet only because your router performs NAT (Network Address Translation), rewriting the private source address to the router's single public address.
The 172.16/12 trap. The tricky one is the middle block. It is /12, not /16, so it covers 172.16.0.0 through 172.31.255.255 — the second octet runs 16 to 31 inclusive. 172.15.0.0 is public; 172.32.0.0 is public; 172.20.5.4 is private.
Knowledge check (concept): Reason from the table: is
172.31.255.255public or private? What about172.32.0.1? Which single octet decides it?
Definition. Certain addresses have fixed, reserved meanings.
| Address / range | Meaning |
|---|---|
127.0.0.0/8 (usually 127.0.0.1) |
Loopback — refers to the local machine itself |
0.0.0.0 |
"Unspecified" / "any" — as a destination it is invalid; as a bind address it means "all interfaces" |
169.254.0.0/16 |
Link-local — self-assigned when DHCP fails (APIPA) |
255.255.255.255 |
Limited broadcast |
How loopback works internally. A packet sent to 127.0.0.1 never touches a network cable. The OS recognises the loopback range and routes the packet straight back up its own stack. This is why every lab in this course targets loopback: you talk to a server on your own machine with zero risk of reaching anyone else.
Pitfall. Treating 0.0.0.0 as a real host. When a server "listens on 0.0.0.0:8080" it means "accept connections arriving on any of my interfaces," not that some machine has that address.
Knowledge check (explain in your own words): Why does a packet to
127.0.0.1never appear on the physical network?
Definition. An IPv6 address is 128 bits, written as eight groups of four hexadecimal digits separated by colons: 2001:0db8:85a3:0000:0000:8a2e:0370:7334.
Two shortening rules.
0db8 → db8, 0000 → 0.::.So the address above compresses to 2001:db8:85a3::8a2e:370:7334.
Full: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
Step 1: 2001: db8:85a3: 0: 0:8a2e: 370:7334 (drop leading zeros)
Step 2: 2001: db8:85a3::8a2e:370:7334 (:: for the 0:0 run)
The one-:: rule. :: may appear only once, because otherwise the address would be ambiguous — a parser could not tell how many zero groups each :: represents. 2001::25de::cade is invalid.
Common IPv6 addresses.
| Address | Meaning |
|---|---|
::1 |
Loopback (the IPv6 equivalent of 127.0.0.1) |
:: |
Unspecified / "any" |
fe80::/10 |
Link-local (starts with fe80:) |
2000::/3 |
Global unicast (public, routable) |
Pitfall. Writing :: twice, or forgetting that ::1 (loopback) is not the same as :1 or 1::.
Knowledge check (predict): How many all-zero 16-bit groups does the
::infe80::1stand for?
IPv4 dotted-quad and its numeric equivalent:
192 . 168 . 1 . 10 four octets, each 0-255
└─┬─┘ one octet = 8 bits
32 bits total = one uint32_t
Programmatic parsing and classification in C uses the POSIX sockets API:
#include <arpa/inet.h> // inet_pton, struct in_addr / in6_addr
struct in_addr v4; // holds a 32-bit IPv4 address (network byte order)
struct in6_addr v6; // holds a 128-bit IPv6 address
// inet_pton returns 1 on success, 0 if the string is not a valid literal,
// and -1 on an unsupported address family.
int ok = inet_pton(AF_INET, "192.168.1.10", &v4);
inet_pton ("presentation to network") is the safe, modern parser: it rejects malformed input and handles both AF_INET (IPv4) and AF_INET6 (IPv6). Prefer it over the old inet_aton/inet_addr, which accept sloppy input like 1.2 or return an ambiguous value for 255.255.255.255.
An IP address identifies a host on a network at the Internet layer.
An IPv4 address is 32 bits, written as four dotted decimal numbers: 192.168.1.10.
Each number is an octet (8 bits), with a value from 0 to 255. This gives about 4.3 billion addresses. That is not enough for today's internet, which is why NAT and IPv6 exist.
These ranges are not routable on the public internet. They are used inside homes and offices:
10.0.0.0/8172.16.0.0/12192.168.0.0/16127.0.0.1 - loopback (this machine). Every lab in this course targets loopback.0.0.0.0 - "any" or "unspecified".169.254.0.0/16 - link-local (assigned when DHCP fails).An IPv6 address is 128 bits, written as eight groups of hexadecimal digits:
2001:0db8:85a3::8a2e:0370:7334
:: compresses one run of all-zero groups.::1.fe80::.A public IP is globally routable and assigned by an ISP or registry.
A private IP only works inside a local network. It must be translated (via NAT) to reach the internet.
During recon, knowing whether an address is public or private tells you whether it is directly reachable.
#include <stdio.h> #include <stdint.h> #include <string.h> #include <arpa/inet.h> // inet_pton, ntohl
// Classify an IPv4 address held as a host-order 32-bit integer. static const char *classify_v4(uint32_t ip) { uint8_t a = (ip >> 24) & 0xFF; // first octet uint8_t b = (ip >> 16) & 0xFF; // second octet
if (a == 127) return "loopback";
if (a == 10) return "private (10.0.0.0/8)";
if (a == 172 && b >= 16 && b <= 31) return "private (172.16.0.0/12)";
if (a == 192 && b == 168) return "private (192.168.0.0/16)";
if (a == 169 && b == 254) return "link-local (169.254.0.0/16)";
if (ip == 0) return "unspecified (0.0.0.0)";
return "public (globally routable)";
}
int main(void) { const char *samples[] = { "192.168.1.10", "10.0.0.5", "172.16.5.4", "172.40.5.4", "127.0.0.1", "169.254.13.7", "8.8.8.8", "300.1.1.1", "::1" }; size_t n = sizeof(samples) / sizeof(samples[0]);
for (size_t i = 0; i < n; i++) {
const char *s = samples[i];
struct in_addr v4;
struct in6_addr v6;
if (inet_pton(AF_INET, s, &v4) == 1) {
// v4.s_addr is in network byte order; convert to host order first.
uint32_t ip = ntohl(v4.s_addr);
printf("%-15s IPv4 -> %s\n", s, classify_v4(ip));
} else if (inet_pton(AF_INET6, s, &v6) == 1) {
// Detect the IPv6 loopback ::1 by comparing to the known constant.
const char *kind = memcmp(&v6, &in6addr_loopback,
sizeof v6) == 0 ? "loopback" : "IPv6";
printf("%-15s IPv6 -> %s\n", s, kind);
} else {
printf("%-15s (not a valid IP literal)\n", s);
}
}
return 0;
}
classify_v4 takes the address as a single host-order uint32_t and pulls out the octets it needs with shifts and masks. (ip >> 24) & 0xFF slides the top 8 bits down to the bottom and keeps only those 8 bits — that is the first octet. (ip >> 16) & 0xFF does the same for the second octet.
The checks run in a deliberate order:
| Condition | Matches | Result |
|---|---|---|
a == 127 |
127.x.x.x |
loopback |
a == 10 |
10.x.x.x |
private /8 |
a == 172 && 16<=b<=31 |
172.16–172.31 |
private /12 |
a == 192 && b == 168 |
192.168.x.x |
private /16 |
a == 169 && b == 254 |
169.254.x.x |
link-local |
ip == 0 |
0.0.0.0 |
unspecified |
| (fall-through) | everything else | public |
In main, we loop over a mix of valid and invalid strings. For each one:
inet_pton(AF_INET, s, &v4) tries to parse s as IPv4. It returns 1 only for a strict, valid dotted-quad. "300.1.1.1" fails here (octet > 255), so it is not misclassified as public — the strict parser rejects it.v4.s_addr holds the address in network byte order (big-endian). On a little-endian machine the bytes are reversed relative to how we think of the number, so ntohl ("network to host long") flips them into host order. Only then do the shift-and-mask octet extractions in classify_v4 line up correctly.AF_INET6. "::1" succeeds here. We compare the parsed bytes against the library constant in6addr_loopback with memcmp; a zero result means it is the loopback address."300.1.1.1" lands.Trace for 172.16.5.4: parses as IPv4, ntohl gives host-order integer, a = 172, b = 16. First two checks fail; a == 172 && 16 <= 16 <= 31 is true → "private (172.16.0.0/12)". For 172.40.5.4, b = 40, that check fails and it falls through to "public".
Expected output:
192.168.1.10 IPv4 -> private (192.168.0.0/16)
10.0.0.5 IPv4 -> private (10.0.0.0/8)
172.16.5.4 IPv4 -> private (172.16.0.0/12)
172.40.5.4 IPv4 -> public (globally routable)
127.0.0.1 IPv4 -> loopback
169.254.13.7 IPv4 -> link-local (169.254.0.0/16)
8.8.8.8 IPv4 -> public (globally routable)
300.1.1.1 (not a valid IP literal)
::1 IPv6 -> loopback
Mistake 1 — treating 172.16/12 as 172.16/16.
// WRONG: only catches 172.16.x.x, misses 172.17 through 172.31
if (a == 172 && b == 16) return "private";
Why it is wrong: the private block is a /12, spanning 172.16 to 172.31. This code reports 172.20.5.4 as public. Corrected:
if (a == 172 && b >= 16 && b <= 31) return "private";
Recognise it by testing the boundaries: 172.15, 172.16, 172.31, 172.32. Only the middle two should be private.
Mistake 2 — parsing IPv4 by splitting on dots without validating range.
// WRONG: accepts 300.1.1.1 and 1.2.3.4.5
sscanf(s, "%d.%d.%d.%d", &a, &b, &c, &d);
Why it is wrong: sscanf does not check that each field is 0–255, does not reject extra trailing characters, and treats 1.2 as partially valid. Use inet_pton, which is strict, or validate every octet yourself.
Mistake 3 — forgetting byte order.
uint32_t ip = v4.s_addr; // WRONG on little-endian hosts
uint8_t first = (ip >> 24) & 0xFF; // gets the wrong octet
s_addr is network (big-endian) byte order. Skipping ntohl makes 127.0.0.1 look like 1.0.0.127. Corrected: uint32_t ip = ntohl(v4.s_addr);.
Mistake 4 — using :: twice in IPv6. Writing 2001::abcd::1 is invalid because the address becomes ambiguous. Use :: for only one zero run; write the shorter run out in full.
Compiler errors.
inet_pton / ntohl — you forgot #include <arpa/inet.h>.in6addr_loopback — same header; it declares the extern constant.Runtime / logic errors.
ntohl, so the octets are reversed. Print the raw integer in hex and compare with what you expect.inet_pton returns 0 for something you think is valid: check for stray whitespace or a trailing newline in the string (common when reading from fgets).AF_INET6, not AF_INET.Concrete steps when it misbehaves.
inet_pton (1, 0, or -1) so you know whether parsing or classification is at fault.printf("%u.%u.%u.%u\n", a, b, c, d) right after extraction.172.15.0.0, 172.16.0.0, 172.31.255.255, 172.32.0.0.Questions to ask when it doesn't work: Did I convert byte order? Am I using the strict parser? Are my range checks inclusive on both ends? Did the input string carry a hidden \n?
This topic sits right next to classic C memory bugs because address parsing means handling attacker-influenced strings.
inet_ntop, you must pass a buffer at least INET_ADDRSTRLEN (16) for IPv4 or INET6_ADDRSTRLEN (46) for IPv6. Undersizing it truncates or overflows. Never assume "an IP is at most 15 chars" — IPv6 blows past that.char buf[16] and strcpy an untrusted address into it. IPv6 literals are far longer; this is a textbook stack buffer overflow.memcmp. Comparing struct in6_addr with memcmp(&a, &b, sizeof a) is correct because the struct is a plain 16-byte array. Do not hardcode a length that could drift.0x7f.1 or 127.1 (which some legacy functions expand to 127.0.0.1) can be used to smuggle a loopback or internal address past a filter — a real-world SSRF and access-control bypass pattern. Always normalise and validate with inet_pton before making a trust decision, and compare against ranges numerically, not as strings.inet_pton returns 0 you must not read the in_addr/in6_addr — it may hold garbage. Branch on the return value first.Where this shows up.
127.0.0.1 (local-only) versus 0.0.0.0 (all interfaces) is a security decision made millions of times a day. A database bound to 0.0.0.0 and exposed to the internet is a classic breach cause.Best-practice habits.
inet_pton before trusting it; name variables for what they hold (host_order_ip, not x); handle the invalid-input branch explicitly.192.168.001.010 and 192.168.1.10 cannot be confused; be deliberate about IPv4-mapped IPv6 addresses (::ffff:192.168.1.10), which can bypass naive IPv4-only filters.1. (Beginner) Spot the private address. Given the list 8.8.8.8, 10.11.12.13, 172.32.0.1, 192.168.5.5, 172.20.0.9, write down which are private (RFC 1918) and which are public. Justify each using the range table. Concepts: RFC 1918 ranges, the 172.16/12 boundary. Hint: check the second octet carefully for the 172 addresses.
2. (Beginner) Compress an IPv6 address. Take 2001:0db8:0000:0000:0000:ff00:0042:8329 and write its shortest legal form. Then explain why 2001:db8::ff00::8329 would be illegal. Concepts: leading-zero removal, one-:: rule. Expected shortest form has exactly one ::.
3. (Intermediate) Octet extractor. Write a C function void print_octets(uint32_t host_ip) that prints the four octets of a host-order 32-bit address in a.b.c.d form using shifts and masks. Test it with 0xC0A8010A (should print 192.168.1.10). Concepts: bit shifting, masking, IPv4 as a uint32. Hint: shift by 24, 16, 8, 0 and mask with 0xFF.
4. (Intermediate) Strict validator. Write int is_valid_ipv4(const char *s) that returns 1 only for a strict dotted-quad (four fields, each 0–255, no leading zeros like 01, no trailing junk). Return 0 otherwise. Test with 192.168.1.10 (valid), 256.1.1.1, 1.2.3, 1.2.3.4.5, 01.2.3.4 (all invalid). Concepts: input validation, range checking. Hint: you may lean on inet_pton for the parse but you must still reject leading zeros yourself, since inet_pton is lenient about them on some platforms.
5. (Challenge) Range classifier with reachability report. Write a program that reads IP strings (one per line) and prints, for each, whether it is IPv4 or IPv6, its category (loopback / private / link-local / unspecified / public), and a one-word reachability verdict (local, internal, or internet). Handle both address families with inet_pton, reject invalid input gracefully, and make sure 172.31.255.255 is internal but 172.32.0.0 is internet. Concepts: everything in this lesson — parsing, byte order, range checks, IPv6 handling, defensive validation. Do not print a verdict for input that fails to parse.
uint32_t. IPv6 is 128 bits, written in hexadecimal with :: compressing one run of zero groups.10.0.0.0/8, 172.16.0.0/12 (second octet 16–31, the common trap), and 192.168.0.0/16; they are never routed on the public internet and rely on NAT to reach it.127.0.0.1, ::1) points back at the local machine; 0.0.0.0/:: mean "any"; 169.254.0.0/16 and fe80::/10 are link-local.inet_pton(AF_INET|AF_INET6, str, &addr) for strict parsing; ntohl to get host byte order before shifting out octets.172.16/12 as /16, skipping ntohl, trusting a lenient parser, and using :: twice.