From 0cb1429c6295ae4d2c41a83cd05521ac942ad5d6 Mon Sep 17 00:00:00 2001 From: Brian Candler Date: Fri, 6 May 2011 11:41:06 +0100 Subject: [PATCH] Experimental code for reading iptables --- lib/linux/c_struct.rb | 2 +- lib/linux/iptables.rb | 86 ++++++++++++++++++++++++++++++++++++++++++ lib/linux/iptables4.rb | 53 +++++++++++++------------- 3 files changed, 114 insertions(+), 27 deletions(-) create mode 100644 lib/linux/iptables.rb diff --git a/lib/linux/c_struct.rb b/lib/linux/c_struct.rb index de0e2cd..28375e2 100644 --- a/lib/linux/c_struct.rb +++ b/lib/linux/c_struct.rb @@ -144,7 +144,7 @@ class CStruct # Apply padding for structure alignment if necessary size = info[:size] || [default].pack(pattern).bytesize - if align = info[:align] + if align = (opt[:align] || info[:align]) align = size if align == true field_pad alignto(@bytesize, align) - @bytesize end diff --git a/lib/linux/iptables.rb b/lib/linux/iptables.rb new file mode 100644 index 0000000..4a59f2a --- /dev/null +++ b/lib/linux/iptables.rb @@ -0,0 +1,86 @@ +require 'socket' +require 'linux/constants' +require 'linux/c_struct' +require 'linux/netlink/message' # just for :dev_name type + +module Linux + module Ext + require 'ffi' + extend FFI::Library + ffi_lib FFI::Library::LIBC + attach_function :getsockopt, [:int, :int, :int, :buffer_inout, :buffer_inout], :int + end + + # *** THIS CODE IS INCOMPLETE *** + # + # Virtual base class for handling iptables. You should invoke either + # Iptables4 or Iptables6 as appropriate. + # + # (Mirrors the structure of iptables libiptc/libiptc.c, included by + # libip4tc.c and libip6tc.c) + # + # filter = Linux::Iptables4.table("filter") + # + # TODO: should we use ffi's structures instead of CStruct? + # We have to use ffi anyway, until ruby getsockopt supports buffer passing. + # http://redmine.ruby-lang.org/issues/4645 + class Iptables + def self.inherited(subclass) #:nodoc: + subclass.instance_variable_set(:@tables, {}) + end + + def self.socket + @socket ||= Socket.new(self::TC_AF, Socket::SOCK_RAW, Socket::IPPROTO_RAW) + end + + def self.table(tablename = "filter") + @tables[tablename] ||= new(tablename, socket) + end + + def initialize(name, socket) + raise "Invalid table name" if name.bytesize > self.class::TABLE_MAXNAMELEN + @name = name + @socket = socket + reload + end + + def rules + @rules + end + + def getsockopt(level, optname, buf) + buflen = FFI::Buffer.new :socklen_t + if buflen.length == 4 + buflen.put_uint32(0, buf.length) + elsif buflen.length == 8 + buflen.put_uint64(0, buf.length) + else + raise "Unexpected buflen length: #{buflen.length}" + end + res = Ext.getsockopt(@socket.fileno, level, optname, buf, buflen) + raise "getsockopt error: #{res}" if res < 0 # FIXME: get errno? + buf.get_bytes(0, buflen.length == 4 ? buflen.get_uint32(0) : buflen.get_uint64(0)) + end + + def reload + buf = FFI::Buffer.new IPTGetInfo.bytesize + buf.put_string(0, @name) + info = self.class::STRUCT_GETINFO.parse(getsockopt(self.class::TC_IPPROTO, self.class::SO_GET_INFO, buf)) + #warn "valid_hooks=0x%08x, num_entries=%d, size=%d" % [info.valid_hooks, info.num_entries, info.size] + + buf2 = FFI::Buffer.new(self.class::STRUCT_GET_ENTRIES.bytesize + info.size) + buf2.put_bytes(0, self.class::STRUCT_GET_ENTRIES.new(:name=>@name, :size=>info.size).to_str) + getsockopt(self.class::TC_IPPROTO, self.class::SO_GET_ENTRIES, buf2) + + res = [] + ptr = self.class::STRUCT_GET_ENTRIES.bytesize + limit = ptr + info.size + while ptr < limit + res << self.class::STRUCT_ENTRY.parse(buf2.get_bytes(ptr, self.class::STRUCT_ENTRY.bytesize)) + ptr += res.last.next_offset + end + raise "Error parsing rules: got #{res.size} instead of #{info.num_entries}" if res.size != info.num_entries + @rules = res + end + end +end diff --git a/lib/linux/iptables4.rb b/lib/linux/iptables4.rb index 1b00bce..40ea2f1 100644 --- a/lib/linux/iptables4.rb +++ b/lib/linux/iptables4.rb @@ -1,7 +1,4 @@ -require 'socket' -require 'linux/constants' -require 'linux/c_struct' -require 'linux/netlink/message' # just for :dev_name type +require 'linux/iptables' module Linux #- @@ -24,8 +21,7 @@ module Linux class IPTGetEntries < CStruct field :name, :pattern=>"Z#{IPT_TABLE_MAXNAMELEN}", :default=>EMPTY_STRING field :size, :uint - #field :entrytable, :pattern=> - field :entrytable, :binary # struct ipt_entry entrytable[] + field :entrytable, :binary, :align=>1.size # struct ipt_entry entrytable[0] end # struct ipt_entry @@ -43,34 +39,39 @@ module Linux field :flags, :uchar field :invflags, :uchar #### - field :nfcache, :int - field :target_offset, :uint16 - field :next_offset, :uint16 + field :nfcache, :uint + field :target_offset, :uint16 # size of ipt_entry + matches + field :next_offset, :uint16 # size of ipt_entry + matches + target field :comefrom, :uint - field :packet_count, :uint64 + ### struct xt_counters + field :packet_count, :uint64, :align => 8 field :byte_count, :uint64 - field :elems, :binary + ### + field :elems, :binary # matches (if any), then the target + + def after_parse + self.src = src == 0 ? nil : IPAddr.new(src, Socket::AF_INET) + self.dst = dst == 0 ? nil : IPAddr.new(dst, Socket::AF_INET) + self.smsk = smsk == 0 ? nil : IPAddr.new(smsk, Socket::AF_INET) + self.dmsk = dmsk == 0 ? nil : IPAddr.new(dmsk, Socket::AF_INET) + end end # Class for handling iptables. Note that this doesn't actually use # Netlink at all :-( - class Iptables4 - TC_AF = Socket::AF_INET - TC_IPPROTO = Socket::IPPROTO_IP - SO_GET_INFO = IPT_SO_GET_INFO - STRUCT_GETINFO = IPTGetInfo - STRUCT_GET_ENTRIES = IPTGetEntries - - def initialize(tablename = "filter") - @socket = Socket.new(TC_AF, Socket::SOCK_RAW, Socket::IPPROTO_RAW) - info = STRUCT_GETINFO.new(:name => tablename) - # FIXME: ruby won't let us pass structure to getsockopt!! - @socket.getsockopt(TC_IPPROTO, SO_GET_INFO, info) - warn "valid_hooks=0x%08x, num_entries=%d, size=%d" % [info.valid_hooks, info.size, info.num_entries] - end + class Iptables4 < Iptables + TABLE_MAXNAMELEN = IPT_TABLE_MAXNAMELEN + TC_AF = Socket::AF_INET + TC_IPPROTO = Socket::IPPROTO_IP + SO_GET_INFO = IPT_SO_GET_INFO + SO_GET_ENTRIES = IPT_SO_GET_ENTRIES + STRUCT_ENTRY = IPTEntry + STRUCT_GETINFO = IPTGetInfo + STRUCT_GET_ENTRIES = IPTGetEntries end end if __FILE__ == $0 - iptables = Linux::Iptables4.new + require 'pp' + pp Linux::Iptables4.table("filter").rules end