1
+ # !/usr/bin/env perl
2
+ #
3
+ # Collects a lot of data to map the network connections between a set of servers
4
+ #
5
+
6
+ use strict;
7
+ use Carp qw( croak ) ;
8
+ use Data::Dumper;
9
+ use GraphViz;
10
+ use Log::Log4perl qw( :easy) ;
11
+ Log::Log4perl-> easy_init($WARN );
12
+
13
+ my $map = foreach_line(\&parse_command_output);
14
+
15
+ # WARN('Map after parsing is: ', Dumper($map));
16
+ # exit(0);
17
+
18
+
19
+ # ## NEXT STEPS: o pid é local ao server, por cada conn temos de ter o r_name
20
+ # e l_name
21
+ # uma maneira é na passagem pelo netstat manter um hash com key
22
+ # $server:l_addr:l_port:l_proto => { pid, short_name } que depois expandimos
23
+ # para name cruzando com o /service
24
+ #
25
+
26
+ my $g = GraphViz-> new(
27
+ layout => ' dot' ,
28
+ node => { shape => ' box' },
29
+ );
30
+
31
+ # Create a list of IP addresses per server
32
+ my %ip_map = %{$map -> {ip }};
33
+ while (my ($ip , $name ) = each %{$map -> {ip }}) {
34
+ push @{$ip_map {$name }}, $ip ;
35
+ }
36
+
37
+ # create a local hash with open ports on each server, and name all socket connections
38
+ my %open_ports ;
39
+ walk(
40
+ $map -> {netstat },
41
+ sub {
42
+ my ($item , @path ) = @_ ;
43
+ my $server = $path [0];
44
+
45
+ return unless UNIVERSAL::isa($item => ' HASH' );
46
+ return unless $item -> {pid };
47
+ return unless $item -> {proto } eq ' tcp' ; # We currently don't use UDP
48
+
49
+ $item -> {server } = $server ;
50
+ $item -> {name } =
51
+ ($item -> {pid } && $map -> {slash_services }{$server }{$item -> {pid }})
52
+ || $item -> {short_name }
53
+ || ' ???' ;
54
+
55
+ return unless exists $item -> {port };
56
+
57
+ foreach my $ip (@{$ip_map {$server }}) {
58
+ $open_ports {" $ip :$item ->{port}:$item ->{proto}" } = { item => $item , used => 0 };
59
+ }
60
+ }
61
+ );
62
+
63
+ # scan all connections, and see which ones are in use. Collect edges
64
+ my @edges ;
65
+ my %nodes ;
66
+ walk(
67
+ $map -> {netstat },
68
+ sub {
69
+ my ($item , @path ) = @_ ;
70
+ my $server = $path [0];
71
+
72
+ return unless UNIVERSAL::isa($item => ' HASH' );
73
+ return unless $item -> {state } eq ' ESTABLISHED' ;
74
+ return unless $item -> {proto } eq ' tcp' ;
75
+ return unless exists $ip_map {$item -> {l_addr }} && exists $ip_map {$item -> {r_addr }};
76
+
77
+ # src, dst
78
+ my ($bare_server ) = $item -> {server } =~ m / ^([^.]+)/ ;
79
+
80
+ my @conn_info = (
81
+ {
82
+ key => qq{ $item ->{l_addr}:$item ->{l_port}:$item ->{proto}} ,
83
+ srv => $ip_map {$item -> {l_addr }},
84
+ name => qq{ $item ->{l_name}:$item ->{l_port}} ,
85
+ },
86
+ {
87
+ key => qq{ $item ->{r_addr}:$item ->{r_port}:$item ->{proto}} ,
88
+ srv => $ip_map {$item -> {r_addr }},
89
+ name => qq{ $item ->{r_name}:$item ->{r_port}} ,
90
+ },
91
+ );
92
+
93
+ # check which side of the conn is the server, swap if need, mark server as used
94
+ my ($l_key , $r_key ) = ($conn_info [0]{key }, $conn_info [1]{key });
95
+
96
+ if (exists $open_ports {$l_key }) {
97
+ $open_ports {$l_key }{used }++;
98
+ ($conn_info [0], $conn_info [1]) = ($conn_info [1], $conn_info [0]); # swap direction of edge
99
+ }
100
+ elsif (exists $open_ports {$r_key }) {
101
+ $open_ports {$r_key }{used }++
102
+ }
103
+ else {
104
+ return ; # this should not happen :)
105
+ }
106
+
107
+ push @edges , { src => $conn_info [0]{srv } };
108
+ push @{$nodes {$bare_server }{labels }}, $conn_info [1]{name };
109
+ }
110
+ );
111
+ use Data::Dumper; print STDERR " >>>>>> " , Dumper(\%nodes );
112
+
113
+ # Scan ports in use: create nodes
114
+ walk(
115
+ \%nodes ,
116
+ sub {
117
+ my ($item , $server ) = @_ ;
118
+
119
+ return unless UNIVERSAL::isa($item => ' HASH' );
120
+ return unless $item -> {labels };
121
+
122
+ $g -> add_node($server , headlabel => $server , label => $nodes {$server });
123
+ }
124
+ );
125
+
126
+
127
+ print $g -> as_canon;
128
+
129
+ # #########
130
+ # Parsing
131
+
132
+ sub parse_command_output {
133
+ my ($line , $map ) = @_ ;
134
+
135
+ # ignore empty lines
136
+ return if $line =~ / ^\s *$ / ;
137
+
138
+ # ouput has section, this is the start of a section
139
+ if ($line =~ / ^\s *--- (.+) ---/ ) {
140
+ INFO(" Found section '$1 '" );
141
+ $map -> {current_section } = $1 ;
142
+ return ;
143
+ }
144
+
145
+ # each section reports on several server
146
+ if ($line =~ / ^Server (\S +):$ / ) {
147
+ INFO(" Found server '$1 '" );
148
+ $map -> {current_server } = $1 ;
149
+ return ;
150
+ }
151
+ my ($server , $section ) = @{$map }{qw( current_server current_section) };
152
+
153
+ LOGCROAK(" Got content line but no section active: $line " ) if $line && !$section ;
154
+ LOGCROAK(" Got content line but no server active: $line " ) if $line && !$server ;
155
+
156
+ # DEBUG("current line ($section/$server): $line");
157
+
158
+ my $l_map = $map -> {$section }{$server } ||= {};
159
+ if ($section eq ' interfaces' ) { _parse_interfaces($line , $server , $l_map , $map ); }
160
+ elsif ($section eq ' slash_services' ) { _parse_proc_names($line , $server , $l_map , $map ); }
161
+ elsif ($section eq ' netstat' ) { _parse_netstat($line , $server , $l_map , $map ); }
162
+ }
163
+
164
+ sub _parse_interfaces {
165
+ my ($line , $server , $map , $gmap ) = @_ ;
166
+
167
+ if ($line =~ / ^(\S +)/ ) {
168
+ $map -> {current_interface } = $1 ;
169
+ INFO(" Found interface '$1 '" )
170
+ }
171
+ my $iface = $map -> {current_interface };
172
+
173
+ if ($line =~ / inet addr:(\d +\.\d +\.\d +\.\d +)/ ) {
174
+ INFO(" Found IP address '$1 '" );
175
+ $map -> {$iface } = $1 ;
176
+ $map -> {$1 } = $iface ;
177
+ $gmap -> {ip }{$1 } = $server unless substr ($1 , 0, 4) eq ' 127.' ;
178
+ }
179
+ }
180
+
181
+ sub _parse_proc_names {
182
+ my ($line , $server , $map , $gmap ) = @_ ;
183
+
184
+ if ($line =~ m { ^/service/(.+?): up .pid (\d +)} ) {
185
+ $map -> {$2 } = $1 ;
186
+ $map -> {$1 } = $2 ;
187
+ INFO(" Found service '$1 ' with pid '$2 '" );
188
+ }
189
+ }
190
+
191
+ sub _parse_netstat {
192
+ my ($line , $server , $map , $gmap ) = @_ ;
193
+
194
+ # Active Internet connections (servers and established)
195
+ return if $line =~ / ^Active Internet connections/ ;
196
+
197
+ # Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
198
+ return if $line =~ / Proto\s +Recv-Q\s +Send-Q/ ;
199
+
200
+ # Active UNIX domain sockets (servers and established)
201
+ return if $line =~ / ^Active UNIX domain sockets/ ;
202
+
203
+ # Proto RefCnt Flags Type State I-Node PID/Program name Path
204
+ return if $line =~ / ^Proto RefCnt Flags/ ;
205
+
206
+
207
+ # tcp 0 0 0.0.0.0:25 0.0.0.0:* LISTEN 344/tcpserver
208
+ if ($line =~ m { ^tcp\s +\d +\s +\d +\s +(\d +\.\d +\.\d +\.\d +):(\d +).+(LISTEN)\s +(\d +)/(.+)} ) {
209
+ my $data = { proto => ' tcp' , addr => $1 , port => $2 , state => $3 , pid => $4 , short_name => $5 };
210
+ my $open_sock = $map -> {open_sock } ||= [];
211
+ push @$open_sock , $data ;
212
+ INFO(" Found open socket '$1 :$2 /tcp' with pid '$3 ' at $server " );
213
+ }
214
+ # tcp 0 0 0.0.0.0:2222 0.0.0.0:* LISTEN -
215
+ elsif ($line =~ m { ^tcp\s +\d +\s +\d +\s +(\d +\.\d +\.\d +\.\d +):(\d +).+(LISTEN)\s +-} ) {
216
+ my $data = { proto => ' tcp' , addr => $1 , port => $2 , state => $3 };
217
+ my $open_sock = $map -> {open_sock } ||= [];
218
+ push @$open_sock , $data ;
219
+ INFO(" Found open socket '$1 :$2 /tcp' without pid at $server " );
220
+ }
221
+ # tcp 0 0 213.13.146.24:5222 85.240.82.208:1048 ESTABLISHED31858/beam.smp
222
+ elsif ($line =~ m { ^(\S +)\s +\d +\s +\d +\s +(\d +\.\d +\.\d +\.\d +):(\d +)\s +(\d +\.\d +\.\d +\.\d +):(\d +)\s +(ESTABLISHED)\s *(\d +)/(.+)} ) {
223
+ my $data = { proto => $1 , l_addr => $2 , l_port => $3 , r_addr => $4 , r_port => $5 , state => $6 , pid => $7 , short_name => $8 };
224
+ my $conns = $map -> {conns } ||= [];
225
+ push @$conns , $data ;
226
+ INFO(" Found connection '$2 :$3 /$1 ' - '$4 :$5 /$1 ' with pid '$6 ' at $server " );
227
+ }
228
+ # tcp 0 0 10.135.33.30:50605 10.135.33.14:3306 ESTABLISHED-
229
+ elsif ($line =~ m { ^(\S +)\s +\d +\s +\d +\s +(\d +\.\d +\.\d +\.\d +):(\d +)\s +(\d +\.\d +\.\d +\.\d +):(\d +)\s +(ESTABLISHED)\s *-} ) {
230
+ my $data = { proto => $1 , l_addr => $2 , l_port => $3 , r_addr => $4 , r_port => $5 , state => $6 };
231
+ my $conns = $map -> {conns } ||= [];
232
+ push @$conns , $data ;
233
+ INFO(" Found connection '$2 :$3 /$1 ' - '$4 :$5 /$1 ' with at $server " );
234
+ }
235
+ # udp 0 0 0.0.0.0:32768 0.0.0.0:* 3430/rpc.statd
236
+ elsif ($line =~ m { ^udp\s +\d +\s +\d +\s +(\d +\.\d +\.\d +\.\d +):(\d +).+(\d +)/(.+)} ) {
237
+ my $data = { proto => ' udp' , addr => $1 , port => $2 , pid => $3 , short_name => $4 , state => ' LISTEN' };
238
+ my $open_sock = $map -> {open_sock } ||= [];
239
+ push @$open_sock , $data ;
240
+ INFO(" Found open socket '$1 :$2 /udp' with pid '$3 ' at $server " );
241
+ }
242
+ # udp 0 0 0.0.0.0:32768 0.0.0.0:* -
243
+ elsif ($line =~ m { ^udp\s +\d +\s +\d +\s +(\d +\.\d +\.\d +\.\d +):(\d +).+-} ) {
244
+ my $data = { proto => ' udp' , addr => $1 , port => $2 , state => ' LISTEN' };
245
+ my $open_sock = $map -> {open_sock } ||= [];
246
+ push @$open_sock , $data ;
247
+ INFO(" Found open socket '$1 :$2 /udp' at $server " );
248
+ }
249
+ elsif ($line =~ / ^tcp6\s / ) {
250
+ return ;
251
+ }
252
+ elsif ($line =~ / ^unix\s / ) {
253
+ return ;
254
+ }
255
+ elsif ($line =~ / \s +FIN_WAIT1\s +-/ ) {
256
+ return ;
257
+ }
258
+ elsif ($line =~ / \s +(?:FIN_WAIT1|TIME_WAIT|LAST_ACK|SYN_SENT|CLOSING|CLOSE_WAIT)/ ) {
259
+ return ;
260
+ }
261
+ else {
262
+ WARN(' Unparsed netstat line: ' , $line );
263
+ }
264
+ }
265
+
266
+ # ##################################
267
+ # Iterate over a file descriptor, l
268
+
269
+ sub foreach_line {
270
+ my ($cb ) = @_ ;
271
+
272
+ my %context ;
273
+
274
+ # open(my $fh, '<', $file) || croak('Could not open file: $file');
275
+
276
+ while (my $line = <>) {
277
+ chomp ($line );
278
+ # eval { $cb->($line, \%context) };
279
+ $cb -> ($line , \%context );
280
+ if ($@ ) {
281
+ print STDERR " ERROR: $@ " ;
282
+ return undef ;
283
+ }
284
+ }
285
+
286
+ return \%context ;
287
+ }
288
+
289
+ sub walk {
290
+ my ($root , $action , @rest ) = @_ ;
291
+
292
+ $action -> ($root , @rest );
293
+
294
+ return if !ref ($root );
295
+ if (UNIVERSAL::isa($root => ' ARRAY' )) {
296
+ my $i = 0;
297
+ walk($_ , $action , @rest , $i ++) for @$root ;
298
+ }
299
+ elsif (UNIVERSAL::isa($root => ' HASH' )) {
300
+ walk($root -> {$_ }, $action , @rest , $_ ) for keys %$root ;
301
+ }
302
+ }
0 commit comments