Networking Fundamentals · beginner · ~10 min

IP addresses: IPv4, IPv6, public vs private

- 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.

Overview

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.

Why it matters

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.

Core concepts

1. The IPv4 address and the octet

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?

2. Private ranges (RFC 1918)

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.010.255.255.255 ~16.7 million
172.16.0.0/12 172.16.0.0172.31.255.255 ~1 million
192.168.0.0/16 192.168.0.0192.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.255 public or private? What about 172.32.0.1? Which single octet decides it?

3. Loopback and other special addresses

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.1 never appear on the physical network?

4. IPv6 and zero-compression

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.

  1. Leading zeros in each group may be dropped: 0db8db8, 00000.
  2. Exactly one run of consecutive all-zero groups may be replaced by ::.

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 :: in fe80::1 stand for?

Syntax notes

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.

Lesson

An IP address identifies a host on a network at the Internet layer.

IPv4

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.

Private ranges (RFC 1918)

These ranges are not routable on the public internet. They are used inside homes and offices:

  • 10.0.0.0/8
  • 172.16.0.0/12
  • 192.168.0.0/16

Special addresses

  • 127.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).

IPv6

An IPv6 address is 128 bits, written as eight groups of hexadecimal digits:

2001:0db8:85a3::8a2e:0370:7334

  • The double colon :: compresses one run of all-zero groups.
  • Loopback is ::1.
  • Link-local addresses start with fe80::.

Public vs private

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.

Code examples

#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;

}

Line by line

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.16172.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:

  1. 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.
  2. On success, 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.
  3. If IPv4 parsing fails, we try 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.
  4. Anything that parses as neither prints the "not a valid IP literal" branch — this is where "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

Common mistakes

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.

Debugging tips

Compiler errors.

  • implicit declaration of inet_pton / ntohl — you forgot #include <arpa/inet.h>.
  • unknown type in6addr_loopback — same header; it declares the extern constant.

Runtime / logic errors.

  • Every address comes back as "public": you almost certainly skipped 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).
  • IPv6 addresses fail to parse: verify you passed AF_INET6, not AF_INET.

Concrete steps when it misbehaves.

  1. Print the return value of inet_pton (1, 0, or -1) so you know whether parsing or classification is at fault.
  2. Print octets individually: printf("%u.%u.%u.%u\n", a, b, c, d) right after extraction.
  3. Test boundary inputs on purpose: 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?

Memory safety

This topic sits right next to classic C memory bugs because address parsing means handling attacker-influenced strings.

  • Buffer sizes for text conversion. When converting the other direction with 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.
  • Never build a fixed char buf[16] and strcpy an untrusted address into it. IPv6 literals are far longer; this is a textbook stack buffer overflow.
  • Struct sizes with 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.
  • Strict validation is a security control. A lenient parser that accepts 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.
  • Uninitialised structs. If inet_pton returns 0 you must not read the in_addr/in6_addr — it may hold garbage. Branch on the return value first.

Real-world uses

Where this shows up.

  • Operating systems & servers: binding a service to 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.
  • Networking: routers, firewalls, and load balancers match traffic against ranges exactly like the ones in this lesson; NAT exists precisely to bridge RFC 1918 private space to public space.
  • Security / recon: the first step of an assessment is sorting targets into public (directly reachable) and private (needs a foothold), and excluding out-of-scope and loopback addresses.
  • Cloud: every VPC uses RFC 1918 ranges internally; understanding them is required to design a network.

Best-practice habits.

  • Beginner: always validate input with inet_pton before trusting it; name variables for what they hold (host_order_ip, not x); handle the invalid-input branch explicitly.
  • Advanced: store addresses in their binary form and compare numerically with masks; treat any address that reaches a trust boundary as untrusted until normalised; log addresses in a canonical form so 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.

Practice tasks

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.

Summary

  • An IPv4 address is 32 bits, written as four octets (each 0–255); underneath it is a single uint32_t. IPv6 is 128 bits, written in hexadecimal with :: compressing one run of zero groups.
  • The RFC 1918 private ranges are 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.
  • Loopback (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.
  • Key syntax: inet_pton(AF_INET|AF_INET6, str, &addr) for strict parsing; ntohl to get host byte order before shifting out octets.
  • Common mistakes: treating 172.16/12 as /16, skipping ntohl, trusting a lenient parser, and using :: twice.
  • Remember: the public-vs-private distinction answers the first question in networking and recon alike — is this address reachable, and from where? Always validate untrusted addresses with a strict parser before making a trust decision.

Practice with these exercises