File Coverage

blib/lib/OSPF/LSDB/ospfd.pm
Criterion Covered Total %
statement 227 241 94.1
branch 211 230 91.7
condition 6 6 100.0
subroutine 16 17 94.1
pod 2 11 18.1
total 462 505 91.4


line stmt bran cond sub pod time code
1             ##########################################################################
2             # Copyright (c) 2010-2022 Alexander Bluhm
3             #
4             # Permission to use, copy, modify, and distribute this software for any
5             # purpose with or without fee is hereby granted, provided that the above
6             # copyright notice and this permission notice appear in all copies.
7             #
8             # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9             # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10             # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11             # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12             # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13             # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14             # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15             ##########################################################################
16              
17 18     18   478892 use strict;
  18         85  
  18         437  
18 18     18   71 use warnings;
  18         25  
  18         731  
19              
20             =pod
21              
22             =head1 NAME
23              
24             OSPF::LSDB::ospfd - parse OpenBSD B link state database
25              
26             =head1 SYNOPSIS
27              
28             use OSPF::LSDB::ospfd;
29              
30             my $ospfd = OSPF::LSDB::ospfd-Enew();
31              
32             my $ospfd = OSPF::LSDB::ospfd-Enew(ssh => "user@host");
33              
34             $ospfd-Eparse(%files);
35              
36             =head1 DESCRIPTION
37              
38             The OSPF::LSDB::ospfd module parses the output of OpenBSD B
39             and fills the L base object.
40             The output of
41             C,
42             C,
43             C,
44             C,
45             C,
46             C
47             is needed.
48             It can be given as separate files or obtained dynamically.
49             In the latter case B is invoked if permissions are not
50             sufficient to run B.
51             If the object has been created with the C argument, the specified
52             user and host are used to login and run B there.
53              
54             There is only one public method:
55              
56             =cut
57              
58             package OSPF::LSDB::ospfd;
59 18     18   76 use base 'OSPF::LSDB';
  18         33  
  18         6581  
60 18     18   5875 use File::Slurp;
  18         215032  
  18         1079  
61 18     18   115 use Regexp::Common;
  18         31  
  18         172  
62 18         147 use fields qw(
63             ospfd ospfctl ospfsock showdb
64             selfid
65             router network summary boundary external
66 18     18   430773 );
  18         36  
67              
68             sub new {
69 16     16 1 706 my OSPF::LSDB::ospfd $self = OSPF::LSDB::new(@_);
70 16         42 $self->{ospfd} = "ospfd";
71 16         43 $self->{ospfctl} = "ospfctl";
72             # XXX hard coded routing domain 0
73 16         38 $self->{ospfsock} = "/var/run/ospfd.sock.0";
74             $self->{showdb} = {
75 16         84 router => "router",
76             network => "network",
77             summary => "summary",
78             boundary => "asbr",
79             external => "external",
80             };
81 16         49 return $self;
82             }
83              
84             # shortcut
85             my $IP = qr/$RE{net}{IPv4}{-keep}/;
86              
87             sub ospfctl_show {
88 0     0 0 0 my OSPF::LSDB::ospfd $self = shift;
89 0         0 my @cmd = ($self->{ospfctl}, "show", @_);
90 0 0       0 if ($self->{ssh}) {
    0          
91 0         0 unshift @cmd, "ssh", $self->{ssh};
92             } elsif (-S $self->{ospfsock}) {
93             # no doas if user is root or in wheel group
94             # srw-rw---- 1 root wheel 0 Jun 13 10:10 /var/run/ospfd.sock
95 0 0       0 unshift @cmd, "doas" unless -w $self->{ospfsock};
96             } else {
97 0         0 die "Control socket '$self->{ospfsock}' for $self->{ospfd} missing.\n";
98             }
99 0 0       0 open(my $fh, '-|', @cmd)
100             or die "Open pipe from '@cmd' failed: $!";
101 0         0 my @lines = $fh->getlines();
102 0 0       0 close($fh) or die $! ?
    0          
103             "Close pipe from '@cmd' failed: $!" :
104             "Command '@cmd' failed: $?\n";
105 0 0       0 return wantarray ? @lines : join("", @lines);
106             }
107              
108             sub read_files {
109 4     4 0 7 my OSPF::LSDB::ospfd $self = shift;
110 4         13 my %files = @_;
111 4         8 my $file = $files{selfid};
112 4 50       31 my @lines = $file ? read_file($file) : $self->ospfctl_show("summary");
113 4         668 $self->{selfid} = \@lines;
114 4         10 foreach (sort keys %{$self->{showdb}}) {
  4         27  
115 24         42 my $file = $files{$_};
116             my @lines = $file ? read_file($file) :
117 24 50       80 $self->ospfctl_show("database", $self->{showdb}{$_});
118 24         5450 $self->{$_} = \@lines;
119             }
120             }
121              
122             sub parse_self {
123 6     6 0 49 my OSPF::LSDB::ospfd $self = shift;
124 6         11 my($routerid, @areas);
125 6         11 foreach (@{$self->{selfid}}) {
  6         21  
126 99 100       872 if (/^Router ID: $IP$/) {
    100          
127 6         32 $routerid = $1;
128             } elsif (/^Area ID: $IP$/) {
129 10         31 push @areas, $1;
130             }
131             }
132 6         35 $self->{ospf}{self} = { routerid => $routerid, areas => \@areas };
133             }
134              
135             sub parse_router {
136 16     16 0 10478 my OSPF::LSDB::ospfd $self = shift;
137 16         32 my($area, $router, $link) = ("", "", "");
138 16         26 my(@routers, $lnum, $type, $r, $l);
139 16         17 foreach (@{$self->{router}}) {
  16         31  
140 1150 100 100     65367 if (/^ +Router Link States \(Area $IP\)$/) {
    100 100        
    100          
    100          
141 22 100       73 die "$_ Link $link of router $router in area $area not finished.\n"
142             if $l;
143 21 100       42 die "$_ Too few links at router $router in area $area.\n" if $lnum;
144 20 100       33 die "$_ Router $router in area $area not finished.\n" if $r;
145 19         35 $area = $1;
146 19         27 next;
147             } elsif (/^$/) {
148 193 100       311 if ($l) {
149 105 100       159 die "Link $link of router $router in area $area without type.\n"
150             if ! $type;
151 104         95 push @{$r->{$type.'s'}}, $l;
  104         233  
152 104         131 undef $type;
153 104         119 undef $l;
154             }
155 192 100       257 if (! $lnum) {
156 81         96 undef $r;
157             }
158 192         372 next;
159             } elsif (! $r && /^\w/) {
160 53 100       98 die "$_ No area for router defined.\n" if ! $area;
161 52         66 $router = "";
162 52         96 $r = { area => $area };
163 52         67 push @routers, $r;
164             } elsif (! $l && /^ {4}\w/) {
165 110 100       202 die "$_ Too many links at router $router in area $area.\n"
166             if ! $lnum;
167 109         123 $lnum--;
168 109         134 $link = "";
169 109         131 $l = {};
170             }
171 933 100       2727 if (/^LS age: $RE{num}{int}{-keep}$/) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
172 52         5522 $r->{age} = $1;
173             } elsif (/^LS Type: ([\w ()]+)$/) {
174 52 100       5058 die "$_ Type of router-LSA is $1 and not Router in area $area.\n"
175             if $1 ne "Router";
176             } elsif (/^Link State ID: $IP$/) {
177 51         4925 $r->{router} = $1;
178 51         255 $router = $1;
179             } elsif (/^Advertising Router: $IP$/) {
180 51         5094 $r->{routerid} = $1;
181             } elsif (/^LS Seq Number: (0x$RE{num}{hex})$/) {
182 51         9620 $r->{sequence} = $1;
183             } elsif (/^Flags: ([-*|\w]*)$/) {
184 48         8864 my $flags = $1;
185 48         86 foreach (qw(V E B)) {
186 144 100       1251 $r->{bits}{$_} = $flags =~ /\b$_\b/ ? 1 : 0;
187             }
188             } elsif (/^Number of Links: $RE{num}{int}{-keep}$/) {
189 48         13894 $lnum = $1;
190             } elsif (/^ Link connected to: ([\w -]+)$/) {
191 108 50       29944 if ($1 eq "Point-to-Point") {
    100          
    100          
    50          
192 0         0 $type = "pointtopoint";
193             } elsif ($1 eq "Transit Network") {
194 79         798 $type = "transit";
195             } elsif ($1 eq "Stub Network") {
196 28         244 $type = "stub";
197             } elsif ($1 eq "Virtual Link") {
198 0         0 $type = "virtual";
199             } else {
200 1         10 die "$_ Unknown link type $1 at router $router ".
201             "in area $area.\n";
202             }
203             } elsif (/^ Link ID \(Neighbors Router ID\): $IP$/) {
204 0         0 $l->{routerid} = $1;
205 0         0 $link = $1;
206             } elsif (/^ Link ID \(Designated Router address\): $IP$/) {
207 79         22113 $l->{address} = $1;
208 79         743 $link = $1;
209             } elsif (/^ Link ID \(Network ID\): $IP$/) {
210 29         7930 $l->{network} = $1;
211 29         268 $link = $1;
212             } elsif (/^ Link Data \(Router Interface address\): $IP$/) {
213 79         22894 $l->{interface} = $1;
214             } elsif (/^ Link Data \(Network Mask\): $IP$/) {
215 29         8531 $l->{netmask} = $1;
216             } elsif (/^ Metric: $RE{num}{int}{-keep}$/) {
217 105         40663 $l->{metric} = $1;
218             } elsif (/^ {4}\w/) {
219 1         439 die "$_ Unknown line at link $link of router $router ".
220             "in area $area.\n";
221             } elsif (! /^(Options|Checksum|Length):/) {
222 1         370 die "$_ Unknown line at router $router in area $area.\n";
223             }
224             }
225 6 100       31 die "Link $link of router $router in area $area not finished.\n" if $l;
226 5 100       22 die "Too few links at router $router in area $area.\n" if $lnum;
227 4 100       18 die "Router $router in area $area not finished.\n" if $r;
228 3         19 $self->{ospf}{database}{routers} = \@routers;
229             }
230              
231             sub parse_network {
232 11     11 0 7246 my OSPF::LSDB::ospfd $self = shift;
233 11         27 my($area, $network) = ("", "");
234 11         14 my(@networks, $attachments, $rnum, $n);
235 11         13 foreach (@{$self->{network}}) {
  11         25  
236 518 100       36262 if (/^ +Net Link States \(Area $IP\)$/) {
    100          
    100          
237 16 100       39 die "$_ Attached routers of network $network in area $area ".
238             "not finished.\n" if $attachments;
239 15 100       31 die "$_ Network $network in area $area not finished.\n" if $n;
240 14         28 $area = $1;
241 14         20 next;
242             } elsif (/^$/) {
243 64 100       108 die "$_ Too few attached routers at network $network ".
244             "in area $area.\n" if $rnum;
245 63         67 undef $attachments;
246 63         61 undef $n;
247 63         97 next;
248             } elsif (! $n) {
249 41 100       104 die "$_ No area for network defined.\n" if ! $area;
250 40         47 $network = "";
251 40         68 $n = { area => $area };
252 40         63 push @networks, $n;
253             }
254 437 100       1265 if (/^LS age: $RE{num}{int}{-keep}$/) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
255 40         4086 $n->{age} = $1;
256             } elsif (/^LS Type: ([\w ()]+)$/) {
257 40 100       4021 die "$_ Type of network-LSA is $1 and not Network in area $area.\n"
258             if $1 ne "Network";
259             } elsif (/^Link State ID: $IP \(address of Designated Router\)$/) {
260 39         3826 $n->{address} = $1;
261 39         188 $network = $1;
262             } elsif (/^Advertising Router: $IP$/) {
263 39         3794 $n->{routerid} = $1;
264             } elsif (/^LS Seq Number: (0x$RE{num}{hex})$/) {
265 39         8353 $n->{sequence} = $1;
266             } elsif (/^Network Mask: $IP$/) {
267 36         6943 $n->{netmask} = $1;
268             } elsif (/^Number of Routers: $RE{num}{int}{-keep}$/) {
269 4         1084 $rnum = $1;
270             } elsif (/^ Attached Router: $IP$/) {
271 86 100       24159 if (! $attachments) {
272 36         57 $attachments = [];
273 36         57 $n->{attachments} = $attachments;
274             }
275 86 100       135 $rnum-- if defined $rnum ;
276 86         924 push @$attachments, { routerid => $1 };
277             } elsif (! /^(Options|Checksum|Length):/) {
278 1         280 die "$_ Unknown line at network $network in area $area.\n";
279             }
280             }
281 5 100       22 die "Attached routers of network $network in area $area not finished.\n"
282             if $attachments;
283 4 100       16 die "Network $network in area $area not finished.\n" if $n;
284 3         15 $self->{ospf}{database}{networks} = \@networks;
285             }
286              
287             sub parse_summary {
288 8     8 0 4355 my OSPF::LSDB::ospfd $self = shift;
289 8         17 my($area, $summary) = ("", "");
290 8         12 my(@summarys, $s);
291 8         11 foreach (@{$self->{summary}}) {
  8         19  
292 1391 100       110524 if (/^ +Summary Net Link States \(Area $IP\)$/) {
    100          
    100          
293 12 100       39 die "$_ Summary $summary in area $area not finished.\n" if $s;
294 11         32 $area = $1;
295 11         20 next;
296             } elsif (/^$/) {
297 145         173 undef $s;
298 145         238 next;
299             } elsif (! $s) {
300 126 100       208 die "$_ No area for summary defined.\n" if ! $area;
301 125         133 $summary = "";
302 125         186 $s = { area => $area };
303 125         191 push @summarys, $s;
304             }
305 1233 100       3433 if (/^LS age: $RE{num}{int}{-keep}$/) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
306 125         11956 $s->{age} = $1;
307             } elsif (/^LS Type: ([\w ()]+)$/) {
308 125 100       12900 die "$_ Type of summary-LSA is $1 and not Summary (Network) ".
309             "in area $area.\n" if $1 ne "Summary (Network)";
310             } elsif (/^Link State ID: $IP \(Network ID\)$/) {
311 124         11982 $s->{address} = $1;
312 124         565 $summary = $1;
313             } elsif (/^Advertising Router: $IP$/) {
314 124         12757 $s->{routerid} = $1;
315             } elsif (/^LS Seq Number: (0x$RE{num}{hex})$/) {
316 124         22904 $s->{sequence} = $1;
317             } elsif (/^Network Mask: $IP$/) {
318 121         23408 $s->{netmask} = $1;
319             } elsif (/^Metric: $RE{num}{int}{-keep}$/) {
320 121         33779 $s->{metric} = $1;
321             } elsif (! /^(Options|Checksum|Length):/) {
322 1         279 die "$_ Unknown line at summary $summary in area $area.\n";
323             }
324             }
325 4 100       17 die "Summary $summary in area $area not finished.\n" if $s;
326 3         15 $self->{ospf}{database}{summarys} = \@summarys;
327             }
328              
329             sub parse_boundary {
330 8     8 0 4522 my OSPF::LSDB::ospfd $self = shift;
331 8         19 my($area, $boundary) = ("", "");
332 8         11 my(@boundarys, $b);
333 8         11 foreach (@{$self->{boundary}}) {
  8         18  
334 731 100       74978 if (/^ +Summary Router Link States \(Area $IP\)$/) {
    100          
    100          
335 12 100       36 die "$_ Boundary $boundary in area $area not finished.\n" if $b;
336 11         22 $area = $1;
337 11         15 next;
338             } elsif (/^$/) {
339 85         100 undef $b;
340 85         140 next;
341             } elsif (! $b) {
342 66 100       102 die "$_ No area for boundary defined.\n" if ! $area;
343 65         72 $boundary = "";
344 65         115 $b = { area => $area };
345 65         176 push @boundarys, $b;
346             }
347 633 100       1791 if (/^LS age: $RE{num}{int}{-keep}$/) {
    100          
    100          
    100          
    100          
    100          
    100          
348 65         6938 $b->{age} = $1;
349             } elsif (/^LS Type: ([\w ()]+)$/) {
350 65 100       6442 die "$_ Type of boundary-LSA is $1 and not Summary (Router) ".
351             "in area $area.\n" if $1 ne "Summary (Router)";
352             } elsif (/^Link State ID: $IP \(ASBR Router ID\)$/) {
353 64         6420 $b->{asbrouter} = $1;
354 64         298 $boundary = $1;
355             } elsif (/^Advertising Router: $IP$/) {
356 64         6268 $b->{routerid} = $1;
357             } elsif (/^LS Seq Number: (0x$RE{num}{hex})$/) {
358 64         12281 $b->{sequence} = $1;
359             } elsif (/^Metric: $RE{num}{int}{-keep}$/) {
360 61         17054 $b->{metric} = $1;
361             } elsif (! /^(Options|Checksum|Length|Network Mask):/) {
362 1         267 die "$_ Unknown line at boundary $boundary in area $area.\n";
363             }
364             }
365 4 100       20 die "Boundary $boundary in area $area not finished.\n" if $b;
366 3         16 $self->{ospf}{database}{boundarys} = \@boundarys;
367             }
368              
369             sub parse_external {
370 8     8 0 4457 my OSPF::LSDB::ospfd $self = shift;
371 8         13 my $external = "";
372 8         10 my(@externals, $e);
373 8         11 foreach (@{$self->{external}}) {
  8         21  
374 1981 100       164958 if (/^ +Type-5 AS External Link States$/) {
    100          
    100          
375 10 100       28 die "$_ External $external not finished.\n" if $e;
376 9 100       25 die "$_ Too many external sections.\n", if @externals;
377 8         14 next;
378             } elsif (/^$/) {
379 154         173 undef $e;
380 154         204 next;
381             } elsif (! $e) {
382 142         187 $external = "";
383 142         171 $e = {};
384 142         209 push @externals, $e;
385             }
386 1817 100       5058 if (/^LS age: $RE{num}{int}{-keep}$/) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
387 142         14141 $e->{age} = $1;
388             } elsif (/^LS Type: ([\w ()]+)$/) {
389 142 100       14522 die "$_ Type of external-LSA is $1 and not AS External.\n"
390             if $1 ne "AS External";
391             } elsif (/^Link State ID: $IP \(External Network Number\)$/) {
392 141         13703 $e->{address} = $1;
393 141         708 $external = $1;
394             } elsif (/^Advertising Router: $IP$/) {
395 141         14052 $e->{routerid} = $1;
396             } elsif (/^LS Seq Number: (0x$RE{num}{hex})$/) {
397 141         26738 $e->{sequence} = $1;
398             } elsif (/^Network Mask: $IP$/) {
399 138         26643 $e->{netmask} = $1;
400             } elsif (/^ Metric type: ([1-2])$/) {
401 138         26215 $e->{type} = $1;
402             } elsif (/^ Metric: $RE{num}{int}{-keep}$/) {
403 138         39026 $e->{metric} = $1;
404             } elsif (/^ Forwarding Address: $IP$/) {
405 138         39018 $e->{forward} = $1;
406             } elsif (! /^(Options|Checksum|Length| External Route Tag):/) {
407 1         270 die "$_ Unknown line at external $external.\n";
408             }
409             }
410 4 100       19 die "External $external not finished.\n" if $e;
411 3         17 $self->{ospf}{database}{externals} = \@externals;
412             }
413              
414             sub parse_lsdb {
415 4     4 0 8 my OSPF::LSDB::ospfd $self = shift;
416 4         17 $self->parse_router();
417 4         39 $self->parse_network();
418 4         20 $self->parse_summary();
419 4         18 $self->parse_boundary();
420 4         16 $self->parse_external();
421             }
422              
423             =pod
424              
425             =over 4
426              
427             =item $self-Eparse(%files)
428              
429             This function takes a hash with file names as value containing the
430             B output data.
431             The hash keys are named C, C, C, C,
432             C, C.
433             If a hash entry is missing, B is run instead to obtain the
434             information dynamically.
435              
436             The complete OSPF link state database is stored in the B field
437             of the base class.
438              
439             =back
440              
441             =cut
442              
443             sub parse {
444 4     4 1 12 my OSPF::LSDB::ospfd $self = shift;
445 4         17 my %files = @_;
446 4         27 $self->read_files(%files);
447 4         29 $self->parse_self();
448 4         20 $self->parse_lsdb();
449 4         36 $self->{ospf}{ipv6} = 0;
450             }
451              
452             =pod
453              
454             This module has been tested with OpenBSD 4.8 and 5.1.
455             If it works with other versions is unknown.
456              
457             =head1 ERRORS
458              
459             The methods die if any error occurs.
460              
461             =head1 SEE ALSO
462              
463             L,
464             L
465              
466             L
467              
468             =head1 AUTHORS
469              
470             Alexander Bluhm
471              
472             =cut
473              
474             1;