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:

  1. module Infrid
  2. class IPAddress
  3. include Comparable
  4. def initialize(address)
  5. @address = address
  6. end
  7. def split
  8. @address.split(.).map {|s| s.to_i }
  9. end
  10. def <=>(other)
  11. split <=> other.split
  12. end
  13. def to_s
  14. @address
  15. end
  16. end
  17. 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.

  1. ip_address_a = IPAddress.new(127.0.0.1)
  2. ip_address_b = IPAddress.new(126.0.0.1)
  3. ip_address_c = IPAddress.new(127.4.34.3)
  4. ip_address_a < ip_address_b
  5. >> true
  6. ip_address_a > ip_address_c
  7. >> true
  8. ip_address_a.between ip_address_b, ip_address_c
  9. >> 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:

  1. class BannedIp < ActiveRecord::Base
  2. @banned_ips # hash of ips and masks
  3. validates_presence_of :first_ip, :message =>"first address is needed"
  4. validates_presence_of :last_ip, :message =>"last address is needed"
  5. 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? }
  6. 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? }
  7. def self.banned?(ip)
  8. reload_banned_ips if @banned_ips.nil?
  9. begin
  10. ip = Infrid::IPAddress.new(ip)
  11. @banned_ips.each { |b|
  12. return true if ip.between?(b[0], b[1])
  13. }
  14. rescue
  15. logger.info "IP FORMAT ERROR"
  16. return true
  17. end
  18. false
  19. end
  20. def self.banned_ips
  21. reload_banned_ips if @banned_ips.nil?
  22. @banned_ips.collect {|b| b[0].to_s + ".." + b[1].to_s }.join"\n"
  23. end
  24. #keeps a cache of all banned ip ranges
  25. def self.reload_banned_ips
  26. r = connection.select_all("select first_ip, last_ip from banned_ips")
  27. if !r
  28. @banned_ips=[]
  29. end
  30. @banned_ips = r.map {|item| [Infrid::IPAddress.new(item["first_ip"]),Infrid::IPAddress.new(item["last_ip"])] }
  31. end
  32. 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