Systems · Lesson 26.5

IP addresses

Source
Read IPv4 and IPv6 addresses with IpAddress::Parse, write their canonical form with WriteText, tell loopback, private and public addresses apart, and read socket addresses — an address and a port — with SocketAddress::Parse. Text that is not an address is none.

Before two programs can talk over a network, one has to name the other. The name is an IP address, and once a port is added, a socket address. This lesson reads, writes and sorts both, with no network involved — they are plain values, and the next two lessons put them to work:

import Net::{ IpAddress, Ipv4Address, Ipv6Address, SocketAddress };

Two kinds of address

KindSizeWritten asExample
IPv44 bytesfour decimal numbers from 0 to 255192.168.1.20
IPv616 byteseight groups of hex digits; :: for one zero run2001:db8::1

IPv4 has room for about four billion addresses, which the world ran out of long ago; IPv6 has room for 3.4 × 10³⁸. Both are in use side by side, so Net has a type for each — Ipv4Address and Ipv6Address, whose octets are public arrays of bytes — and IpAddress, which holds either. IsIpv6(), AsIpv4() and AsIpv6() say which, the last two as optionals.

Reading and writing

IpAddress::Parse reads either kind, and returns IpAddress?: text that is not an address is none, with no reason attached. It is strict on purpose:

  • no leading zeros — 192.168.01.20 is refused, because some systems read 01 as octal;
  • no shortened IPv4 such as 127.1, which some C libraries quietly expand to 127.0.0.1;
  • no host names — example.com is a name to look up, not an address.

An address has no {} form. WriteText writes its one canonical form into a buffer and returns the length — at most 15 characters for IPv4 and 39 for IPv6 — so buffer[..length] is the text:

match IpAddress::Parse(text) {
    address? => {
        // An IPv6 address takes at most 39 characters, so the buffer always has room.
        var buffer: char8[39];
        let length = address.WriteText(buffer[..]);
        let version = address.IsIpv6() ? "IPv6" : "IPv4";
        PrintLine("{:<26} {} {:<16} {}", text, version, buffer[..length], Kind(address));
    },
    none => PrintLine("{:<26} not an IP address", text)
}

The canonical IPv6 form is lower case, drops leading zeros in each group, and shortens the longest run of zero groups to ::. So 2001:DB8:0:0:0:0:0:1 comes back as 2001:db8::1 — compare addresses, never their text.

Classifying

Some address ranges have a meaning of their own:

RangeMeaning
127.0.0.0/8, ::1loopback: this machine, never on the wire
10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16private: a home or office network, not the internet
fc00::/7IPv6's private range, "unique local"
fe80::/10link-local: only the cable or Wi-Fi you are on
0.0.0.0, ::unspecified: "every address of this machine" when listening

/8 means "the first 8 bits are fixed". IsLoopback is built in; the rest are read from the octets, which is where bitwise operators earn their keep — fc00::/7 fixes only the first seven bits, so the last bit of the first byte is masked off:

if (v6.octets[0] & 0xFE) == 0xFC { return "private network"; }
if v6.octets[0] == 0xFE && (v6.octets[1] & 0xC0) == 0x80 { return "link-local"; }

Everything else the program calls public, although 2001:db8::/32 — like 192.0.2.0/24 for IPv4 — is in fact set aside for examples in documentation, and never used on a real network.

Socket addresses

An IP address finds a machine; a port, a number from 0 to 65535, finds one program on it. Together they are a SocketAddress, with public ip and port fields. Its text is address:port, and an IPv6 address goes in brackets so that its colons are not mistaken for the port's — [::1]:443, never ::1:443:

let local = SocketAddress::New(IpAddress::FromIpv4(Ipv4Address::Localhost()), 3000);
let text = local.ToString(allocator)?;

ToString allocates a String, so it can fail with a TextError; WriteText into a buffer cannot. Equals compares two addresses — and an IPv4 and an IPv6 address are never equal, even 127.0.0.1 and ::1, which both mean this machine.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// Every machine on a network has an IP address. IPv4 writes one as four numbers from 0 to 255,
// `192.168.1.20`; IPv6, which has room for far more machines, writes 16 bytes as eight groups of
// hexadecimal digits, `2001:db8:0:0:0:0:0:1`, where `::` may stand for one run of zero groups:
// `2001:db8::1`. Add a port, the number that picks one program on that machine, and you have a
// socket address: `192.168.1.20:8080`, or `[2001:db8::1]:443`, with IPv6 in brackets so its
// colons are not mistaken for the port's.
//
// The `Net` package keeps these as plain values. `Parse` reads the text and returns an optional:
// text that is not an address is `none`. It is strict on purpose — no leading zeros, which some
// systems read as octal, and no shortened forms such as `127.1`. `WriteText` writes the one
// canonical form into a buffer, IPv6 in lower case with the longest run of zeros shortened.
import Allocator::{ Allocator, SystemAllocator };
import Io::PrintLine;
import Net::{ IpAddress, Ipv4Address, Ipv6Address, SocketAddress };
import Text::TextError;

// What kind of address it is. `IsLoopback` is built in; the private ranges are read from the
// octets, which are public fields.
func Kind(address: IpAddress) -> char8[..] {
    if address.IsLoopback() { return "loopback, this machine"; }
    match address.AsIpv4() {
        v4? => {
            let o = v4.octets;
            if v4.Equals(Ipv4Address::Unspecified()) { return "unspecified, any address"; }
            let ten = o[0] == 10;
            let oneSevenTwo = o[0] == 172 && o[1] >= 16 && o[1] <= 31;
            let oneNineTwo = o[0] == 192 && o[1] == 168;
            if ten || oneSevenTwo || oneNineTwo { return "private network"; }
            return "public";
        },
        none => {}
    }
    let v6 = address.AsIpv6() ?? Ipv6Address::Unspecified();
    if v6.Equals(Ipv6Address::Unspecified()) { return "unspecified, any address"; }
    if (v6.octets[0] & 0xFE) == 0xFC { return "private network"; }
    if v6.octets[0] == 0xFE && (v6.octets[1] & 0xC0) == 0x80 { return "link-local"; }
    return "public";
}

func Show(text: char8[..]) {
    match IpAddress::Parse(text) {
        address? => {
            // An IPv6 address takes at most 39 characters, so the buffer always has room.
            var buffer: char8[39];
            let length = address.WriteText(buffer[..]);
            let version = address.IsIpv6() ? "IPv6" : "IPv4";
            PrintLine("{:<26} {} {:<16} {}", text, version, buffer[..length], Kind(address));
        },
        none => PrintLine("{:<26} not an IP address", text)
    }
}

func ShowSocket(text: char8[..]) {
    match SocketAddress::Parse(text) {
        socket? => {
            var buffer: char8[47];
            let length = socket.WriteText(buffer[..]);
            PrintLine("{:<26} port {:<5} {}", text, socket.port, buffer[..length]);
        },
        none => PrintLine("{:<26} not a socket address", text)
    }
}

func Main() -> ! TextError {
    var system = SystemAllocator();
    let allocator: Allocator = system;

    Show("127.0.0.1");
    Show("192.168.1.20");
    Show("172.20.0.5");
    Show("8.8.8.8");
    Show("0.0.0.0");
    Show("::1");
    Show("2001:DB8:0:0:0:0:0:1");
    Show("fd12:3456::7");
    Show("fe80::1");
    Show("256.1.1.1");
    Show("192.168.01.20");
    Show("127.1");
    Show("example.com");

    ShowSocket("127.0.0.1:8080");
    ShowSocket("[::1]:443");
    ShowSocket("::1:443");
    ShowSocket("127.0.0.1:70000");
    ShowSocket("localhost:80");

    // Built from parts. `ToString` allocates the text, so it can fail.
    let local = SocketAddress::New(IpAddress::FromIpv4(Ipv4Address::Localhost()), 3000);
    let text = local.ToString(allocator)?;
    PrintLine("built from parts           {}", text);
    match SocketAddress::Parse("127.0.0.1:3000") {
        parsed? => PrintLine("same as 127.0.0.1:3000?    {}", local.Equals(parsed)),
        none => PrintLine("127.0.0.1:3000 did not parse")
    }

    // An IPv4 and an IPv6 address are never equal, even for the same machine:
    let four = IpAddress::FromIpv4(Ipv4Address::Localhost());
    let six = IpAddress::FromIpv6(Ipv6Address::Localhost());
    PrintLine("127.0.0.1 equals ::1?      {}", four.Equals(six));

    // An address is not text, and has no `{}` form of its own; `WriteText` is how it is shown.
    // And `Parse` may find no address, so its result is an optional:
    //
    //     PrintLine("{}", four);
    //         error: no matching overload for 'PrintLine' with argument types (char8[..], IpAddress)
    //
    //     let other: IpAddress = IpAddress::Parse("::1");
    //         error: cannot assign 'IpAddress?' to 'IpAddress'
}

Besides Io, its Rux.toml lists Allocator, Net and Text under [Dependencies].

Run it

cd Examples/Systems/IpAddress
rux run
127.0.0.1                  IPv4 127.0.0.1        loopback, this machine
192.168.1.20               IPv4 192.168.1.20     private network
172.20.0.5                 IPv4 172.20.0.5       private network
8.8.8.8                    IPv4 8.8.8.8          public
0.0.0.0                    IPv4 0.0.0.0          unspecified, any address
::1                        IPv6 ::1              loopback, this machine
2001:DB8:0:0:0:0:0:1       IPv6 2001:db8::1      public
fd12:3456::7               IPv6 fd12:3456::7     private network
fe80::1                    IPv6 fe80::1          link-local
256.1.1.1                  not an IP address
192.168.01.20              not an IP address
127.1                      not an IP address
example.com                not an IP address
127.0.0.1:8080             port 8080  127.0.0.1:8080
[::1]:443                  port 443   [::1]:443
::1:443                    not a socket address
127.0.0.1:70000            not a socket address
localhost:80               not a socket address
built from parts           127.0.0.1:3000
same as 127.0.0.1:3000?    true
127.0.0.1 equals ::1?      false

Common mistakes

Try it yourself

  1. Add 192.0.2.1 and 2001:db8::1 to a "documentation" class of their own.
  2. Show("100.64.0.1") prints public. Look up what 100.64.0.0/10 is for, and classify it.
  3. Parse a socket address, change its port field, and write it back with WriteText.
  4. Write a function that returns true when two socket addresses are on the same machine, whatever their ports.

Learn more

  • Net API — the address types and everything else in the package
  • Presence — matching address? and none
  • Bitwise — the masks behind every /n range
  • Bitwise operators in the Rux Reference
  • Next: TCP, which listens and connects at these addresses