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   505954 use strict;
  18         86  
  18         825  
18 18     18   78 use warnings;
  18         27  
  18         853  
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   96 use base 'OSPF::LSDB';
  18         35  
  18         7156  
60 18     18   6801 use File::Slurp;
  18         248791  
  18         1141  
61 18     18   126 use Regexp::Common;
  18         34  
  18         162  
62 18         198 use fields qw(
63             ospfd ospfctl ospfsock showdb
64             selfid
65             router network summary boundary external
66 18     18   498394 );
  18         41  
67              
68             sub new {
69 16     16 1 831 my OSPF::LSDB::ospfd $self = OSPF::LSDB::new(@_);
70 16         50 $self->{ospfd} = "ospfd";
71 16         43 $self->{ospfctl} = "ospfctl";
72             # XXX hard coded routing domain 0
73 16         39 $self->{ospfsock} = "/var/run/ospfd.sock.0";
74             $self->{showdb} = {
75 16         85 router => "router",
76             network => "network",
77             summary => "summary",
78             boundary => "asbr",
79             external => "external",
80             };
81 16         51 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 8 my OSPF::LSDB::ospfd $self = shift;
110 4         14 my %files = @_;
111 4         13 my $file = $files{selfid};
112 4 50       32 my @lines = $file ? read_file($file) : $self->ospfctl_show("summary");
113 4         902 $self->{selfid} = \@lines;
114 4         12 foreach (sort keys %{$self->{showdb}}) {
  4         37  
115 24         48 my $file = $files{$_};
116             my @lines = $file ? read_file($file) :
117 24 50       81 $self->ospfctl_show("database", $self->{showdb}{$_});
118 24         6275 $self->{$_} = \@lines;
119             }
120             }
121              
122             sub parse_self {
123 6     6 0 41 my OSPF::LSDB::ospfd $self = shift;
124 6         15 my($routerid, @areas);
125 6         12 foreach (@{$self->{selfid}}) {
  6         23  
126 99 100       1130 if (/^Router ID: $IP$/) {
    100          
127 6         37 $routerid = $1;
128             } elsif (/^Area ID: $IP$/) {
129 10         38 push @areas, $1;
130             }
131             }
132 6         39 $self->{ospf}{self} = { routerid => $routerid, areas => \@areas };
133             }
134              
135             sub parse_router {
136 16     16 0 11546 my OSPF::LSDB::ospfd $self = shift;
137 16         44 my($area, $router, $link) = ("", "", "");
138 16         24 my(@routers, $lnum, $type, $r, $l);
139 16         26 foreach (@{$self->{router}}) {
  16         33  
140 1150 100 100     75649 if (/^ +Router Link States \(Area $IP\)$/) {
    100 100        
    100          
    100          
141 22 100       100 die "$_ Link $link of router $router in area $area not finished.\n"
142             if $l;
143 21 100       49 die "$_ Too few links at router $router in area $area.\n" if $lnum;
144 20 100       41 die "$_ Router $router in area $area not finished.\n" if $r;
145 19         43 $area = $1;
146 19         32 next;
147             } elsif (/^$/) {
148 193 100       514 if ($l) {
149 105 100       197 die "Link $link of router $router in area $area without type.\n"
150             if ! $type;
151 104         150 push @{$r->{$type.'s'}}, $l;
  104         326  
152 104         177 undef $type;
153 104         146 undef $l;
154             }
155 192 100       350 if (! $lnum) {
156 81         109 undef $r;
157             }
158 192         393 next;
159             } elsif (! $r && /^\w/) {
160 53 100       138 die "$_ No area for router defined.\n" if ! $area;
161 52         79 $router = "";
162 52         126 $r = { area => $area };
163 52         91 push @routers, $r;
164             } elsif (! $l && /^ {4}\w/) {
165 110 100       252 die "$_ Too many links at router $router in area $area.\n"
166             if ! $lnum;
167 109         175 $lnum--;
168 109         172 $link = "";
169 109         173 $l = {};
170             }
171 933 100       3182 if (/^LS age: $RE{num}{int}{-keep}$/) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
172 52         6515 $r->{age} = $1;
173             } elsif (/^LS Type: ([\w ()]+)$/) {
174 52 100       5939 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         5686 $r->{router} = $1;
178 51         293 $router = $1;
179             } elsif (/^Advertising Router: $IP$/) {
180 51         5862 $r->{routerid} = $1;
181             } elsif (/^LS Seq Number: (0x$RE{num}{hex})$/) {
182 51         11497 $r->{sequence} = $1;
183             } elsif (/^Flags: ([-*|\w]*)$/) {
184 48         10247 my $flags = $1;
185 48         100 foreach (qw(V E B)) {
186 144 100       1764 $r->{bits}{$_} = $flags =~ /\b$_\b/ ? 1 : 0;
187             }
188             } elsif (/^Number of Links: $RE{num}{int}{-keep}$/) {
189 48         15721 $lnum = $1;
190             } elsif (/^ Link connected to: ([\w -]+)$/) {
191 108 50       34751 if ($1 eq "Point-to-Point") {
    100          
    100          
    50          
192 0         0 $type = "pointtopoint";
193             } elsif ($1 eq "Transit Network") {
194 79         813 $type = "transit";
195             } elsif ($1 eq "Stub Network") {
196 28         290 $type = "stub";
197             } elsif ($1 eq "Virtual Link") {
198 0         0 $type = "virtual";
199             } else {
200 1         12 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         25255 $l->{address} = $1;
208 79         861 $link = $1;
209             } elsif (/^ Link ID \(Network ID\): $IP$/) {
210 29         9388 $l->{network} = $1;
211 29         317 $link = $1;
212             } elsif (/^ Link Data \(Router Interface address\): $IP$/) {
213 79         26068 $l->{interface} = $1;
214             } elsif (/^ Link Data \(Network Mask\): $IP$/) {
215 29         9702 $l->{netmask} = $1;
216             } elsif (/^ Metric: $RE{num}{int}{-keep}$/) {
217 105         46724 $l->{metric} = $1;
218             } elsif (/^ {4}\w/) {
219 1         432 die "$_ Unknown line at link $link of router $router ".
220             "in area $area.\n";
221             } elsif (! /^(Options|Checksum|Length):/) {
222 1         410 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       25 die "Too few links at router $router in area $area.\n" if $lnum;
227 4 100       20 die "Router $router in area $area not finished.\n" if $r;
228 3         21 $self->{ospf}{database}{routers} = \@routers;
229             }
230              
231             sub parse_network {
232 11     11 0 7578 my OSPF::LSDB::ospfd $self = shift;
233 11         28 my($area, $network) = ("", "");
234 11         37 my(@networks, $attachments, $rnum, $n);
235 11         15 foreach (@{$self->{network}}) {
  11         30  
236 518 100       41701 if (/^ +Net Link States \(Area $IP\)$/) {
    100          
    100          
237 16 100       48 die "$_ Attached routers of network $network in area $area ".
238             "not finished.\n" if $attachments;
239 15 100       55 die "$_ Network $network in area $area not finished.\n" if $n;
240 14         34 $area = $1;
241 14         27 next;
242             } elsif (/^$/) {
243 64 100       161 die "$_ Too few attached routers at network $network ".
244             "in area $area.\n" if $rnum;
245 63         90 undef $attachments;
246 63         152 undef $n;
247 63         152 next;
248             } elsif (! $n) {
249 41 100       148 die "$_ No area for network defined.\n" if ! $area;
250 40         65 $network = "";
251 40         94 $n = { area => $area };
252 40         84 push @networks, $n;
253             }
254 437 100       1441 if (/^LS age: $RE{num}{int}{-keep}$/) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
255 40         4894 $n->{age} = $1;
256             } elsif (/^LS Type: ([\w ()]+)$/) {
257 40 100       4517 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         4287 $n->{address} = $1;
261 39         222 $network = $1;
262             } elsif (/^Advertising Router: $IP$/) {
263 39         4531 $n->{routerid} = $1;
264             } elsif (/^LS Seq Number: (0x$RE{num}{hex})$/) {
265 39         8412 $n->{sequence} = $1;
266             } elsif (/^Network Mask: $IP$/) {
267 36         7872 $n->{netmask} = $1;
268             } elsif (/^Number of Routers: $RE{num}{int}{-keep}$/) {
269 4         1241 $rnum = $1;
270             } elsif (/^ Attached Router: $IP$/) {
271 86 100       27400 if (! $attachments) {
272 36         91 $attachments = [];
273 36         79 $n->{attachments} = $attachments;
274             }
275 86 100       150 $rnum-- if defined $rnum ;
276 86         1104 push @$attachments, { routerid => $1 };
277             } elsif (! /^(Options|Checksum|Length):/) {
278 1         360 die "$_ Unknown line at network $network in area $area.\n";
279             }
280             }
281 5 100       29 die "Attached routers of network $network in area $area not finished.\n"
282             if $attachments;
283 4 100       20 die "Network $network in area $area not finished.\n" if $n;
284 3         18 $self->{ospf}{database}{networks} = \@networks;
285             }
286              
287             sub parse_summary {
288 8     8 0 4971 my OSPF::LSDB::ospfd $self = shift;
289 8         21 my($area, $summary) = ("", "");
290 8         13 my(@summarys, $s);
291 8         13 foreach (@{$self->{summary}}) {
  8         23  
292 1391 100       126923 if (/^ +Summary Net Link States \(Area $IP\)$/) {
    100          
    100          
293 12 100       42 die "$_ Summary $summary in area $area not finished.\n" if $s;
294 11         30 $area = $1;
295 11         24 next;
296             } elsif (/^$/) {
297 145         238 undef $s;
298 145         366 next;
299             } elsif (! $s) {
300 126 100       297 die "$_ No area for summary defined.\n" if ! $area;
301 125         195 $summary = "";
302 125         267 $s = { area => $area };
303 125         270 push @summarys, $s;
304             }
305 1233 100       4069 if (/^LS age: $RE{num}{int}{-keep}$/) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
306 125         14173 $s->{age} = $1;
307             } elsif (/^LS Type: ([\w ()]+)$/) {
308 125 100       14267 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         13697 $s->{address} = $1;
312 124         703 $summary = $1;
313             } elsif (/^Advertising Router: $IP$/) {
314 124         14384 $s->{routerid} = $1;
315             } elsif (/^LS Seq Number: (0x$RE{num}{hex})$/) {
316 124         27180 $s->{sequence} = $1;
317             } elsif (/^Network Mask: $IP$/) {
318 121         26521 $s->{netmask} = $1;
319             } elsif (/^Metric: $RE{num}{int}{-keep}$/) {
320 121         39723 $s->{metric} = $1;
321             } elsif (! /^(Options|Checksum|Length):/) {
322 1         307 die "$_ Unknown line at summary $summary in area $area.\n";
323             }
324             }
325 4 100       22 die "Summary $summary in area $area not finished.\n" if $s;
326 3         19 $self->{ospf}{database}{summarys} = \@summarys;
327             }
328              
329             sub parse_boundary {
330 8     8 0 3799 my OSPF::LSDB::ospfd $self = shift;
331 8         18 my($area, $boundary) = ("", "");
332 8         15 my(@boundarys, $b);
333 8         12 foreach (@{$self->{boundary}}) {
  8         23  
334 731 100       85694 if (/^ +Summary Router Link States \(Area $IP\)$/) {
    100          
    100          
335 12 100       39 die "$_ Boundary $boundary in area $area not finished.\n" if $b;
336 11         27 $area = $1;
337 11         23 next;
338             } elsif (/^$/) {
339 85         141 undef $b;
340 85         194 next;
341             } elsif (! $b) {
342 66 100       183 die "$_ No area for boundary defined.\n" if ! $area;
343 65         100 $boundary = "";
344 65         147 $b = { area => $area };
345 65         137 push @boundarys, $b;
346             }
347 633 100       2108 if (/^LS age: $RE{num}{int}{-keep}$/) {
    100          
    100          
    100          
    100          
    100          
    100          
348 65         7718 $b->{age} = $1;
349             } elsif (/^LS Type: ([\w ()]+)$/) {
350 65 100       7353 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         7024 $b->{asbrouter} = $1;
354 64         340 $boundary = $1;
355             } elsif (/^Advertising Router: $IP$/) {
356 64         7483 $b->{routerid} = $1;
357             } elsif (/^LS Seq Number: (0x$RE{num}{hex})$/) {
358 64         14126 $b->{sequence} = $1;
359             } elsif (/^Metric: $RE{num}{int}{-keep}$/) {
360 61         19924 $b->{metric} = $1;
361             } elsif (! /^(Options|Checksum|Length|Network Mask):/) {
362 1         305 die "$_ Unknown line at boundary $boundary in area $area.\n";
363             }
364             }
365 4 100       25 die "Boundary $boundary in area $area not finished.\n" if $b;
366 3         22 $self->{ospf}{database}{boundarys} = \@boundarys;
367             }
368              
369             sub parse_external {
370 8     8 0 3844 my OSPF::LSDB::ospfd $self = shift;
371 8         18 my $external = "";
372 8         13 my(@externals, $e);
373 8         15 foreach (@{$self->{external}}) {
  8         23  
374 1981 100       190429 if (/^ +Type-5 AS External Link States$/) {
    100          
    100          
375 10 100       35 die "$_ External $external not finished.\n" if $e;
376 9 100       24 die "$_ Too many external sections.\n", if @externals;
377 8         16 next;
378             } elsif (/^$/) {
379 154         324 undef $e;
380 154         374 next;
381             } elsif (! $e) {
382 142         312 $external = "";
383 142         297 $e = {};
384 142         368 push @externals, $e;
385             }
386 1817 100       5868 if (/^LS age: $RE{num}{int}{-keep}$/) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
387 142         16440 $e->{age} = $1;
388             } elsif (/^LS Type: ([\w ()]+)$/) {
389 142 100       16278 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         16131 $e->{address} = $1;
393 141         832 $external = $1;
394             } elsif (/^Advertising Router: $IP$/) {
395 141         16370 $e->{routerid} = $1;
396             } elsif (/^LS Seq Number: (0x$RE{num}{hex})$/) {
397 141         31063 $e->{sequence} = $1;
398             } elsif (/^Network Mask: $IP$/) {
399 138         30122 $e->{netmask} = $1;
400             } elsif (/^ Metric type: ([1-2])$/) {
401 138         30557 $e->{type} = $1;
402             } elsif (/^ Metric: $RE{num}{int}{-keep}$/) {
403 138         45517 $e->{metric} = $1;
404             } elsif (/^ Forwarding Address: $IP$/) {
405 138         45890 $e->{forward} = $1;
406             } elsif (! /^(Options|Checksum|Length| External Route Tag):/) {
407 1         325 die "$_ Unknown line at external $external.\n";
408             }
409             }
410 4 100       24 die "External $external not finished.\n" if $e;
411 3         23 $self->{ospf}{database}{externals} = \@externals;
412             }
413              
414             sub parse_lsdb {
415 4     4 0 10 my OSPF::LSDB::ospfd $self = shift;
416 4         18 $self->parse_router();
417 4         31 $self->parse_network();
418 4         23 $self->parse_summary();
419 4         23 $self->parse_boundary();
420 4         20 $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 15 my OSPF::LSDB::ospfd $self = shift;
445 4         23 my %files = @_;
446 4         34 $self->read_files(%files);
447 4         37 $self->parse_self();
448 4         24 $self->parse_lsdb();
449 4         40 $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;