File Coverage

blib/lib/App/Iptables2Dot.pm
Criterion Covered Total %
statement 145 150 96.6
branch 49 54 90.7
condition 30 43 69.7
subroutine 15 15 100.0
pod 5 5 100.0
total 244 267 91.3


);
line stmt bran cond sub pod time code
1             package App::Iptables2Dot;
2              
3             # vim: set sw=4 ts=4 tw=78 et si filetype=perl:
4              
5 9     9   633626 use warnings;
  9         88  
  9         307  
6 9     9   50 use strict;
  9         19  
  9         191  
7 9     9   47 use Carp;
  9         17  
  9         500  
8 9     9   7247 use Getopt::Long qw(GetOptionsFromString);
  9         119002  
  9         57  
9              
10 9     9   5348 use version; our $VERSION = qv('v0.3.3');
  9         16661  
  9         56  
11              
12             # Module implementation here
13              
14             my @optdefs = qw(
15             checksum-fill
16             clamp-mss-to-pmtu
17             comment=s
18             ctstate=s
19             destination|d=s
20             destination-ports|dports=s
21             dport=s
22             dst-type=s
23             gid-owner=s
24             goto|g=s
25             helper=s
26             in-interface|i=s
27             icmp-type=s
28             jump|j=s
29             limit=s
30             limit-burst=s
31             log-prefix=s
32             m=s
33             mac-source=s
34             match-set=s
35             mss=s
36             notrack
37             o=s
38             physdev-in=s
39             physdev-is-bridged
40             physdev-is-in
41             physdev-is-out
42             physdev-out=s
43             protocol|p=s
44             reject-with
45             socket-exists
46             source|s=s
47             sport=s
48             state=s
49             suppl-groups
50             tcp-flags=s
51             to-destination=s
52             to-ports=s
53             to-source
54             uid-owner=s
55             ulog-prefix=s
56             );
57              
58             sub new {
59 8     8 1 726 my ($self) = @_;
60 8   33     72 my $type = ref($self) || $self;
61              
62 8         46 $self = bless { nodemap => {}, nn => 0 }, $type;
63              
64 8         29 return $self;
65             } # new()
66              
67             sub add_optdef {
68 1     1 1 862 my $optdef = shift;
69 1         5 push @optdefs, $optdef;
70             } # add_optdef()
71              
72             # dot_graph($opt, @graphs)
73             #
74             # Creates a graph in the 'dot' language for all tables given in the list
75             # @graphs.
76             #
77             # Returns the graph as string.
78             #
79             sub dot_graph {
80 10     10 1 8078 my $self = shift;
81 10         23 my $opt = shift;
82 10         20 my $subgraphs = '';
83 10         27 foreach my $graph (@_) {
84 10         40 $subgraphs .= $self->_dot_subgraph($opt,$graph);
85             }
86 10         45 my $ranks = join "; ", $self->_internal_nodes($opt,@_); # determine all internal chains
87 10         130 my $graph = <<"EOGRAPH";
88             digraph iptables {
89             { rank = source; $ranks; }
90             rankdir = LR;
91             $subgraphs
92             }
93             EOGRAPH
94 10         47 return $graph;
95             } # dot_graph()
96              
97             sub read_iptables {
98 9     9 1 36 my ($self,$input) = @_;
99              
100 9         303 while (<$input>) {
101 283         661 $self->_read_iptables_line($_);
102             }
103             } # read_iptables()
104              
105             sub read_iptables_file {
106 9     9 1 71 my ($self,$fname) = @_;
107              
108 9 50       443 if (open(my $input, '<', $fname)) {
109 9         69 $self->read_iptables($input);
110 8         148 close $input;
111             }
112             else {
113 0         0 die "can't open file '$fname' to read iptables-save output";
114             }
115             } # read_iptables_file()
116              
117             ## internal functions only
118              
119             # _dot_edges($table)
120             #
121             # Lists all jumps between chains in the given table as edge description in the
122             # 'dot' language.
123             #
124             # Returns a list of edge descriptions.
125             #
126             sub _dot_edges {
127 10     10   32 my ($self,$opt,$table) = @_;
128 10         61 my @edges = ();
129 10         19 my %seen = ();
130 10         55 my $re_it = qr/^(MASQUERADE|RETURN|TCPMSS)$/;
131 10         18 foreach my $edge (@{$self->{jumps}->{$table}}) {
  10         30  
132 99         151 my $tp = ':w';
133 99         126 my $lbl = '';
134 99 0 33     180 if ($opt->{edgelabel} && $edge->[2]) {
135 0         0 $lbl = " [label=\"$edge->[2]\"]";
136             }
137 99 100       425 unless ($edge->[1] =~ $re_it) {
138 94         154 $tp = ":name:w";
139             }
140 99         144 my $e0 = $edge->[0];
141 99         146 my $e1 = $edge->[1];
142 99 100       180 if ($opt->{"use-numbered-nodes"}) {
143 3   33     9 $e0 = $self->{nodemap}->{$edge->[0]} || $edge->[0];
144 3   66     12 $e1 = $self->{nodemap}->{$edge->[1]} || $edge->[1];
145             }
146 99 100       165 if ($opt->{showrules}) {
147 86 100       144 if (my $ot = $opt->{'omittargets'}) {
148 3         8 my %omit = map { $_ => 1, } split(',',$ot);
  6         16  
149             push @edges, "$e0:R$edge->[3]:e -> $e1$tp$lbl;"
150 3 100       16 unless ($omit{$edge->[1]});
151             }
152             else {
153 83         314 push @edges, "$e0:R$edge->[3]:e -> $e1$tp$lbl;";
154             }
155             }
156             else {
157 13         37 my $etext = "$e0:e -> $e1$tp$lbl";
158 13 100       40 unless ($seen{$etext} ++) {
159 9         21 push @edges, $etext;
160             }
161             }
162             }
163 10 100 66     55 if ($opt->{showrules} || $opt->{edgelabel}) {
164 7         67 return @edges;
165             }
166             else {
167             my @le = map {
168 3 100       8 1 < $seen{$_} ? qq($_ [label="($seen{$_})"];) : qq($_;);
  9         38  
169             } @edges;
170 3         18 return @le;
171             }
172             } # _dot_edges()
173              
174             # _dot_nodes($table)
175             #
176             # Lists all chains in the given table as node descriptions in the 'dot'
177             # language.
178             #
179             # Returns a list of node descriptions.
180             #
181             sub _dot_nodes {
182 10     10   27 my ($self,$opt,$table) = @_;
183 10         21 my @nodes = ();
184 10         21 my %used = ();
185 10 100 100     58 unless ($opt->{showunusednodes} || $opt->{"use-numbered-nodes"}) {
186 8         15 %used = map { $_->[0] => 1, } @{$self->{jumps}->{$table}};
  91         192  
  8         29  
187             }
188 10         30 foreach my $node (keys %{$self->{chains}->{$table}}) {
  10         63  
189             next unless ($used{$node}
190             || $opt->{showunusednodes}
191 97 100 100     309 || $opt->{"use-numbered-nodes"});
      100        
192 56         83 my @rules = ();
193 56         80 my $rn = 0;
194 56 100       105 if ($opt->{showrules}) {
195 47         63 foreach my $rule (@{$self->{chains}->{$table}->{$node}->{rules}}) {
  47         117  
196 105         244 push @rules, qq(
$rule
197 105         156 $rn++;
198             }
199             }
200 56         181 my $lbl = "" \n)
201             . qq(
$node
202             . join("\n", @rules, "
");
203 56 100       109 if ($opt->{"use-numbered-nodes"}) {
204 3         11 push @nodes, $self->{nodemap}->{$node} ." [shape=none,margin=0,label=<$lbl>];";
205             }
206             else {
207 53         188 push @nodes, "$node [shape=none,margin=0,label=<$lbl>];";
208             }
209             }
210 10         104 return @nodes;
211             } # _dot_nodes()
212              
213             # _dot_subgraph($opt, $table)
214             #
215             # Creates a subgraph in the 'dot' language for the table given in $table.
216             #
217             # Returns the subgraph as string.
218             #
219             sub _dot_subgraph {
220 10     10   26 my ($self,$opt,$table) = @_;
221 10         32 my $nodes = join "\n ", $self->_dot_nodes($opt,$table);
222 10         48 my $edges = join "\n ", $self->_dot_edges($opt,$table);
223 10         115 my $graph = <<"EOGRAPH";
224             subgraph $table {
225             $nodes
226             $edges
227             }
228             EOGRAPH
229 10         76 return $graph;
230             } # _dot_subgraph()
231              
232             # _internal_nodes(@tables)
233             #
234             # Lists all chains from all tables in @tables, that are internal chains.
235             #
236             # Returns a list of all internal tables.
237             #
238             sub _internal_nodes {
239 10     10   20 my $self = shift;
240 10         17 my $opt = shift;
241 10         33 my $re_in = qr/^(PREROUTING|POSTROUTING|INPUT|FORWARD|OUTPUT)$/;
242 10         22 my @nodes = ();
243 10         20 my %have_node = ();
244 10         18 my %used = ();
245 10         25 foreach my $table (@_) {
246 10 100 100     70 unless ($opt->{showunusednodes} || $opt->{"use-numbered-nodes"}) {
247 8         20 %used = map { $_->[0] => 1, } @{$self->{jumps}->{$table}};
  91         173  
  8         24  
248             }
249 10         25 foreach my $node (sort keys %{$self->{chains}->{$table}}) {
  10         78  
250             next unless ($used{$node}
251             || $opt->{showunusednodes}
252 97 100 100     313 || $opt->{"use-numbered-nodes"});
      100        
253 56 100 66     326 if (!$have_node{$node} && $node =~ $re_in) {
254 19 100       50 if ($opt->{"use-numbered-nodes"}) {
255 1   33     15 push @nodes, $self->{nodemap}->{$node} || qq("$node");
256             }
257             else {
258 18         46 push @nodes, qq("$node");
259             }
260 19         49 $have_node{$node} = 1;
261             }
262             }
263             }
264 10         61 return @nodes;
265             } # _internal_nodes()
266              
267             # _read_iptables_line($line)
268             #
269             # Reads the next line from iptables output and creates an entry in the rules
270             # and or jump table for it.
271             #
272             # Returns nothing.
273             #
274             sub _read_iptables_line {
275 283     283   595 my ($self,$line) = @_;
276 283 100       733 return if ($line =~ /^#.*$/);
277 269 100       570 return if ($line =~ /^COMMIT$/);
278 257         409 chomp;
279 257 100       1134 if ($line =~ /^\*(\S+)$/) {
    100          
    50          
280 16         90 $self->{last_table} = $1;
281 16         31 push @{$self->{tables}}, $1;
  16         61  
282 16         68 $self->{chains}->{$1} = {};
283 16         225 $self->{jumps}->{$1} = [];
284             }
285             elsif ($line =~ /^:(\S+)\s.+$/) {
286 110         435 $self->{chains}->{$self->{last_table}}->{$1} = { rules => [] };
287 110 100       280 unless ($self->{nodemap}->{$1}) {
288 96         244 $self->{nodemap}->{$1} = "node" . $self->{nn};
289 96         164 $self->{nn} += 1;
290             }
291             }
292             elsif ($line =~ /^-A\s(\S+)\s(.+)$/) {
293 131         315 my $chain = $1;
294 131         293 my $rule = $2;
295 131         205 my %opt;
296 131         261 my $last_table = $self->{last_table};
297 131         477 my ($ret, $args) = GetOptionsFromString($rule,\%opt,@optdefs);
298 131 100       405460 if ($ret) {
299 130   100     524 my $iface = $opt{'in-interface'} || '';
300 130   0     402 my $target = $opt{'jump'} || $opt{'goto'} || '';
301 130 100       530 unless ($target =~ /^(ACCEPT|DROP|REJECT)$/) {
302 94 50       337 unless ($self->{nodemap}->{$1}) {
303 0         0 $self->{nodemap}->{$1} = "node" . $self->{nn};
304 0         0 $self->{nn} += 1;
305             }
306 94         132 my $rn = scalar @{$self->{chains}{$last_table}->{$chain}->{rules}};
  94         240  
307 94         206 push @{$self->{jumps}->{$last_table}}, [ $chain, $target, $iface, $rn ];
  94         376  
308             }
309             }
310             else {
311 1         34 die "unknown argument in rule: $rule";
312             }
313 130         205 push @{$self->{chains}->{$last_table}->{$chain}->{rules}}, $rule;
  130         558  
314             }
315             else {
316 0         0 die "unrecognized line: $line";
317             }
318 256         996 return;
319             } # _read_iptables_line()
320              
321             1; # Magic true value required at end of module
322             __END__