#!/usr/bin/ruby
#
# Details: Fgen subsystem, Cisco rule generator
# Authors: Dmitry Maksyoma <dmaks@esphion.com>
# Started: 10/2005
#
# (c) 2005 Esphion Limited
# Copyright in this document, whether in written, electronic or other
# format including any source code or other computer code set forth
# therein or attached thereto and copyright in all parts thereof is owned
# by Esphion Limited, who reserves all rights therein.
# In particular, no part of this document/computer code may be reproduced,
# copied, stored in a retrieval system, used or transmitted by any means
# whatsoever without the prior written consent of Esphion Limited.
#
# All enquiries in relation thereto should be directed to:
#
# Esphion Ltd
# P.O. Box 300496,
# Albany,
# Auckland,
# New Zealand.
#
# Phone: +64 9 415 0227
# Fax:	 +64 9 415 0228
# Email: info@esphion.com

$:.insert(-2, File.join(File.dirname(File.expand_path($0)), '../lib'))
require 'pattern/entities'
require 'fgen/formatters'
require 'fgen/common'
include Entities

# Supported by Cisco IP options.
# http://www.cisco.com/univercd/cc/td/doc/product/software/ios124/124cr/hiap_r/apl_d1h.htm#wp1115817
IP_OPTIONS_DESC_CISCO = { 147=>'add-ext', 134=>'com-security', 151=>'dps',
  15=>'encode', 0=>'eool', 145=>'ext-ip', 133=>'ext-security', 205=>'finn',
  144=>'imitd', 131=>'lsr', 11=>'mtup', 12=>'mtur', 1=>'no-op', 150=>'nsapa',
  7=>'record-route', 148=>'router-alert', 149=>'sdb', 130=>'security',
  137=>'ssr', 136=>'stream-id', 68=>'timestamp', 82=>'traceroute',
  152=>'ump', 142=>'visa', 10=>'zsu' }

FLAGS_TO_CHECK_CISCO = [TCPHeader::FIN_FLAG, TCPHeader::SYN_FLAG,
  TCPHeader::RST_FLAG, TCPHeader::PSH_FLAG, TCPHeader::ACK_FLAG,
  TCPHeader::URG_FLAG]

# Cisco IOS 12.4 and PIX 7.2 only support a limited set of protocol names
# (other protocols can still be specified by numbers). The following 2
# hashes will be consulted when translating a protocol number to a protocol
# name by Cisco and Pix filters respectievly.
SUPPORTED_PROTOCOL_NAMES_CISCO = {}
%w(eigrp gre icmp igmp ipinip nos ospf tcp udp).each { |p|
  id = IPHeader.desc_proto[p]
  SUPPORTED_PROTOCOL_NAMES_CISCO[id] = p if id
}
SUPPORTED_PROTOCOL_NAMES_PIX = {}
%w(egp icmp tcp udp).each { |p|
  id = IPHeader.desc_proto[p]
  SUPPORTED_PROTOCOL_NAMES_PIX[id] = p if id
}

def cisco?
  @cisco ||= FGEN_TYPE == 'cisco'
end

def fmt_ip_cisco(bytes, wildcard=true)
  mask = [255, 255, 255, 255]
  0.upto(mask.size-1){ |i|
    if bytes[i] 
      mask[i] = 0
    else
      bytes[i] = 0
    end
  }
  mask.map! { |b| 255 - b } unless wildcard
  bytes.join('.') + " " + mask.join('.')
end

def format_cisco(pattern, name, src_ips, dst_ips)
  return unless pattern.ip

  name = purify_name(name)
  res = []
  pixHeader = ''

  if cisco?
    res << "ip access-list extended #{name}"
  else
    pixHeader = "access-list #{name} "
  end

  # Make cartesian product.
  unless src_ips || dst_ips
    res << pixHeader + make_stmt(pattern)
  else
    if src_ips
      src_ips.map!{|ip| ip + " 0.0.0.0"}
    else 
      src_ips = [nil]
    end
    if dst_ips
      dst_ips.map!{|ip| ip + " 0.0.0.0"}
    else 
      dst_ips = [nil]
    end
    src_ips.each{|src_ip|
      dst_ips.each{|dst_ip|
	res << (pixHeader + make_stmt(pattern, src_ip, dst_ip))
      }
    }
  end
  res.join("\n")
end

def make_stmt(pattern, src_ip=nil, dst_ip=nil)
  ipl = pattern.ip
  ippl = pattern.ipp
  srcip = src_ip ? src_ip : fmt_ip_cisco(ipl[:srcip], cisco?)
  dstip = dst_ip ? dst_ip : fmt_ip_cisco(ipl[:dstip], cisco?)

  srcport = dstport = flags_str = icmp = nil
  if ippl
    if (ipl.proto)
      # If protocol allows, add src/dst ports.
      if (ipl.proto_tcp || ipl.proto_udp)
	srcport = ippl.srcport
	srcport = "eq #{srcport}" if srcport
	dstport = ippl.dstport
	dstport = "eq #{dstport}" if dstport
      elsif (ipl.proto_icmp)
	icmp = [ippl.itype, pix? ? nil : ippl.icode].compact
	icmp = icmp.size > 0 ? icmp.join(' ') : nil
      end
    end

    if cisco?
      # Add TCP flags.
      if ipl.proto_tcp
	flags_byte = ippl.flags
	flags_plus = ""; flags_minus = ""
	if flags_byte
	  FLAGS_TO_CHECK_CISCO.each{|flag|
	    if flags_byte & flag > 0
	      flags_plus << " +" + TCPHeader.flag_desc[flag]
	    else
	      flags_minus << " -" + TCPHeader.flag_desc[flag]
	    end
	  }
	  flags_str = "match-all" + flags_plus + flags_minus
	end
      # Add IGMP type.
      elsif ipl.proto_igmp
	igmp = ippl.itype
      end
    end
  end

  # IP options.
  if cisco? && !(ipl.proto_icmp || ipl.proto_igmp || ipl.proto_tcp ||
      ipl.proto_udp)
    ipopts = []
    ipl.ipopts.each { |opt|
      next unless desc = IP_OPTIONS_DESC_CISCO[opt]

      ipopts << desc
    }

    if ipopts.empty?
      ipopts = nil
    else
      ipopts = "option " << ipopts.join(' ')
    end
  end

  # Add `tos' and `fragments'.
  tos = "tos #{ipl.tos}" if cisco? && ipl.tos
  fragments = "fragments" if cisco? && ipl.frag_offset && ipl.frag_offset > 0

  unless ipl.proto
    proto = 'ip'
  else
    proto = cisco? ? SUPPORTED_PROTOCOL_NAMES_CISCO[ipl.proto] :
      SUPPORTED_PROTOCOL_NAMES_PIX[ipl.proto]
    proto = ipl.proto if proto.nil?
  end

  "deny #{proto} #{srcip} #{srcport} #{dstip} #{dstport} #{ipopts} #{icmp} " \
  "#{igmp} #{flags_str} #{tos} #{fragments}".strip.gsub(/[ \t\r\f]+/, ' ')
end

def pix?
  !@cisco
end

#determine format from executable name ($0)
FGEN_TYPE = File.basename($0)

def format_pattern(p, name, src_ips, dst_ips)
  case FGEN_TYPE
  when "cisco"
    format_cisco(p, name, src_ips, dst_ips)
  when "pix"
    format_cisco(p, name, src_ips, dst_ips)
  else
    $stderr.puts "unknown format: #{FGEN_TYPE}"
    exit 1
  end
end

do_format
