Skip to content
Haiku OSDeep Dive Published Updated 6 min readViews unavailable

Haiku BNetworkAddress and Scoped IPv6: Preserve Interface Identity

Preserve IPv6 link-local scope IDs in Haiku BNetworkAddress, and audit sockaddr conversion, equality, and text formatting against current source.

A link-local IPv6 address is not a globally unique destination. The same literal, such as fe80::1, may be reachable on more than one local link, so an application must preserve the zone or interface context that gives the address meaning. The socket structure carries that context in sin6_scope_id; the address bytes alone are not a complete endpoint identity.

Haiku exposes both BNetworkAddress and BNetworkInterface, but the current source has several important boundaries: the public sockaddr_in6 includes sin6_scope_id; BNetworkAddress::SetTo(const sockaddr_in6&) copies the structure; ToString() formats the IPv6 address bytes without a zone suffix; and Equals() compares the address bytes and, by default, the port, but not the scope ID. Treat the convenience string and equality method as presentation/convenience operations, not as a complete key for scoped endpoints.

Build the structure explicitly

RFC 3493 describes sin6_scope_id as identifying the interface set appropriate to an address’s scope, while leaving the mapping implementation-specific. It also defines interface-index APIs and says interface indices are local identifiers. Haiku’s public BNetworkInterface::Index() returns an interface index, and Haiku’s BSD-layout sockaddr_in6 header provides sin6_scope_id. The following is a focused construction example for a target Haiku build: validate the interface, set the candidate scope explicitly, and test the resulting address with the exact socket operation your application needs.

#include <arpa/inet.h>
#include <NetworkAddress.h>
#include <NetworkInterface.h>
#include <netinet/in.h>

status_t MakeLinkLocalEndpoint(const char* interfaceName,
    BNetworkAddress& endpoint)
{
    if (interfaceName == NULL || interfaceName[0] == '\0')
        return B_BAD_VALUE;

    BNetworkInterface interface(interfaceName);
    if (!interface.Exists())
        return B_ERROR;

    const uint32 interfaceIndex = interface.Index();
    if (interfaceIndex == 0)
        return B_ERROR;

    sockaddr_in6 address = {};
    address.sin6_len = sizeof(address);
    address.sin6_family = AF_INET6;
    address.sin6_port = htons(443);
    address.sin6_flowinfo = 0;
    address.sin6_scope_id = interfaceIndex;

    if (inet_pton(AF_INET6, "fe80::1", &address.sin6_addr) != 1)
        return B_BAD_VALUE;

    endpoint.SetTo(address);
    return endpoint.InitCheck();
}

This creates an endpoint value; it does not prove that fe80::1 exists, that the interface is up, or that a TCP connection will succeed. The interface name and address are illustrative. RFC 3493 explicitly leaves the scope-ID mapping to the implementation, so test that the target Haiku release’s socket path interprets the chosen value as intended. Re-query the interface before using a long-lived saved value: an interface index is local system state, not a portable identifier to copy between machines or boots.

Haiku’s current NetworkAddress.cpp implementation copies an IPv6 sockaddr into its internal storage and sets the stored length; the sockaddr_in6 overload delegates to that path. This preserves a supplied scope ID in the object. Preserve the raw sockaddr_in6 or explicitly include the scope in an application-owned value when converting or serializing it; do not assume a helper method retains every field just because the original constructor did.

Do not key scoped endpoints by ToString() or Equals()

At the reviewed Haiku source revision, BNetworkAddress::ToString() calls inet_ntop() with only sin6_addr. It does not format sin6_scope_id, so two link-local endpoints with identical address and port but different interface scopes can produce the same text. BNetworkAddress::Equals() first checks family and optionally port, then for IPv6 compares only sin6_addr; it does not compare sin6_scope_id. operator== delegates to that method. A map keyed by ToString(), or a deduplication check using Equals(), can therefore merge distinct link-local endpoints on this implementation.

For identity-sensitive code, compare the full tuple you actually use: address family, IPv6 address, scope ID, and port. If the application distinguishes transport protocols, include that too; a TCP and UDP endpoint with the same address and port are not the same service. A small helper for raw structures makes the intended rule explicit:

#include <cstring>

bool SameScopedEndpoint(const sockaddr_in6& left,
    const sockaddr_in6& right)
{
    return left.sin6_family == AF_INET6
        && right.sin6_family == AF_INET6
        && left.sin6_scope_id == right.sin6_scope_id
        && left.sin6_port == right.sin6_port
        && std::memcmp(&left.sin6_addr, &right.sin6_addr,
            sizeof(left.sin6_addr)) == 0;
}

The helper is intentionally strict about scope: scope zero and a nonzero scope do not collapse to the same key. Whether a particular socket operation can infer a missing scope is a separate runtime question; do not make the key permissive based on that behavior. If you need to display the endpoint, show the address plus a separately resolved interface label or an explicitly generated local zone identifier. Do not use that display string as a durable machine-independent serialization format.

There is a second source-level caution. In the reviewed Haiku kernel IPv6 source, ipv6_hash_address() has a TODO asking whether sin6_flowinfo and sin6_scope_id should be included; the current helper hashes the IPv6 bytes and, when requested, the port. That function is not proof that every routing or socket path ignores scope, and this article does not claim that. It is a concrete reason to test any code that depends on hash-based distinction between otherwise identical scoped addresses rather than assuming the structure field is honored uniformly everywhere.

Test the boundary on the target release

Use two configured links that can each reach the same link-local literal, where the test environment allows it. Construct otherwise-identical sockaddr_in6 values with different scope IDs, then verify each layer independently: BNetworkAddress::SetTo() preserves the supplied field; the application’s own endpoint key distinguishes it; a display path reports the selected interface; and the exact bind(), connect(), or sendto() operation reaches only the expected link. Record the Haiku build, interface names and indices, socket type, operation, and returned status. Do not treat a unit test of the helper as proof of kernel routing behavior.

Also test interface removal and recreation, a zero scope ID, a stale saved index, malformed numeric input, and a global-scope address. A link-local-specific workaround should not silently alter global IPv6 endpoints. If the application accepts a textual zone suffix, parse and validate it under the target platform’s rules, resolve it to current interface state, and keep both the original user input and normalized endpoint separate for diagnostics.

Keep interface identity local. RFC 9844, which updates RFC 4007, states that zone identifiers have meaning only on the node where they are defined. A numeric index can differ on another host. Persist a user-facing interface choice or configuration policy, then resolve and verify it on the current machine; do not transmit a Haiku interface index as if it were globally meaningful. For logs, include a human-readable interface name next to the address, redact sensitive peer data where appropriate, and make clear whether the printed string is a display value or a complete endpoint key.

The practical rule is simple: preserve the full socket structure, but do not assume Haiku’s convenience formatting or equality methods preserve the scope dimension. Verify actual socket behavior on the supported Haiku release, and make application-level endpoint identity explicit wherever link-local traffic is involved.

Related:

Sources:

Comments