#!/usr/bin/ruby
#
# Details: Fgen subsystem, Junos rule generator.
# Authors: Dmitry Maksyoma <dmaksema@allot.com>
# Started: 22/09/2011
#
# (c) 2011 Allot Communications
# 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 Allot Communications, 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.
#
# Syntax:
# edit firewall family inet filter Allot
#   // Unless `next term' given, matching terminates processing.
#   term first {
#     from {
#       //All conditions must match.
#     }
#     then {
#       discard; //by default accept.
#     }
#   }
#
#   // So that packets not discarded by default.
#   term catch-all { then accept; }
#
# http://www.juniper.net/techpubs/software/junos/junos94/swconfig-policy/specifying-numeric-range-filter-match-conditions.html#id-10834476

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

PROTO_DESC = { 51 => 'ah', 8 => 'egp', 50 => 'esp', 47 => 'gre', 1 => 'icmp', 2 => 'igmp',
  4 => 'ipip', 89 => 'ospf', 103 => 'pim', 46 => 'rsvp', 6 => 'tcp',
  17 => 'udp' }
IP_OPTIONS_DESC_JUNOS = { 131 => 'loose-source-route', 7 => 'record-route',
  148 => 'router-alert', 130 => 'security', 136 => 'stream-id',
  137 => 'strict-source-route', 68 => 'timestamp' }
TCP_FLAGS_TO_CHECK = [TCPHeader::SYN_FLAG, TCPHeader::ACK_FLAG, TCPHeader::FIN_FLAG, 
  TCPHeader::RST_FLAG, TCPHeader::URG_FLAG, TCPHeader::PSH_FLAG]

def format_ip(bytes);	bytes.index(nil) ? nil : (bytes.join('.') << '/32')  end

# Create Junos filter.
def format_pattern pattern, name, src_ips, dst_ips
  link, ipl, ippl, term = pattern.link, pattern.ip, pattern.ipp, []
  return unless ipl

  name.sub! '-Filter', ''

  # Source and destination MACs.
  if link
    [[:srcmac, 'source-mac-address'],
	[:dstmac, 'destination-mac-address']].each { |fld, keyword|
      macBytes = link.send fld
      term.push "#{keyword} #{EtherHeader.mac_to_s macBytes}" if macBytes
    }
  end

  # TOS field (take the most of it).
  term.push "dscp #{ipl.dscp}" if ipl.dscp
  term.push "packet-length #{ipl.total_len}" if ipl.total_len
  if ipl.flags
    flags = []
    [[ipl.flags_df, 'dont-fragment'], [ipl.flags_more, 'more-fragments'],
	[ipl.flags_reserved, 'reserved']].each { |flag, desc|
      flags.push((flag ? '' : '!') << desc)
    }
    term.push "fragment-flags \"#{flags.join ' & '}\""
  end
  term.push "fragment-offset #{ipl.frag_offset}" if ipl.frag_offset
  term.push "ttl #{ipl.ttl}" if ipl.ttl

  # IP protocol.
  if ipl.proto
    term.push "protocol #{PROTO_DESC[ipl.proto] || ipl.proto}"

    # IPsec AH & ESP.
    if payload = pattern.payload
      keyword = case ipl.proto
      when 50 # IPSEC-ESP
	'esp-spi'
      when 51 # IPSEC-AH
	'ah-spi'
      end

      term.push "#{keyword} #{'0x%x' % payload.spi}" \
	if keyword && payload.spi
    end
  end

  # Whole IPs within the pattern take precedence of whatever is passed in
  # src_ips/dst_ips.
  src_ips = [$_] if $_ = format_ip(ipl[:srcip])
  dst_ips = [$_] if $_ = format_ip(ipl[:dstip])
  # FIXME: use src host, src port, dst host, dst port order.
  term.push "source-address {\n        " <<
    src_ips.join(";\n") << ";\n      }" if src_ips
  term.push "destination-address {\n        " <<
    dst_ips.join(";\n") << ";\n      }" if dst_ips
  if ippl && (ipl.proto_tcp || ipl.proto_udp)
    term.push "source-port #{ippl.srcport}" if ippl.srcport
    term.push "destination-port #{ippl.dstport}" if ippl.dstport
  end

  if ipl.ipopts && !ipl.ipopts.empty?
    ipoptions = ipl.ipopts.inject([]) { |sum, opt|
      sum.push IP_OPTIONS_DESC_JUNOS[opt] || opt
    }
    term.push('ip-options [' << ipoptions.join(' ') << ']')
  end

  if ippl
    if ipl.proto_tcp && (flags = ippl.flags)
      term.push 'tcp-flags "' << TCP_FLAGS_TO_CHECK.map { |flag|
	(flags & flag > 0 ? '' : '!') << TCPHeader.flag_desc[flag]
      }.join(' & ') << '"'
    elsif ipl.proto_icmp
      term.push "icmp-type #{ippl.itype}" if ippl.itype
      term.push "icmp-code #{ippl.icode}" if ippl.icode
    end
  end

  return if term.empty?

  "edit firewall family inet filter Allot\n  term #{name} {\n" <<
    "    from {\n      " << term.join(";\n      ") << ";\n    }\n" <<
    "    then { discard; }\n  }\n" <<
    "  term catch-all { then accept; }"
end

do_format
