2 # dmon -- DTrace-backed IP network real-time monitoring utility
3 # Copyright (C) 2022-2025 Sergey Matveev <stargrave@stargrave.org>
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, version 3 of the License.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 dmon is DTrace-backed IP network real-time monitoring utility. It can be
21 treated as a replacement for iftop utility, that does not always work
24 It generates DTrace script to gather data about send and received IP
25 packets, or about TCP, UDP, UDP-Lite, SCTP ones. It collects and
26 aggregates necessary data and sends it back to that Perl program, that
27 prettifies its output.
31 There are two modes of operation:
35 =item IP packets monitoring
37 You have to specify exact network interface names you wish to take into
38 account. Pay attention that all packets are captured, even forwarded
39 ones, that is useful on router.
41 =item TCP/UDP/UDP-Lite/SCTP packets monitoring
43 With C<-p> option you have to specify the transport protocol you want to
44 monitor. Only explicit send and receive calls are gathered, so no
45 forwarded packets won't be counted.
49 =head2 Hostnames resolving
51 By default this program tries to asynchronously resolve all hostname for
52 IP addresses in background. You can turn this off with C<-H> option.
53 C<-P> option also disables service name lookup for the port numbers.
57 Each line shows source and destination addresses with the traffic
58 direction. If transport mode monitoring is active, then each address
59 also contains the transport-level port. Unless explicitly disabled by
60 command lines options, ports and IP addresses are tried to be resolved
61 to human readable names.
63 Number of transferred bytes is shown nearby. There are three numbers:
64 bytes passed for the last second, for last 5 seconds and for the 10
65 seconds. All calculations are done in powers of two, so C<KB> actually
66 means C<KiB>, but it is shortened to save the terminal space.
67 Only the payload is considered, so transport/IP headers overhead is
68 not counted. All entries are ordered by 10sec speed value.
70 Top section contains number of send/received packets (also as a tuple of
71 1s/5s/10s) and total amount of send/received traffic with corresponding
76 Sergey Matveev L<mailto:stargrave@stargrave.org>
83 my $VERSION = "0.1.0";
85 ########################################################################
88 $Getopt::Std::STANDARD_HELP_VERSION = 1;
90 sub VERSION_MESSAGE { print "dmon $VERSION\n" }
92 my $HeightDefault = 40;
93 my %Opts = (h => $HeightDefault);
96 print "Usage: $0 [{-d|-D}] [-h HEIGHT] [-H] [-P]
97 {-p [tcp|udp|udplite|sctp] | ifname0 ifname1 ...}
98 -d -- output only generated DTrace script
99 -D -- take input from stdin, not summoned DTrace
100 -h -- set output height, by default it is $HeightDefault
101 -H -- do not lookup host names
102 -P -- do not lookup service names
103 -p -- do not collect IP packets, but specified protocol ones
107 getopts "dDh:HPp:", \%Opts;
108 if ((not exists $Opts{p}) and ($#ARGV < 0)) { die HELP_MESSAGE }
111 #########################################################################
113 use File::Temp qw/tempfile/;
114 my ($dfh, $dfn) = tempfile();
115 END { unlink $dfn if defined $dfn }
116 my $Proto = (defined $Opts{p}) ? $Opts{p} : "ip";
118 #pragma D option quiet
119 #pragma D option switchrate=10Hz
122 printa("T %@d %@d %@d %@d\n", @rxpkts, @txpkts, @rxbytes, @txbytes);
128 if ($Proto eq "ip") {
129 my $conds = join " || ", map { "args[3]->if_name == \"$_\"" } @ARGV;
131 my ($probe, $dir) = @_;
136 @" . $dir . "[args[2]->ip_saddr, args[2]->ip_daddr] = sum(args[2]->ip_plength);
137 \@${dir}pkts = count();
138 \@${dir}bytes = sum(args[2]->ip_plength);
141 ipprobe "ip:::send", "tx";
142 ipprobe "ip:::receive", "rx";
146 printa(\"< %s %s %\@d\\n\", \@rx);
148 printa(\"> %s %s %\@d\\n\", \@tx);
154 my ($probe, $dir) = @_;
158 @" . $dir . "[args[2]->ip_saddr,
159 args[4]->${Proto}_sport,
161 args[4]->${Proto}_dport] = sum(args[2]->ip_plength);
162 \@${dir}pkts = count();
163 \@${dir}bytes = sum(args[2]->ip_plength);
166 nonipprobe "${Proto}:::send", "tx";
167 nonipprobe "${Proto}:::receive", "rx";
171 printa(\"< [%s]:%d [%s]:%d %\@d\\n\", \@rx);
173 printa(\"> [%s]:%d [%s]:%d %\@d\\n\", \@tx);
178 ########################################################################
180 my @sizePrefix = ("E", "P", "T", "G", "M", "K");
181 my @countBound = (1e18, 1e15, 1e12, 1e9, 1e6, 1e3);
182 my @sizeBound = (1<<60, 1<<50, 1<<40, 1<<30, 1<<20, 1<<10);
185 my ($s, $suffix, $bounds) = @_;
186 foreach my $i (0 .. $#{$bounds}) {
187 next if $s < @{$bounds}[$i];
188 return sprintf "%.2f%s%s", $s / @{$bounds}[$i], $sizePrefix[$i], $suffix;
193 sub humanCount { return human shift, "", \@countBound }
194 sub humanSize { return human shift, "B", \@sizeBound }
196 ########################################################################
198 use Socket qw(AF_INET AF_INET6 inet_pton);
202 my $family = (index($host, ":") == -1) ? AF_INET : AF_INET6;
203 my $packed = inet_pton $family, $host;
204 my @addr = gethostbyaddr $packed, $family;
205 return (@addr > 0) ? $addr[0] : "[$host]";
210 my %HostCache :shared;
213 unless (defined $Opts{H}) {
215 $ResolverQueue = Thread::Queue->new();
216 $ResolverQueue->limit = 0;
217 threads->create(sub {
218 while (my $host = $ResolverQueue->dequeue()) {
219 my $name = gethost $host;
221 $HostCache{$host} = $name;
226 ########################################################################
233 if ($Proto eq "ip") {
236 my $i = rindex($pair, ":");
237 $host = substr $pair, 1, $i-2;
238 $port = substr $pair, $i+1;
247 $name = $HostCache{$host};
249 unless (defined $name) {
250 $ResolverQueue->enqueue($host);
254 if ($Proto eq "ip") { return $name };
260 $serv = $ServCache{$port};
261 unless (defined $serv) {
262 $serv = getservbyport $port, $Proto;
263 unless (defined $serv) { $serv = $port }
264 $ServCache{$port} = $serv;
267 return "$name:$serv";
270 ########################################################################
275 foreach (@_[1 .. 4]) { $sum += $_ }
276 push @r, int($sum / 5);
277 foreach (@_[5 .. 9]) { $sum += $_ }
278 push @r, int($sum / 10);
282 my (@allRxPktsAgg, @allTxPktsAgg);
283 my (@allRxBytesAgg, @allTxBytesAgg);
285 my ($allRxBytesPeak, $allTxBytesPeak, $totalBytesPeak) = (0, 0, 0);
287 Rx: @|||||| @|||||| @|||||| @|||||| @|||||| @|||||| peak: @||||||
288 humanCount($allRxPktsAgg[0]), humanCount($allRxPktsAgg[1]), humanCount($allRxPktsAgg[2]), humanSize($allRxBytesAgg[0]), humanSize($allRxBytesAgg[1]), humanSize($allRxBytesAgg[2]), humanSize($allRxBytesPeak)
289 Tx: @|||||| @|||||| @|||||| @|||||| @|||||| @|||||| peak: @||||||
290 humanCount($allTxPktsAgg[0]), humanCount($allTxPktsAgg[1]), humanCount($allTxPktsAgg[2]), humanSize($allTxBytesAgg[0]), humanSize($allTxBytesAgg[1]), humanSize($allTxBytesAgg[2]), humanSize($allTxBytesPeak)
291 Total: @|||||| @|||||| @|||||| peak: @||||||
292 humanSize($allRxBytesAgg[0] + $allTxBytesAgg[0]), humanSize($allRxBytesAgg[1] + $allTxBytesAgg[1]), humanSize($allRxBytesAgg[2] + $allTxBytesAgg[2]), humanSize($totalBytesPeak)
293 ------------------------------------------------------------------------------------------------------------------------
296 my ($left, $dir, $right, $agg);
298 @>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>... @| @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<... @|||||| @|||||| @||||||
299 humanHost($left), $dir, humanHost($right), humanSize($agg->[0]), humanSize($agg->[1]), humanSize($agg->[2])
302 ########################################################################
307 open $dexec, "<", "$dfn" or die "$!";
308 while (<$dexec>) { print }
314 open $dexec, "-|", "dtrace -s $dfn" or die "$!";
319 my %bytes = ("<" => {}, ">" => {});
320 my %seen = ("<" => {}, ">" => {});
321 my @allRxPkts = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
322 my @allTxPkts = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
323 my @allRxBytes = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
324 my @allTxBytes = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
326 next unless /^[<>T]/;
328 if ($cols[0] eq "T") {
329 unshift @allRxPkts, $cols[1]; pop @allRxPkts;
330 unshift @allTxPkts, $cols[2]; pop @allTxPkts;
331 unshift @allRxBytes, $cols[3]; pop @allRxBytes;
332 unshift @allTxBytes, $cols[4]; pop @allTxBytes;
333 if ($cols[3] > $allRxBytesPeak) { $allRxBytesPeak = $cols[3] }
334 if ($cols[4] > $allTxBytesPeak) { $allTxBytesPeak = $cols[4] }
335 if (($cols[3] + $cols[4]) > $totalBytesPeak) {
336 $totalBytesPeak = $cols[3] + $cols[4];
338 @allRxPktsAgg = agg1510 @allRxPkts;
339 @allTxPktsAgg = agg1510 @allTxPkts;
340 @allRxBytesAgg = agg1510 @allRxBytes;
341 @allTxBytesAgg = agg1510 @allTxBytes;
344 foreach my $dir (keys %bytes) {
345 foreach my $src (keys %{$bytes{$dir}}) {
346 foreach my $dst (keys %{$bytes{$dir}{$src}}) {
347 my $vals = $bytes{$dir}{$src}{$dst};
348 if ((not exists $seen{$dir}{$src}) or
349 (not exists $seen{$dir}{$src}{$dst})) {
353 $bytes{$dir}{$src}{$dst} = [@{$vals}[0 .. 9]];
355 my @r = ($dir eq "<") ? ($dst, "<=", $src) : ($src, "=>", $dst);
356 push @r, [agg1510 @{$vals}];
357 if ($r[-1][-1] == 0) {
358 delete $bytes{$dir}{$src}{$dst};
365 @res = sort { $b->[-1][2] <=> $a->[-1][2] } @res;
366 printf "\033[H\033[J";
368 foreach (@res[0 .. $Opts{h}]) {
370 ($left, $dir, $right, $agg) = @{$_};
373 %seen = ("<" => {}, ">" => {});
376 unless (exists $bytes{$cols[0]}{$cols[1]}) {
377 %{$bytes{$cols[0]}{$cols[1]}} = ();
379 unless (exists $bytes{$cols[0]}{$cols[1]}{$cols[2]}) {
380 @{$bytes{$cols[0]}{$cols[1]}{$cols[2]}} = (0, 0, 0, 0, 0, 0, 0, 0, 0);
382 unshift @{$bytes{$cols[0]}{$cols[1]}{$cols[2]}}, $cols[3];
383 unless (exists $seen{$cols[0]}{$cols[1]}) {
384 %{$seen{$cols[0]}{$cols[1]}} = ();
386 $seen{$cols[0]}{$cols[1]}{$cols[2]} = 1;