DEV Community

K@zuki.
K@zuki.

Posted on

Subtle but Important: IPAddr Behavior Change in Ruby 3.1

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}"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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...
]
Enter fullscreen mode Exit fullscreen mode

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)