I’ve been moving visualchat.co.uk to ruby on rails recently and one of the things they have to do is block certain ip addresses, in fact, entire ranges of ip addresses, as they get a lot of naughty mischievous chilldren (read as fucktards) creating havoc.
I tinkered about a bit and then asked on #ruby-lang where one David Black answered the call. The code he gave me is my favourite bit of ruby code ever. I think his method of comparing ip addresses is so very nice it’s unbelievable.
He told me to create an ip address class, which includes the comparable mixin (see here for more info).
Like this:
- module Infrid
- class IPAddress
- include Comparable
- def initialize(address)
- @address = address
- end
- def split
- @address.split(‘.‘).map {|s| s.to_i }
- end
- def <=>(other)
- split <=> other.split
- end
- def to_s
- @address
- end
- end
- end
Note that I put this code in my own Infrid module which I store in lib/Infrid of my rails app.
Now you can create ip address objects, and do code like this.
- ip_address_a = IPAddress.new(‘127.0.0.1‘)
- ip_address_b = IPAddress.new(‘126.0.0.1‘)
- ip_address_c = IPAddress.new(‘127.4.34.3‘)
- ip_address_a < ip_address_b
- >> true
- ip_address_a > ip_address_c
- >> true
- ip_address_a.between ip_address_b, ip_address_c
- >> true
How cool is that?
I then use this in my rails app to check if an ip address is banned. I have a banned_ip model which keeps ranges of ip’s using first_ip and last_ip.
It looks like this:
- class BannedIp < ActiveRecord::Base
- @banned_ips # hash of ips and masks
- validates_presence_of :first_ip, :message =>"first address is needed"
- validates_presence_of :last_ip, :message =>"last address is needed"
- validates_format_of :first_ip, :with => REG_IP, :message => "is invalid (must be x.x.x.x where x is 0-255)", :if => Proc.new {|ar| !ar.first_ip.blank? }
- validates_format_of :last_ip, :with => REG_IP, :message => "is invalid (must be x.x.x.x where x is 0-255)", :if => Proc.new {|ar| !ar.last_ip.blank? }
- def self.banned?(ip)
- reload_banned_ips if @banned_ips.nil?
- begin
- ip = Infrid::IPAddress.new(ip)
- @banned_ips.each { |b|
- return true if ip.between?(b[0], b[1])
- }
- rescue
- logger.info "IP FORMAT ERROR"
- return true
- end
- false
- end
- def self.banned_ips
- reload_banned_ips if @banned_ips.nil?
- @banned_ips.collect {|b| b[0].to_s + ".." + b[1].to_s }.join"\n"
- end
- #keeps a cache of all banned ip ranges
- def self.reload_banned_ips
- r = connection.select_all("select first_ip, last_ip from banned_ips")
- if !r
- @banned_ips=[]
- end
- @banned_ips = r.map {|item| [Infrid::IPAddress.new(item["first_ip"]),Infrid::IPAddress.new(item["last_ip"])] }
- end
- end
You can then call the banned? method with the ip address from the request, e.g. request.remote_ip, or any other ip you want.
It’s worth noting that I have this reload_banned_ips method which stores my ip address objects in a static variable @banned_ips. My reasoning for this is that I don’t want to go back to the db and create ip_address objects every single time I check if an ip address is banned.
I’ll talk about this and my “non-drb-drb” rails technique for saving on db activity in a future blog post.
Also, a note for the pedants, I wouldn’t have done any of this if I wasn’t checking an ip address falls within a range, in which case it would’ve been a straight forward string comparison.
Again, thanks to David Black, who turned me on to the comparable module and in doing so showed me some kick arse , yet very elegant rails moves. His website is www.rubypal.com

RSS Feed
Hi,
It looks like REG_IP is undefined in this?