Skip to content

Support scoped IPv6 addresses in SocketAddress.init(ipAddress:port:)#3525

Merged
glbrntt merged 8 commits intoapple:mainfrom
wendylabsinc:fix/ipv6-scoped-address-parsing
Mar 6, 2026
Merged

Support scoped IPv6 addresses in SocketAddress.init(ipAddress:port:)#3525
glbrntt merged 8 commits intoapple:mainfrom
wendylabsinc:fix/ipv6-scoped-address-parsing

Conversation

@mihai-chiorean
Copy link
Copy Markdown
Contributor

@mihai-chiorean mihai-chiorean commented Feb 26, 2026

Motivation

SocketAddress.init(ipAddress:port:) uses inet_pton to parse IP address strings. inet_pton does not support the %scope suffix used in IPv6 link-local addresses (e.g., fe80::1%eth0), causing these addresses to fail with failedToParseIPString. This forces downstream consumers like grpc-swift-nio-transport to reimplement scoped IPv6 parsing using getaddrinfo themselves.

Related: grpc/grpc-swift-nio-transport#147

Modifications

When the IP address string contains a % character, fall back to getaddrinfo with AI_NUMERICHOST instead of inet_pton. This properly parses the %scope suffix and sets sin6_scope_id in the resulting sockaddr_in6. The scoped path is extracted into a private _parseScopedIPv6 helper, guarded with #if !os(Windows) && !os(WASI). The non-scoped inet_pton path is completely unchanged.

Added 5 tests:

  • Named scope (fe80::1%lo) — verifies sin6_scope_id matches if_nametoindex
  • Numeric scope (fe80::1%1) — verifies numeric index parsing
  • Scoped vs non-scoped inequality — different sin6_scope_id means different addresses
  • Invalid scopes rejected — empty scope and nonexistent interface both throw
  • Non-scoped regression — ::1 still works with scope_id == 0

Result

SocketAddress(ipAddress: "fe80::1%eth0", port: 80) now succeeds and preserves the scope ID in sin6_scope_id. Non-scoped addresses continue to work exactly as before.

Context

This came up while working on IPv6 link-local support in grpc-swift-nio-transport#146, where the workaround was to use getaddrinfo directly in the gRPC layer. The reviewer suggested upstreaming this to swift-nio so the workaround can be removed.

Motivation:

`SocketAddress.init(ipAddress:port:)` uses `inet_pton` to parse IP
address strings. `inet_pton` does not support the `%scope` suffix used
in IPv6 link-local addresses (e.g., `fe80::1%eth0`), causing these
addresses to fail with `failedToParseIPString`. This forces downstream
consumers like grpc-swift-nio-transport to reimplement scoped IPv6
parsing using `getaddrinfo` themselves.

Modifications:

When the IP address string contains a `%` character, fall back to
`getaddrinfo` with `AI_NUMERICHOST` instead of `inet_pton`. This
properly parses the `%scope` suffix and sets `sin6_scope_id` in the
resulting `sockaddr_in6`. The scoped path is extracted into a private
`_parseScopedIPv6` helper, guarded with `#if !os(Windows) && !os(WASI)`.
The non-scoped `inet_pton` path is completely unchanged.

Added 5 tests: named scope (`%lo`), numeric scope (`%1`), scoped vs
non-scoped inequality, invalid scopes rejected, non-scoped regression.

Result:

`SocketAddress(ipAddress: "fe80::1%eth0", port: 80)` now succeeds and
preserves the scope ID in `sin6_scope_id`. Non-scoped addresses continue
to work exactly as before.
@Lukasa Lukasa added the 🔨 semver/patch No public API change. label Mar 3, 2026
macOS's getaddrinfo silently accepts empty scopes and nonexistent
interface names with scope_id == 0, unlike Linux which rejects them.
Add pre-validation for empty scope strings and post-validation that
scope_id is nonzero. Also make scoped IPv6 tests use the actual
loopback interface index instead of hardcoding 1.
@mihai-chiorean
Copy link
Copy Markdown
Contributor Author

I fixed the tests

@mihai-chiorean
Copy link
Copy Markdown
Contributor Author

@Lukasa mind kicking off CI? it should pass this time.

@glbrntt glbrntt enabled auto-merge (squash) March 6, 2026 08:45
@glbrntt glbrntt merged commit acf9bbe into apple:main Mar 6, 2026
51 of 55 checks passed
@mihai-chiorean
Copy link
Copy Markdown
Contributor Author

the previous failures don't seem related to this pr

glbrntt pushed a commit to grpc/grpc-swift-nio-transport that referenced this pull request Mar 17, 2026
## Summary

This PR preserves IPv6 scope IDs (e.g., `%eth0` in `fe80::1%eth0`) when
converting between gRPC and NIO socket address types.

**Updated**: Now significantly simplified since swift-nio natively
supports scoped IPv6 addresses as of
[apple/swift-nio#3525](apple/swift-nio#3525)
(merged March 6, 2026).

## Motivation

IPv6 link-local addresses (`fe80::/10`) require a scope ID to identify
which network interface to use. Without preservation of the scope ID,
`connect()` fails with `EINVAL` on link-local addresses.

## Changes

### Scope ID Preservation (NIO → gRPC)
In `NIOSocketAddress+GRPCSocketAddress.swift`, when converting from
NIO's `SocketAddress` to gRPC's `SocketAddress.IPv6`:
- Reads `sin6_scope_id` from the underlying `sockaddr_in6`
- Uses `if_indextoname()` to convert scope ID to interface name
- Appends the scope to the host string (e.g., `fe80::1` →
`fe80::1%eth0`)

### Simplified Scope ID Parsing (gRPC → NIO)
**Previously**: Used `getaddrinfo` with `AI_NUMERICHOST` to parse scoped
IPv6 addresses since `inet_pton` doesn't support `%scope` suffixes.

**Now**: Simply calls `SocketAddress.init(ipAddress:port:)` since
swift-nio now natively handles scoped IPv6 addresses.

This removes ~30 lines of workaround code and makes the implementation
much cleaner.

## Dependencies

**Requires**: swift-nio main branch (will change to a version
requirement once swift-nio releases version containing #3525)

Related:
- swift-nio PR: apple/swift-nio#3525
- Previous grpc-swift-nio-transport PR:
#146

## Testing

- ✅ Builds successfully with swift-nio main branch
- ✅ Existing tests in DNSResolverTests verify scope preservation
- ✅ NIOSocketAddressConversionTests verify bidirectional conversion
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 semver/patch No public API change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants