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