Skip to content

Add IPv6 support#16

Merged
antondalgren merged 12 commits intomainfrom
ipv6-support
Feb 19, 2026
Merged

Add IPv6 support#16
antondalgren merged 12 commits intomainfrom
ipv6-support

Conversation

@antondalgren
Copy link
Contributor

@antondalgren antondalgren commented Feb 9, 2026

WHY are these changes introduced?

Ipv6 support

WHAT is this pull request doing?

Implements MessageV2 format for IPv6.

HOW was this pull request tested?

Specs and manuallly by sending auth requests to server with v1 and v2 support 84codes/sparoid#17

- Add message_v2 format supporting both IPv4 and IPv6 with prefix length
- Resolve and send to both IPv4 and IPv6 addresses
- Detect local global IPv6 addresses with their prefix length
- Sort messages by size so v1 messages are sent first for backward compatibility
- Update tests to reflect new API (public_ip returns array)
- Add tests for message_v2, string_to_ip, create_messages, key validation, keygen
- Bump minimum Ruby version to 3.2

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@antondalgren antondalgren requested a review from a team as a code owner February 9, 2026 13:53
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements IPv6 support for the Sparoid single packet authorization client by introducing a new MessageV2 format that includes IP family and prefix information. The implementation enables dual-stack operation, allowing the client to send authorization packets over both IPv4 and IPv6 networks.

Changes:

  • Implemented MessageV2 format supporting both IPv4 and IPv6 addresses with CIDR range information
  • Added dual-stack public IP resolution via both ipv6.icanhazip.com and ipv4.icanhazip.com
  • Updated minimum Ruby version requirement from 2.5.0 to 3.2.0

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
lib/sparoid.rb Core implementation of IPv6 support including message_v2 format, dual-stack public IP resolution, local IPv6 interface detection, and updated auth flow to handle multiple IP addresses
lib/sparoid/cli.rb Added backtrace output for debugging errors
test/sparoid_test.rb Added comprehensive tests for IPv6 functionality including message_v2 format, string_to_ip conversion, and message generation; updated existing tests to work with array-based IP returns
test/test_helper.rb Added fileutils require for test utilities
sparoid.gemspec Raised minimum Ruby version from 2.5.0 to 3.2.0
.rubocop.yml Updated target Ruby version to 3.2 and disabled MethodLength cop
.github/workflows/main.yml Updated CI matrix to test Ruby 3.2, 3.3, 3.4, 4.0, and ruby-head

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +261 to +273
def public_ipv6_with_range
global_ipv6_ifs = Socket.getifaddrs.select do |addr|
addrinfo = addr.addr
addrinfo&.ipv6? && global_ipv6?(addrinfo)
end

global_ipv6_ifs.map do |iface|
addrinfo = iface.addr
netmask_addr = IPAddr.new(iface.netmask.ip_address)
prefixlen = netmask_addr.to_i.to_s(2).count("1")
next addrinfo.ip_address, prefixlen
end
end
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Socket.getifaddrs method may not be available on all platforms (it was added in Ruby 2.1 but behavior can vary). Additionally, the structure and availability of netmask information can differ across operating systems. Consider adding error handling for cases where getifaddrs might not work as expected, or at minimum documenting the platform requirements.

Copilot uses AI. Check for mistakes.
lib/sparoid.rb Outdated
Comment on lines +93 to +102
ipv6_added = false
public_ipv6_with_range.each do |addr, prefixlen|
ipv6 = Resolv::IPv6.create(addr)
messages << message_v2(ipv6, prefixlen)
ipv6_added = true
end

cached_public_ip.each do |ip|
next if ip.is_a?(Resolv::IPv6) && ipv6_added

Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic next if ip.is_a?(Resolv::IPv6) && ipv6_added skips ALL IPv6 addresses from the cached public IP if ANY local IPv6 address was found via public_ipv6_with_range. This could be problematic because the public IPv6 address obtained from the HTTP check (e.g., from icanhazip.com) might be different from the local interface IPv6 addresses (e.g., due to NAT66 or privacy extensions). Both should potentially be included. Consider a more nuanced deduplication strategy or removing this filter entirely.

Suggested change
ipv6_added = false
public_ipv6_with_range.each do |addr, prefixlen|
ipv6 = Resolv::IPv6.create(addr)
messages << message_v2(ipv6, prefixlen)
ipv6_added = true
end
cached_public_ip.each do |ip|
next if ip.is_a?(Resolv::IPv6) && ipv6_added
public_ipv6_with_range.each do |addr, prefixlen|
ipv6 = Resolv::IPv6.create(addr)
messages << message_v2(ipv6, prefixlen)
end
cached_public_ip.each do |ip|

Copilot uses AI. Check for mistakes.
spec.homepage = "https://github.com/84codes/sparoid.rb"
spec.license = "MIT"
spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The minimum Ruby version requirement is being raised from 2.5.0 to 3.2.0, which is a significant jump that will exclude users on older Ruby versions. While Ruby 2.5 is end-of-life, this change should be clearly documented as a breaking change. If there are specific Ruby 3.2+ features being used that necessitate this bump, they should be documented. Otherwise, consider whether a less restrictive minimum version (e.g., 3.0 or 3.1) might be more appropriate to maximize compatibility.

Suggested change
spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")

Copilot uses AI. Check for mistakes.
end
end
rescue ArgumentError => e
return write_cache if /cannot interpret as IPv4 address/.match?(e.message)
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message check /cannot interpret as IPv4 address/ is now outdated since the cache can contain IPv6 addresses. When string_to_ip fails to parse an IPv6 address, it will raise "Unsupported IP format" which won't match this pattern. Consider updating the error handling to be more generic or handle both IPv4 and IPv6 parsing errors appropriately.

Suggested change
return write_cache if /cannot interpret as IPv4 address/.match?(e.message)
return write_cache if /cannot interpret as IPv4 address|Unsupported IP format/.match?(e.message)

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +17
"ipv6.icanhazip.com",
"ipv4.icanhazip.com"
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The order of URLs in the URLS array puts IPv6 first (ipv6.icanhazip.com) before IPv4 (ipv4.icanhazip.com). If the system doesn't have IPv6 connectivity, the IPv6 request will timeout (taking up to 5 seconds), causing unnecessary delay before falling back to IPv4. Consider reordering to prioritize IPv4 first for better performance on IPv4-only systems, or implementing parallel requests to both endpoints.

Suggested change
"ipv6.icanhazip.com",
"ipv4.icanhazip.com"
"ipv4.icanhazip.com",
"ipv6.icanhazip.com"

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +28
addrs.each do |addr|
messages = generate_messages(open_for_ip)
data = messages.map do |message|
prefix_hmac(hmac_key, encrypt(key, message))
end
sendmsg(addr, data)
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generate_messages(open_for_ip) call on line 24 is inside the addrs.each loop, causing the same messages to be regenerated for each address. Since the messages don't depend on the specific address being sent to, this is inefficient. Consider moving the message generation outside the loop to generate them once and reuse for all addresses.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@oskgu360 oskgu360 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noice!

Gonna give it a spin of office (noipv6) + home (native ipv6) before the ✅ But looks good, some minor style stuff in comments

  • Office (tested of both new sparoid-server and old)
Feb 16 13:51:08 test-windy-hawk-01 sparoid-server[398023]: Running nftables command for 185.153.213.88
Feb 16 13:51:08 test-windy-hawk-01 sparoid-server[398023]: [::ffff:185.153.213.88]:53420 packet accepted ip=185.153.213.88
Feb 16 13:51:08 test-windy-hawk-01 sparoid-server[398023]: Running nftables command for 185.153.213.88/32
Feb 16 13:51:08 test-windy-hawk-01 sparoid-server[398023]: [::ffff:185.153.213.88]:53420 packet accepted ip=185.153.213.88/32
Feb 16 13:51:08 test-windy-hawk-01 sparoid-server[398023]: Running nftablesv6 command for fd14:b1bb:b165:402b:1024:d935:faf6:5ff8/64
Feb 16 13:51:08 test-windy-hawk-01 sparoid-server[398023]: [::ffff:185.153.213.88]:53420 packet accepted ip=fd14:b1bb:b165:402b:1024:d935:faf6:5ff8/64
  • Home (ipv6 native)
17 06:30:06 test-windy-hawk-01 sparoid-server[691471]: Running nftables command for 90.224.127.58
Feb 17 06:30:06 test-windy-hawk-01 sparoid-server[691471]: [2001:2042:3427:cf80:d48a:75c7:6cf5:2373]:58853 packet accepted ip=90.224.127.58
Feb 17 06:30:06 test-windy-hawk-01 sparoid-server[691471]: Running nftables command for 90.224.127.58/32
Feb 17 06:30:06 test-windy-hawk-01 sparoid-server[691471]: [2001:2042:3427:cf80:d48a:75c7:6cf5:2373]:58853 packet accepted ip=90.224.127.58/32
Feb 17 06:30:06 test-windy-hawk-01 sparoid-server[691471]: Running nftablesv6 command for 2001:2042:3427:cf80:00e5:41ab:d69d:b433/64
Feb 17 06:30:06 test-windy-hawk-01 sparoid-server[691471]: [2001:2042:3427:cf80:d48a:75c7:6cf5:2373]:58853 packet accepted ip=2001:2042:3427:cf80:00e5:41ab:d69d:b433/64
Feb 17 06:30:06 test-windy-hawk-01 sparoid-server[691471]: Running nftablesv6 command for 2001:2042:3427:cf80:d48a:75c7:6cf5:2373/64
Feb 17 06:30:06 test-windy-hawk-01 sparoid-server[691471]: [2001:2042:3427:cf80:d48a:75c7:6cf5:2373]:58853 packet accepted ip=2001:2042:3427:cf80:d48a:75c7:6cf5:2373/64
Feb 17 06:30:06 test-windy-hawk-01 sparoid-server[691471]: Running nftables command for 90.224.127.58
Feb 17 06:30:06 test-windy-hawk-01 sparoid-server[691471]: [::ffff:90.224.127.58]:52655 packet accepted ip=90.224.127.58
Feb 17 06:30:06 test-windy-hawk-01 sparoid-server[691471]: Running nftablesv6 command for 2001:2042:3427:cf80:00e5:41ab:d69d:b433/64
Feb 17 06:30:06 test-windy-hawk-01 sparoid-server[691471]: [::ffff:90.224.127.58]:52655 packet accepted ip=2001:2042:3427:cf80:00e5:41ab:d69d:b433/64
Feb 17 06:30:06 test-windy-hawk-01 sparoid-server[691471]: Running nftables command for 90.224.127.58/32
Feb 17 06:30:06 test-windy-hawk-01 sparoid-server[691471]: [::ffff:90.224.127.58]:52655 packet accepted ip=90.224.127.58/32
Feb 17 06:30:06 test-windy-hawk-01 sparoid-server[691471]: Running nftablesv6 command for 2001:2042:3427:cf80:d48a:75c7:6cf5:2373/64
Feb 17 06:30:06 test-windy-hawk-01 sparoid-server[691471]: [::ffff:90.224.127.58]:52655 packet accepted ip=2001:2042:3427:cf80:d48a:75c7:6cf5:2373/64

Copy link
Contributor

@oskgu360 oskgu360 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Do as you please with comments, nothing of to high importance imo :)

antondalgren and others added 6 commits February 19, 2026 14:20
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@antondalgren antondalgren merged commit 291a94b into main Feb 19, 2026
8 checks passed
@antondalgren antondalgren deleted the ipv6-support branch February 19, 2026 13:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants