Skip to content

Commit bc65c86

Browse files
committed
Experimental script to draw a .dot file with TCP connections
Signed-off-by: Pedro Melo <[email protected]>
1 parent 1269f96 commit bc65c86

File tree

1 file changed

+302
-0
lines changed

1 file changed

+302
-0
lines changed

bin/x-map-network-connections-mesh

+302
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
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

Comments
 (0)