Road to Cisco ACL Parser

id:stereocat:20090720:1248100238 の続き。ちょっとお試し。といっても、本当に Parse::Eyapp の使い方を調べるというレベル。というか、前回やったのひと月前かよ。1ヶ月放置だったか。

use strict;
use warnings;
use Parse::Eyapp;
use Data::Dumper;

my $grammar = q{

# grammar head section
%{
use lib qw(.);
use strict;
use warnings;
use Data::Dumper;
use Regexp::Common qw/number net/;
use AclEntry;

my $curr;
%}


# grammar body section

%%

## level 1
expr: extended_acl
    ;

## level 2
extended_acl: ACL acl_number.aclnum permission.perm protocol.prot src_dst_spec.sspec src_dst_spec.dspec protocol_qualifier.protq logging.logg
{
  $curr = AclEntry->new();
  $curr->acl_number($aclnum);
  $curr->permission($perm);
  $curr->protocol($prot);
  $curr->src_spec($sspec);
  $curr->dst_spec($dspec);
  $curr->protocol_qualifier($protq);
  $curr->logging($logg);
  $curr;
}
    ;

## level 3
acl_number:
      # empty
    | NUM
    ;
permission: PERMIT
    | DENY
    ;
protocol: IP
    | TCP
    | UDP
    | ICMP
    | NUM
    ;
src_dst_spec: HOST ip_addr port_num_spec { { ip => $_[2], mask => '0.0.0.0', port => $_[3] } }
    | ip_addr wildcard_mask port_num_spec { { ip => $_[1], mask => $_[2], port => $_[3] } }
    ;
protocol_qualifier:
      # empty
    | ESTABLISHED
    ;
logging:
      # empty
    | LOGGING
    ;

## level 5
ip_addr: IPv4ADDR
    ;
wildcard_mask: IPv4ADDR
    ;
port_num_spec:
      # empty
    | NUM
    ;

%%

# grammar tail section
## Lexer
sub yylex {
    my ($p) = shift;

    for ($p->YYData->{INPUT} ) {
        m/\G\s+/gc;

        # print "# $_ pos=", pos(), "\n";

        # empty
        $_ eq '' and return ( '', undef );

        # ACL keyword
        m/\G(access-list)/gc and  return ( 'ACL', $1 );

        # permission
        m/\G(permit)/gc and return ('PERMIT', $1);
        m/\G(deny)/gc and return ('DENY', $1);

        # protocol
        m/\G(ip)/gc and return ( 'IP', $1);
        m/\G(tcp)/gc and return ( 'TCP', $1);
        m/\G(udp)/gc and return ( 'UDP', $1);
        m/\G(icmp)/gc and return ( 'ICMP', $1);

        # protocol-qualifier
        m/\G(established)/gc and return ( 'ESTABLISHED', $1);

        # logging
        m/\G(logging)/gc and return ( 'LOGGING', $1);

        # host keyword
        m/\G(host)/gc and return ( 'HOST', $1);
        # ip address, wildcard-mask
        m/\G($RE{net}{IPv4}{dec})/gc and return ('IPv4ADDR', $1);


        # number
        m/\G($RE{num}{int}{-keep})/gc and return ( 'NUM', $1);

        # any other
        m/\G(.)/gcs and return ( $1,    $1 );
    }
    return ( '', undef );}

## Error Handler
sub yyerror {
    die "Syntax error near "
        . ( $_[0]->YYCurval ? $_[0]->YYCurval : "end of file" ) . "\n";
}

## main Routine
sub run {
    my ($self) = shift;
    $self->YYParse(
        yylex => \&yylex,
        yyerror => \&yyerror,
        #yydebug => 0x01,
    );
}

};
# end grammar


############################################################

Parse::Eyapp->new_grammar(
    input => $grammar,
    classname => 'AclParser',
    firstline => 6
);

my $aclparser = AclParser->new();
print "? ";
while (<>) {
    last if m{^q(?:uit)?};
    $aclparser->YYData->{INPUT} = $_;
    my $ret;
    eval { $ret = $aclparser->run };
    warn $@ if $@;
    print Dumper $ret if defined $ret;
    print "? ";
}
package AclEntry;

use strict;
use warnings;
use base qw/Class::Accessor::Fast/;

__PACKAGE__->mk_accessors(
    qw/acl_number permission protocol
       src_spec
       dst_spec
       protocol_qualifier logging
      /
  );

1;

実行例

$ perl aclparse.01.bak.pl
? access-list 11 permit tcp 192.168.0.1 0.0.0.255 host 192.168.3.3
$VAR1 = bless( {
                 'protocol_qualifier' => undef,
                 'protocol' => 'tcp',
                 'logging' => undef,
                 'src_spec' => {
                                 'ip' => '192.168.0.1',
                                 'port' => undef,
                                 'mask' => '0.0.0.255'
                               },
                 'permission' => 'permit',
                 'acl_number' => '11',
                 'dst_spec' => {
                                 'ip' => '192.168.3.3',
                                 'port' => undef,
                                 'mask' => '0.0.0.0'
                               }
               }, 'AclEntry' );
? access-list deny udp host 192.168.0.1 host 192.168.3.3
$VAR1 = bless( {
                 'protocol_qualifier' => undef,
                 'protocol' => 'udp',
                 'logging' => undef,
                 'src_spec' => {
                                 'ip' => '192.168.0.1',
                                 'port' => undef,
                                 'mask' => '0.0.0.0'
                               },
                 'permission' => 'deny',
                 'acl_number' => undef,
                 'dst_spec' => {
                                 'ip' => '192.168.3.3',
                                 'port' => undef,
                                 'mask' => '0.0.0.0'
                               }
               }, 'AclEntry' );
? q
$

とまあ、こんな感じで、ACL を Parse して加工の簡単な形にして出してくれないかなーという目論見だったのだが。まあ、どういう形にするかは要検討だな。Parse 結果を保持する class も今回は適当に作ったのだけど、cpan 探せば何かあるんじゃなかろうか。ACL Parse → ACL Object → ACL操作 → ACL出力 みたいな流れでやれるといいかなあ、くらい。NetAddr::IP とかその辺のオブジェクトでIPアドレスとか保持するようにして重複チェックとか、最適化とかに持っていけるような状態に…なったらいいなあ、と。

もうちょっと Parse::Eyapp 使い方調べないといけないな。画面でドキュメント読むのしんどいので今度印刷してこよう。

それにしても、ACL の文法ってどう組むのがいいんだろう。今回はお試しなので any とかのワードすら入れてないし、port 指定についても演算子入れるの忘れてたり。その辺ってどこかにまとまった資料がないのだろうか? とりあえずいまは

と、前回作った acl syntax checker の syntax tree を見ていたのだが。あ、あと

Cisco IOSアクセスリスト

Cisco IOSアクセスリスト

も手元にあるのだけど、いずれも syntax そのものについてそんなに詳しく解説してあるというわけではないので。もっとぱっとわかりやすいもんがないものか…。これ、asa/fwsm とかだとまた個別の機能があったりとかするし、どこまでやるかだよなー。