Conversation
- 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>
3bbc6c5 to
294a9db
Compare
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
lib/sparoid.rb
Outdated
| 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 | ||
|
|
There was a problem hiding this comment.
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.
| 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| |
| 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") |
There was a problem hiding this comment.
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.
| spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0") | |
| spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0") |
| end | ||
| end | ||
| rescue ArgumentError => e | ||
| return write_cache if /cannot interpret as IPv4 address/.match?(e.message) |
There was a problem hiding this comment.
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.
| 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) |
| "ipv6.icanhazip.com", | ||
| "ipv4.icanhazip.com" |
There was a problem hiding this comment.
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.
| "ipv6.icanhazip.com", | |
| "ipv4.icanhazip.com" | |
| "ipv4.icanhazip.com", | |
| "ipv6.icanhazip.com" |
| 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
oskgu360
left a comment
There was a problem hiding this comment.
✅ Do as you please with comments, nothing of to high importance imo :)
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>
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