Perl で Cisco ACL Parser を作れないかな?

先週、仕事で大量の ACL の中身の整理をする羽目になったのだが、Cisco ACL の整理ってものすごく面倒なんだよね。Config 食わせたら、ACL ごとに、permit/deny, protocol, (src|dst)-(ip|mask|port), option を Parse してはいてくれるツールとか CPAN Module とかないのかと。

CPAN を調べると、逆に src/dst ip とかのデータを突っ込んで ACL をはき出すというのはいくつかあったのだけど、config を parse して正規化したデータをはいてくれるというのが見つからない。(探し方が悪い? 誰か知っていたら教えてください)

じゃツールはないのかと思ってみてみたのだけど、類似のツールとしてこういうのがあった。

ただ、どっちも俺がやりたいこととは違うし、そもそもコンパイル通らなかったりした。

とはいえ、"Syntax Checker" の方とかには構文解析用の情報 (yacc/lex file) が入っているわけだ。この辺を流用してやれば、ACL parse できるツールが作れたりしないかな? というのをやってみた。

状況

  • 構文解析/Yacc, BNF記法について復習
  • Perl構文解析してくれるモジュールの使い方を何となく調査
  • Cisco ACL Syntax Checker で定義している ACL Syntax の解析 …… いまここ
  • そのうち実装

構文解析/BNFとはなんぞや/CPAN Module 調査

ざっくり復習。といってもすごくわかりやすいページがあったのでここだけ読んだ。こんなの高専の時にやって以来だなあ。

ツールについてはこの辺から当たりをつける。

まあ、ドキュメントの充実度や開発状況から行けば Parse::Eyapp を使うんだろうな。

Parse::Eyapp 練習

うえのページにあがっていたのをパクる。そのまま書いてもおもしろくないので、Yapp file と 生成したモジュールを利用するスクリプトの二つに分割してみた。

  • calc.yp
# -*- cperl -*-

############################################################
## header section
%{
use strict;
use warnings;
use Regexp::Common;
%}

%left   '-' '+'
%left   '*' '/'
%left   NEG POS
%%

############################################################
## body section

expr: NUM
    | expr '+' expr { $_[1] + $_[3] }
    | expr '-' expr { $_[1] - $_[3] }
    | expr '*' expr { $_[1] * $_[3] }
    | expr '/' expr { $_[1] / $_[3] }
    | '(' expr ')' { $_[2] }
    | '-' expr %prec NEG { -$_[2] }
    | '+' expr %prec POS { $_[2] }
    ;
%%
############################################################
## tail section

## Lexer
sub yylex {
    my ($p) = shift;
    for ( $p->YYData->{INPUT} ) {
        m/\G\s+/gc;
        $_ eq '' and return ( '', undef );
        m/\G(?![+-])$RE{num}{real}{-keep}/gc and return ( 'NUM', $1 );
        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, );
}
  • calc-handler.pl
# -*- cperl -*-

use lib qw(.);
use strict;
use warnings;
use Regexp::Common;
use Data::Dumper;
use Calc;

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

と、ここまで用意して以下のコマンドを実行

$ eyapp -m Calc calc.yp     # Calc.pm の生成
$ perl calc-handler.pl

ほほう。まああとは、Parse::Eyapp の pod にいろいろと書いてあるのでその辺を見てみるのがよろしかろうとは思うが、何となく動きがわかったのでまり詳しくは読んでいないのだ。

で、このあたりで一度、ACL Syntax Checker の yacc file を読んでみたのだけど、部分部分はわかるのだが、全体としてどういう構造になっているのかがわからない。なんか図を出力してくれるものはないのか、と。でも、yacc 構文から構文図を自動的に生成するなんて絶対ツールがあるはず。構文図の生成となればまあ graphviz なのかな、と。思って CPAN 検索したら、何のことはない。GraphViz パッケージの中に GraphViz::Parse::Yacc とか GraphViz::Parse::Yapp とかあるのね。

ということで、まずはサンプルの方から試してみる。使い方を見ると、yapp(yacc) -v で出力した grammer information file を使うみたいなので、あらかじめ出力しておく。

$ eyapp -v calc.yp

これで calc.output が出力される。

  • parseyapp.pl
use strict;
use warnings;
use GraphViz::Parse::Yapp;

my $graph = GraphViz::Parse::Yapp->new('calc.output');
print $graph->as_text;
$graph->as_png('calc.png');

こんな感じ。

うむ。シンプル……もうちょっと複雑な例があってもいいなあ。まあいいや。

Analyze ACL Syntax Checker

で、だいたい道具がそろっただろうから、ぼちぼち ACL Syntax Checker の yacc ファイルの解析をしてみる。そのまま GraphViz::Parse::Yacc を使うのだけれど……。まあ、結論を言うと、ちゃんと動かなかったというか。"Use of uninitialized value in hash element at /usr/lib/perl5/site_perl/5.8/GraphViz/Parse/Yacc.pm line 112, line 877." がガーッと出たあげくに、どうもそれは違うだろうさすがに、というデータが出ているので、これをそのまま使うのはちょっとあきらめる。

とにかく、全体感がつかみたいのであまり細かいことを考えず、普通に正規表現で parse して graphviz で図をはいてみる、という方向へ。(面倒くさがり過ぎか?)

GraphViz Module の使い方とかでいろいろあって今日はこれ作って終わってしまった。GraphViz の使い方についてはまた後日どこかで書こう。

  • ypregex2.pl
use strict;
use warnings;
use GraphViz;
use Data::Dumper;

my $TOKEN  = '[\w]+';
my $TOKENS = '[\w\s]+';
my $graph  = GraphViz->new(
    'directed' => 1,
    'layout'   => 'dot',
);
my $gd = {};    # graph data (empty hash reference)

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

sub add_record {
    my ( $gd, $from_tok, $rule_num, $to_toks ) = @_;
    my $rules;
    if ( !exists( $gd->{$from_tok} ) ) {
        $gd->{$from_tok} = [];
    }
    else {
        $rules = $gd->{$from_tok};
    }

    # print "## ", Dumper $gd;

    my $rec     = [];    # record data (empty list reference)
    my $portnum = 0;
    if ( $to_toks =~ /^\s*$/ ) {
        my $tok_data = {
            'port'  => 'port' . $portnum,
            'token' => 'EMPTY',
        };
        push @$rec, $tok_data;
        $portnum++;
    }
    else {
        foreach my $tok ( split /\s+/, $to_toks ) {
            my $tok_data = {
                'port' => 'port' . ( $rule_num * 10 + $portnum ),
                'token' => $tok,
            };
            push @$rec, $tok_data;
            $portnum++;
        }
        push @{ $gd->{$from_tok} }, $rec;
    }
}

sub make_graph {
    my ( $graph, $gd ) = @_;

    # make nodes
    foreach my $token_name ( keys %$gd ) {
        my $records = $gd->{$token_name};
        my @rec_label;
        foreach my $rec (@$records) {
            my @tok_label;
            foreach my $tok (@$rec) {
                my $tok_str = "<" . $tok->{port} . ">" . $tok->{token};
                push @tok_label, $tok_str;
            }
            push @rec_label, '{' . join( '|', @tok_label ) . '}';
        }
        # my $label = $token_name . '|' . '{' . join( '|', @rec_label ) . '}';
        my $label = '{' . join( '|', @rec_label ) . '}';
        print "add node: $token_name\n  record: $label\n";
        $graph->add_node(
            $token_name,
            color     => "blue",
            shape     => "record",
            label     => $label,
            cluster   => $token_name
        );
    }

    # make edges
    foreach my $token_name ( keys %$gd ) {
        my $records = $gd->{$token_name};
        foreach my $rec (@$records) {
            foreach my $tok (@$rec) {
                my $from_node     = $token_name;
                my $to_node       = $tok->{token};
                my $from_port_num = $tok->{port};
                $from_port_num =~ s/port//;

                if ( $to_node =~ /[A-Z][A-Z_0-9]*/ ) {
                    # if terminal symbol, nothing to do.
                }
                else {
                    print
                        "add edge: $from_node/<port$from_port_num> => $to_node\n";
                    $graph->add_edge( $from_node, $to_node,
                                      from_port => $from_port_num );
                }
            }
        }
    }
}

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

my $token;
my $rule_num = 0;
while (<>) {
    chomp;
    my $line = $_;
    $line =~ s|/\*.*(\*/)?||g;    # delete 1line comment

    # print "#line: $line\n";

    if ( $line =~ /^\s*\d+\s+($TOKEN)\s*:\s*($TOKENS)/ ) {
        $token = $1;

        # add record
        # print "#line: $1 => $2\n";
        $rule_num++;
        add_record( $gd, $token, $rule_num, $2 );
    }
    elsif ( $line =~ /^\s*\d+\s+\|\s*($TOKENS)/ ) {

        # print "#line: $token => $1\n";
        $rule_num++;
        add_record( $gd, $token, $rule_num, $1 );
    }
    elsif ( $line =~ /^\s*$/ ) {

        # end
        $rule_num = 0;
    }
    elsif ( $line =~ /Terminals, with rules where they appear/ ) {
        last;
    }
}

#print Dumper $gd;

make_graph( $graph, $gd );

##print $graph->as_text;
$graph->as_png("ipv4acl.png");

__END__
usage;
yacc -y hoge.y # generate y.output
cat y.output | perl $0

で、ACL Syntax Checker の yacc file は IPv6 用の定義も入っているのだけど、いきなりそこまでフォローしきれないので、その辺はざっくり削って、IPv4 ACL のところだけを出す。それでもそれなりの量ある。

あ、書き忘れたけど、図は拡張 ACL の文法。(そもそも、元にした ACL Syntax Checker が標準 ACL には対応していないので。)

結局のところ

ここまできたのはいいけれどもだ。
最終的にやりたいのはこの辺の構文を実装して、Parse してくれる何かを作ることなのだが。なんかこう見ると先の長さに嫌気がさしてきたな。登山の装備とルートを確認したんだけど集めた物資の量が大量にあって、もうそこまでして行かなくていいんじゃないのかみたいな空気だ。
そんなに全部のオプションを駆使した ACL 書くってこともないので、よく使う範囲内でこの辺の情報を元に自分で構文組み立ててみるのがいいかな。どうせお勉強の範囲を出ないのだし。

ということで、今後どこまで作るかはやる気と仕事の負荷次第。