I need to store IPv6 addresses in a Mongodb database using Rails3 + Mongoid.
There will also (mostly) be IPv4 addresses in the collection.
I need addresses to be stored as decimals since I have to query for addresses that belongs to a network (I'll store networks and addresses in distinct collections).
I've used BigDecimals to store those addresses (since IPv6 addresses are 128bit long) but when I try to find which addresses belongs to a network (concretely: between network and broadcast addresses), I don't find any working solution.
Mongoid "gte" and "lte" only seems to work on integers (BigDecimals are actually strings) and returns an empty list, and I don't find a way to query my mongoid model for a string range.
MongoDB seems to allow this (http://www.mongodb.org/display/DOCS/min+and+max+Query+Specifiers) but I don't find the corresponding syntax in mongoid doc.
Queries like the following raises awful "db assertion failure" :
Network.min(address: ip.network.to_i.to_s).max(address: ip.broadcast.to_i.to_s)
"ip.to_i.to_s" provides a string representation of the decimal address since I'm using IPAddress gem.
Same error with just 'to_i' or "BigDecimal.new(ip.network.to_i)"
The other solution is to store v6 addresses in 2 64bit integers but it's far more complex for range queries and I'd like to use the same behavior for v6 and v4 addresses.
Does anyone have experience on a clean way to deal with IPv6 addresses queries in a database ?
Here is my current Network model :
class Network
# INCLUSIONS
include Mongoid::Document
include Mongoid::Timestamps
# RELATIONS
belongs_to :vlan
# FIELDS
field :description
field :address, type: BigDecimal
field :prefix, type: Integer
field :v6, type: Boolean
field :routed, type: Boolean
# VALIDATIONS
validates :ip,
presence: true
# Address must be a valid IP address
validate :ip do
errors.add(:ip, :invalid) unless ip? && ip == ip.network
end
# INSTANCE METHODS
# Returns string representation of the address
def address
ip.to_s if ip
end
def address= value
raise NoMethodError, 'address can not be set directly'
end
# Provides the IPAddress object
def ip
unless @ip.is_a?(IPAddress) || self[:address].blank?
# Generate IP address
if self[:v6]
@ip = IPAddress::IPv6.parse_u128 self[:address].to_i
else
@ip = IPAddress::IPv4.parse_u32 self[:address].to_i
end
# Set IP prefix
@ip.prefix = self[:prefix] if self[:prefix]
end
@ip
end
# Sets network IP
def ip= value
value = value.to_s
@ip = value
self[:address] = nil
self[:prefix] = nil
self[:v6] = nil
begin
@ip = IPAddress value
self[:address] = @ip.to_i
self[:prefix] = @ip.prefix
self[:v6] = @ip.ipv6?
rescue
end
end
# Whether IP is a IPAddress object
def ip?
ip.is_a? IPAddress
end
# Provides network prefix
def prefix
return ip.prefix if ip?
self[:prefix]
end
def prefix= value
raise NoMethodError, 'prefix can not be set directly'
end
# Provides string representation of the network
def to_s
ip? ? ip.to_string : @ip.to_s
end
def subnets
networks = Network.min(address: ip.network.to_i.to_s).max(address: ip.broadcast.to_i.to_s)
return networks
end
end
Subnets method is the one I'm working on, to detect networks nested into the current one.
Note that I'd like to avoid "strong" db relations between networks/subnets and upcoming hosts addresses to keep them dynamic.
Update:
Here's my final class that's working fine to manage nested IP networks.
Addresses are stored as fixed length strings in hexadecimal. They can be stored in base 32 to match real addresses size but hexa is better for readability.
Subnets method provides a list of all subnets of the current network.
class Network
# INCLUSIONS
include Mongoid::Document
include Mongoid::Timestamps
# RELATIONS
belongs_to :vlan
# FIELDS
field :description
field :address, type: String
field :prefix, type: Integer
field :routed, type: Boolean
field :v6, type: Boolean
# VALIDATIONS
validates :ip,
presence: true
# Address must be a valid IP address
validate do
errors.add(:ip, :invalid) unless ip? && ip == ip.network
end
validate do
errors.add(:ip, :prefix_invalid_v6) if ip && ip.ipv6? && (self[:prefix] < 0 || self[:prefix] > 64)
end
# INSTANCE METHODS
# Returns string representation of the address
def address
ip.to_s if ip
end
def address= value
raise NoMethodError, 'address can not be set directly'
end
# Provides the IPAddress object
def ip
unless @ip.is_a?(IPAddress) || self[:address].blank?
# Generate IP address
if v6
@ip = IPAddress::IPv6.parse_u128 self[:address].to_i(16)
else
@ip = IPAddress::IPv4.parse_u32 self[:address].to_i(16)
end
# Set IP prefix
@ip.prefix = self[:prefix] if self[:prefix]
end
@ip
end
# Sets network IP
def ip= value
value = value.to_s
@ip = value
self[:address] = nil
self[:prefix] = nil
self[:v6] = nil
begin
@ip = IPAddress value
self[:address] = @ip.to_i.to_s(16).rjust((@ip.ipv4? ? 8 : 32), '0')
self[:prefix] = @ip.prefix
self[:v6] = @ip.ipv6?
rescue
end
end
# Whether IP is a IPAddress object
def ip?
ip.is_a? IPAddress
end
# Provides network prefix
def prefix
return ip.prefix if ip?
self[:prefix]
end
def prefix= value
raise NoMethodError, 'prefix can not be set directly'
end
# Provides string representation of the network
def to_s
ip? ? ip.to_string : @ip.to_s
end
# Provides nested subnets list
def subnets
length= ip.ipv4? ? 8 : 32
networks = Network.where(
v6: v6,
:address.gte => (ip.network.to_i.to_s(16)).rjust(length, '0'),
:address.lte => (ip.broadcast.to_i.to_s(16).rjust(length, '0')),
:prefix.gte => ip.prefix
).asc(:address).asc(:prefix)
end
end