Have you ever encountered a situation where your Ruby code works perfectly fine in one version but behaves differently in another?
Today, let's dive into a subtle yet important change in Ruby's IPAddr
class that occurred in version 3.1.
TL;DR
- The behavior of
IPAddr
changed in Ruby 3.1. - IPv4-mapped IPv6 addresses (e.g.,
::ffff:127.0.0.1
) are no longer considered equivalent to their IPv4 counterparts when compared to IPv4 ranges. - This change can affect code that deals with IP address ranges, particularly in network configurations or access control lists.
- A related issue affecting Rails'
TRUSTED_PROXIES
behavior has been reported: Rails Issue #52862
The Change
In Ruby versions prior to 3.1, when comparing an IPv4 range with an IPv4-mapped IPv6 address, the comparison would return true
if the IPv4 part of the address was within the range.
However, this behavior changed in Ruby 3.1.
Let's look at a concrete example:
require 'ipaddr'
ipv4_range = IPAddr.new('127.0.0.0/8')
ipv4_address = '127.0.0.1'
ipv4_mapped_address = '::ffff:127.0.0.1'
puts "Ruby version: #{RUBY_VERSION}"
puts "IPv4 Range === IPv4 Address: #{ipv4_range === ipv4_address}"
puts "IPv4 Range === IPv4-mapped Address: #{ipv4_range === ipv4_mapped_address}"
Running this code in Ruby 3.0 gives us:
# IPv4 Range === IPv4 Address
> IPAddr.new('127.0.0.0/8') === '127.0.0.1'
true
# IPv4 Range === IPv4-mapped Address:
> IPAddr.new('127.0.0.0/8') === '::ffff:127.0.0.1'
true
But in Ruby 3.1 or later:
# IPv4 Range === IPv4 Address
> IPAddr.new('127.0.0.0/8') === '127.0.0.1'
true
# IPv4 Range === IPv4-mapped Address:
> IPAddr.new('127.0.0.0/8') === '::ffff:127.0.0.1'
false
Why Did This Change?
This change was introduced in the ipaddr
gem, which is a bundled gem in Ruby.
The specific commit that introduced this change can be found here.
Implications
While this change makes the behavior more technically correct, it can lead to unexpected results in existing code that relies on the previous behavior.
This is particularly relevant for applications that deal with IP address ranges, such as those handling network configurations or access control lists.
For instance, if you're using IPAddr
to check if an IP is within a trusted range, you might now need to explicitly handle IPv4-mapped addresses separately.
FYI: https://github.com/rails/rails/issues/52862
How to Handle This in Your Rails Code
If you're using Rails and configuring trusted proxies, you might need to adjust your configuration to account for this change.
Here's how you can handle both IPv4 and IPv4-mapped IPv6 addresses:
config.action_dispatch.trusted_proxies = ActionDispatch::RemoteIp::TRUSTED_PROXIES + [
# IPv4-mapped IPv6 loopback
IPAddr.new('::ffff:127.0.0.0/104'),
# IPv4-mapped IPv6 private network
IPAddr.new('::ffff:10.0.0.0/104'),
# other trusted proxies...
]
By explicitly including both IPv4 and IPv4-mapped IPv6 versions of your trusted proxy ranges, you ensure that your Rails application will correctly identify trusted proxies regardless of how they're represented.
Remember to test your configuration thoroughly, especially if you're upgrading from an older version of Ruby or Rails.
Conclusion
This change in IPAddr
behavior serves as a reminder of the importance of thorough testing across different Ruby versions, especially when dealing with core functionalities like network addressing.
It also highlights the ongoing efforts in the Ruby community to improve consistency and correctness, even if it sometimes means breaking changes in minor version updates.
Have you encountered any surprising behavior changes in your Ruby upgrades?
How do you usually handle such situations?
Share your experiences in the comments!
Top comments (0)