IP addresses
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
| Kind | Size | Written as | Example |
|---|---|---|---|
| IPv4 | 4 bytes | four decimal numbers from 0 to 255 | 192.168.1.20 |
| IPv6 | 16 bytes | eight groups of hex digits; :: for one zero run | 2001: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.20is refused, because some systems read01as octal; - no shortened IPv4 such as
127.1, which some C libraries quietly expand to127.0.0.1; - no host names —
example.comis 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:
| Range | Meaning |
|---|---|
127.0.0.0/8, ::1 | loopback: this machine, never on the wire |
10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 | private: a home or office network, not the internet |
fc00::/7 | IPv6's private range, "unique local" |
fe80::/10 | link-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.
// 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
- Add
192.0.2.1and2001:db8::1to a "documentation" class of their own. Show("100.64.0.1")prints public. Look up what100.64.0.0/10is for, and classify it.- Parse a socket address, change its
portfield, and write it back withWriteText. - Write a function that returns
truewhen 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?andnone - Bitwise — the masks behind every
/nrange - Bitwise operators in the Rux Reference
- Next: TCP, which listens and connects at these addresses
26.4 Log
Write structured log records with a Logger: a level, a message and typed Fields, as text or as JSON lines, with levels that filter, context fields on every record, and the process-wide log on standard error.
26.6 TCP
Run a TCP server and client in one program: a TcpListener bound to an ephemeral port on the loopback address accepts a TcpStream, a client thread connects and sends lines, the server answers each, and Shutdown tells the server the client has finished.