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   509656 use strict;
  18         85  
  18         475  
18 18     18   78 use warnings;
  18         27  
  18         719  
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   83 use base 'OSPF::LSDB';
  18         43  
  18         7320  
60 18     18   15985 use File::Slurp;
  18         252905  
  18         1323  
61 18     18   137 use Regexp::Common;
  18         33  
  18         162  
62 18         151 use fields qw(
63             ospfd ospfctl ospfsock showdb
64             selfid
65             router network summary boundary external
66 18     18   509371 );
  18         42  
67              
68             sub new {
69 16     16 1 960 my OSPF::LSDB::ospfd $self = OSPF::LSDB::new(@_);
70 16         52 $self->{ospfd} = "ospfd";
71 16         42 $self->{ospfctl} = "ospfctl";
72             # XXX hard coded routing domain 0
73 16         42 $self->{ospfsock} = "/var/run/ospfd.sock.0";
74             $self->{showdb} = {
75 16         94 router => "router",
76             network => "network",
77             summary => "summary",
78             boundary => "asbr",
79             external => "external",
80             };
81 16         68 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         14 my %files = @_;
111 4         12 my $file = $files{selfid};
112 4 50       26 my @lines = $file ? read_file($file) : $self->ospfctl_show("summary");
113 4         702 $self->{selfid} = \@lines;
114 4         10 foreach (sort keys %{$self->{showdb}}) {
  4         32  
115 24         47 my $file = $files{$_};
116             my @lines = $file ? read_file($file) :
117 24 50       78 $self->ospfctl_show("database", $self->{showdb}{$_});
118 24         6318 $self->{$_} = \@lines;
119             }
120             }
121              
122             sub parse_self {
123 6     6 0 31 my OSPF::LSDB::ospfd $self = shift;
124 6         14 my($routerid, @areas);
125 6         11 foreach (@{$self->{selfid}}) {
  6         20  
126 99 100       1004 if (/^Router ID: $IP$/) {
    100          
127 6         46 $routerid = $1;
128             } elsif (/^Area ID: $IP$/) {
129 10         42 push @areas, $1;
130             }
131             }
132 6         36 $self->{ospf}{self} = { routerid => $routerid, areas => \@areas };
133             }
134              
135             sub parse_router {
136 16     16 0 11595 my OSPF::LSDB::ospfd $self = shift;
137 16         41 my($area, $router, $link) = ("", "", "");
138 16         25 my(@routers, $lnum, $type, $r, $l);
139 16         21 foreach (@{$self->{router}}) {
  16         34  
140 1150 100 100     76035 if (/^ +Router Link States \(Area $IP\)$/) {
    100 100        
    100          
    100          
141 22 100       97 die "$_ Link $link of router $router in area $area not finished.\n"
142             if $l;
143 21 100       53 die "$_ Too few links at router $router in area $area.\n" if $lnum;
144 20 100       39 die "$_ Router $router in area $area not finished.\n" if $r;
145 19         43 $area = $1;
146 19         35 next;
147             } elsif (/^$/) {
148 193 100       385 if ($l) {
149 105 100       220 die "Link $link of router $router in area $area without type.\n"
150             if ! $type;
151 104         129 push @{$r->{$type.'s'}}, $l;
  104         326  
152 104         169 undef $type;
153 104         135 undef $l;
154             }
155 192 100       352 if (! $lnum) {
156 81         113 undef $r;
157             }
158 192         385 next;
159             } elsif (! $r && /^\w/) {
160 53 100       138 die "$_ No area for router defined.\n" if ! $area;
161 52         118 $router = "";
162 52         124 $r = { area => $area };
163 52         112 push @routers, $r;
164             } elsif (! $l && /^ {4}\w/) {
165 110 100       272 die "$_ Too many links at router $router in area $area.\n"
166             if ! $lnum;
167 109         181 $lnum--;
168 109         174 $link = "";
169 109         161 $l = {};
170             }
171 933 100       3158 if (/^LS age: $RE{num}{int}{-keep}$/) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
172 52         6463 $r->{age} = $1;
173             } elsif (/^LS Type: ([\w ()]+)$/) {
174 52 100       6091 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         5643 $r->{router} = $1;
178 51         293 $router = $1;
179             } elsif (/^Advertising Router: $IP$/) {
180 51         5875 $r->{routerid} = $1;
181             } elsif (/^LS Seq Number: (0x$RE{num}{hex})$/) {
182 51         11788 $r->{sequence} = $1;
183             } elsif (/^Flags: ([-*|\w]*)$/) {
184 48         10082 my $flags = $1;
185 48         115 foreach (qw(V E B)) {
186 144 100       1789 $r->{bits}{$_} = $flags =~ /\b$_\b/ ? 1 : 0;
187             }
188             } elsif (/^Number of Links: $RE{num}{int}{-keep}$/) {
189 48         15916 $lnum = $1;
190             } elsif (/^ Link connected to: ([\w -]+)$/) {
191 108 50       34397 if ($1 eq "Point-to-Point") {
    100          
    100          
    50          
192 0         0 $type = "pointtopoint";
193             } elsif ($1 eq "Transit Network") {
194 79         810 $type = "transit";
195             } elsif ($1 eq "Stub Network") {
196 28         287 $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         25308 $l->{address} = $1;
208 79         868 $link = $1;
209             } elsif (/^ Link ID \(Network ID\): $IP$/) {
210 29         9281 $l->{network} = $1;
211 29         298 $link = $1;
212             } elsif (/^ Link Data \(Router Interface address\): $IP$/) {
213 79         26004 $l->{interface} = $1;
214             } elsif (/^ Link Data \(Network Mask\): $IP$/) {
215 29         9681 $l->{netmask} = $1;
216             } elsif (/^ Metric: $RE{num}{int}{-keep}$/) {
217 105         46516 $l->{metric} = $1;
218             } elsif (/^ {4}\w/) {
219 1         444 die "$_ Unknown line at link $link of router $router ".
220             "in area $area.\n";
221             } elsif (! /^(Options|Checksum|Length):/) {
222 1         442 die "$_ Unknown line at router $router in area $area.\n";
223             }
224             }
225 6 100       32 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       20 die "Router $router in area $area not finished.\n" if $r;
228 3         20 $self->{ospf}{database}{routers} = \@routers;
229             }
230              
231             sub parse_network {
232 11     11 0 7377 my OSPF::LSDB::ospfd $self = shift;
233 11         28 my($area, $network) = ("", "");
234 11         19 my(@networks, $attachments, $rnum, $n);
235 11         19 foreach (@{$self->{network}}) {
  11         28  
236 518 100       40367 if (/^ +Net Link States \(Area $IP\)$/) {
    100          
    100          
237 16 100       50 die "$_ Attached routers of network $network in area $area ".
238             "not finished.\n" if $attachments;
239 15 100       39 die "$_ Network $network in area $area not finished.\n" if $n;
240 14         39 $area = $1;
241 14         24 next;
242             } elsif (/^$/) {
243 64 100       158 die "$_ Too few attached routers at network $network ".
244             "in area $area.\n" if $rnum;
245 63         101 undef $attachments;
246 63         77 undef $n;
247 63         142 next;
248             } elsif (! $n) {
249 41 100       121 die "$_ No area for network defined.\n" if ! $area;
250 40         69 $network = "";
251 40         91 $n = { area => $area };
252 40         81 push @networks, $n;
253             }
254 437 100       1412 if (/^LS age: $RE{num}{int}{-keep}$/) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
255 40         4770 $n->{age} = $1;
256             } elsif (/^LS Type: ([\w ()]+)$/) {
257 40 100       4588 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         4414 $n->{address} = $1;
261 39         234 $network = $1;
262             } elsif (/^Advertising Router: $IP$/) {
263 39         4411 $n->{routerid} = $1;
264             } elsif (/^LS Seq Number: (0x$RE{num}{hex})$/) {
265 39         8605 $n->{sequence} = $1;
266             } elsif (/^Network Mask: $IP$/) {
267 36         8267 $n->{netmask} = $1;
268             } elsif (/^Number of Routers: $RE{num}{int}{-keep}$/) {
269 4         1328 $rnum = $1;
270             } elsif (/^ Attached Router: $IP$/) {
271 86 100       27470 if (! $attachments) {
272 36         89 $attachments = [];
273 36         87 $n->{attachments} = $attachments;
274             }
275 86 100       154 $rnum-- if defined $rnum ;
276 86         1188 push @$attachments, { routerid => $1 };
277             } elsif (! /^(Options|Checksum|Length):/) {
278 1         315 die "$_ Unknown line at network $network in area $area.\n";
279             }
280             }
281 5 100       27 die "Attached routers of network $network in area $area not finished.\n"
282             if $attachments;
283 4 100       19 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 4773 my OSPF::LSDB::ospfd $self = shift;
289 8         27 my($area, $summary) = ("", "");
290 8         16 my(@summarys, $s);
291 8         14 foreach (@{$self->{summary}}) {
  8         25  
292 1391 100       127632 if (/^ +Summary Net Link States \(Area $IP\)$/) {
    100          
    100          
293 12 100       46 die "$_ Summary $summary in area $area not finished.\n" if $s;
294 11         27 $area = $1;
295 11         22 next;
296             } elsif (/^$/) {
297 145         235 undef $s;
298 145         291 next;
299             } elsif (! $s) {
300 126 100       328 die "$_ No area for summary defined.\n" if ! $area;
301 125         197 $summary = "";
302 125         254 $s = { area => $area };
303 125         249 push @summarys, $s;
304             }
305 1233 100       3900 if (/^LS age: $RE{num}{int}{-keep}$/) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
306 125         14665 $s->{age} = $1;
307             } elsif (/^LS Type: ([\w ()]+)$/) {
308 125 100       14221 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         13725 $s->{address} = $1;
312 124         697 $summary = $1;
313             } elsif (/^Advertising Router: $IP$/) {
314 124         14056 $s->{routerid} = $1;
315             } elsif (/^LS Seq Number: (0x$RE{num}{hex})$/) {
316 124         27058 $s->{sequence} = $1;
317             } elsif (/^Network Mask: $IP$/) {
318 121         26794 $s->{netmask} = $1;
319             } elsif (/^Metric: $RE{num}{int}{-keep}$/) {
320 121         39757 $s->{metric} = $1;
321             } elsif (! /^(Options|Checksum|Length):/) {
322 1         316 die "$_ Unknown line at summary $summary in area $area.\n";
323             }
324             }
325 4 100       24 die "Summary $summary in area $area not finished.\n" if $s;
326 3         22 $self->{ospf}{database}{summarys} = \@summarys;
327             }
328              
329             sub parse_boundary {
330 8     8 0 3851 my OSPF::LSDB::ospfd $self = shift;
331 8         19 my($area, $boundary) = ("", "");
332 8         14 my(@boundarys, $b);
333 8         13 foreach (@{$self->{boundary}}) {
  8         19  
334 731 100       84692 if (/^ +Summary Router Link States \(Area $IP\)$/) {
    100          
    100          
335 12 100       37 die "$_ Boundary $boundary in area $area not finished.\n" if $b;
336 11         27 $area = $1;
337 11         23 next;
338             } elsif (/^$/) {
339 85         133 undef $b;
340 85         180 next;
341             } elsif (! $b) {
342 66 100       151 die "$_ No area for boundary defined.\n" if ! $area;
343 65         83 $boundary = "";
344 65         150 $b = { area => $area };
345 65         127 push @boundarys, $b;
346             }
347 633 100       2050 if (/^LS age: $RE{num}{int}{-keep}$/) {
    100          
    100          
    100          
    100          
    100          
    100          
348 65         7474 $b->{age} = $1;
349             } elsif (/^LS Type: ([\w ()]+)$/) {
350 65 100       7135 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         7173 $b->{asbrouter} = $1;
354 64         394 $boundary = $1;
355             } elsif (/^Advertising Router: $IP$/) {
356 64         7238 $b->{routerid} = $1;
357             } elsif (/^LS Seq Number: (0x$RE{num}{hex})$/) {
358 64         14051 $b->{sequence} = $1;
359             } elsif (/^Metric: $RE{num}{int}{-keep}$/) {
360 61         19763 $b->{metric} = $1;
361             } elsif (! /^(Options|Checksum|Length|Network Mask):/) {
362 1         308 die "$_ Unknown line at boundary $boundary in area $area.\n";
363             }
364             }
365 4 100       48 die "Boundary $boundary in area $area not finished.\n" if $b;
366 3         17 $self->{ospf}{database}{boundarys} = \@boundarys;
367             }
368              
369             sub parse_external {
370 8     8 0 5167 my OSPF::LSDB::ospfd $self = shift;
371 8         16 my $external = "";
372 8         11 my(@externals, $e);
373 8         12 foreach (@{$self->{external}}) {
  8         22  
374 1981 100       186651 if (/^ +Type-5 AS External Link States$/) {
    100          
    100          
375 10 100       30 die "$_ External $external not finished.\n" if $e;
376 9 100       26 die "$_ Too many external sections.\n", if @externals;
377 8         13 next;
378             } elsif (/^$/) {
379 154         218 undef $e;
380 154         276 next;
381             } elsif (! $e) {
382 142         229 $external = "";
383 142         208 $e = {};
384 142         254 push @externals, $e;
385             }
386 1817 100       5961 if (/^LS age: $RE{num}{int}{-keep}$/) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
387 142         15858 $e->{age} = $1;
388             } elsif (/^LS Type: ([\w ()]+)$/) {
389 142 100       15697 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         15521 $e->{address} = $1;
393 141         726 $external = $1;
394             } elsif (/^Advertising Router: $IP$/) {
395 141         15829 $e->{routerid} = $1;
396             } elsif (/^LS Seq Number: (0x$RE{num}{hex})$/) {
397 141         30123 $e->{sequence} = $1;
398             } elsif (/^Network Mask: $IP$/) {
399 138         29707 $e->{netmask} = $1;
400             } elsif (/^ Metric type: ([1-2])$/) {
401 138         29573 $e->{type} = $1;
402             } elsif (/^ Metric: $RE{num}{int}{-keep}$/) {
403 138         44266 $e->{metric} = $1;
404             } elsif (/^ Forwarding Address: $IP$/) {
405 138         44940 $e->{forward} = $1;
406             } elsif (! /^(Options|Checksum|Length| External Route Tag):/) {
407 1         312 die "$_ Unknown line at external $external.\n";
408             }
409             }
410 4 100       22 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         29 $self->parse_network();
418 4         20 $self->parse_summary();
419 4         18 $self->parse_boundary();
420 4         17 $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 16 my OSPF::LSDB::ospfd $self = shift;
445 4         19 my %files = @_;
446 4         27 $self->read_files(%files);
447 4         34 $self->parse_self();
448 4         21 $self->parse_lsdb();
449 4         28 $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;