From ac7e27197255593501c746e46161c63e2dc27b11 Mon Sep 17 00:00:00 2001 From: Steven Bakker Date: Thu, 31 Mar 2011 16:16:10 +0000 Subject: [PATCH] Intermediate check-in while redoing the control socket code. Move as the server-side stuff to Server.pm, make communications very simple, let the client side do all the conversions. --- TODO | 60 +++--- doc/command_mapping.txt | 136 +++++++----- lib/M6/ARP/Control.pm | 450 ++++++---------------------------------- lib/M6/ARP/Sponge.pm | 10 +- lib/M6/ARP/Util.pm | 8 +- sbin/arpsponge.pl | 63 ++++-- sbin/asctl.pl | 259 +++++++++++++++++++---- 7 files changed, 455 insertions(+), 531 deletions(-) diff --git a/TODO b/TODO index dc04191..edc3373 100644 --- a/TODO +++ b/TODO @@ -1,9 +1,43 @@ @(#) $Id$ +-------------------------------------------------- +Tue Mar 29 23:13:42 CEST 2011 + +Internal storage of IP and MAC addresses is now done +as hex strings. + +Need to rework the client/server protocol, so the +client also sends and receives HEX strings. See the +doc/command_mapping.txt file. + +-------------------------------------------------- +Wish list for future enhancements: + +* setuid() to unprivileged user after opening relevant streams. + +* use ithreads for better real-time behaviour: + + - process + - learner + - sweeper + - prober + + Process: Always active, listens to packets on the wire. + Handles ALIVE->PENDING and DEAD->ALIVE, manages + the ARP table. + + Learner: stays active for "n" iterations, then finishes. + + Prober: waits for Learner to finish, then every second, probes + the IPs that are PENDING, moving them to DEAD if necessary. + + Sweeper: waits for Learner to finish, then periodically probes + "quiet" IPs. + -------------------------------------------------- Wed Mar 23 17:45:17 CET 2011 -* speed improvements: +* DONE: speed improvements: MAC and IP addresses already come in as hex strings. We currently use "hex2ip" and "hex2mac" to convert them @@ -16,7 +50,6 @@ Wed Mar 23 17:45:17 CET 2011 for checking whether an IP address is in a network, which dramatically cuts down on the packet handling loop. - #!/usr/bin/perl use M6::ARP::Util qw( :all ); @@ -68,29 +101,6 @@ sub addr_in_net { return ($a & $mask) == $n; } --------------------------------------------------- -Wish list for future enhancements: - -* setuid() to unprivileged user after opening relevant streams. - -* use ithreads for better real-time behaviour: - - - process - - learner - - sweeper - - prober - - Process: Always active, listens to packets on the wire. - Handles ALIVE->PENDING and DEAD->ALIVE, manages - the ARP table. - - Learner: stays active for "n" iterations, then finishes. - - Prober: waits for Learner to finish, then every second, probes - the IPs that are PENDING, moving them to DEAD if necessary. - - Sweeper: waits for Learner to finish, then periodically probes - "quiet" IPs. -------------------------------------------------- Thu Oct 7 09:16:50 CEST 2010 diff --git a/doc/command_mapping.txt b/doc/command_mapping.txt index abd73fd..1910ff0 100644 --- a/doc/command_mapping.txt +++ b/doc/command_mapping.txt @@ -9,11 +9,9 @@ Basic (socket-level) Communications FF = line-feed (\f) TAB = tab (\t) - log = FF + "LOG" + TAB - * Client sends single-line commands to server, terminated by LF. -* Server sends two kinds of reply: +* Server sends two kinds of reply "log_msg" and "command_output": reply := log_msg | command_output log_msg := log_hdr + text + LF @@ -30,6 +28,26 @@ Basic (socket-level) Communications All text up to (but not including) the ready_prompt. + Log messages are sent by the server whenever it has something to log. + Hence, a client may receive zero or more log messages when it waits + for command output. + +Server commands +--------------- + +quit +ping +get_status +get_arp [X] +clear_arp X +get_ip [X] +clear_ip X +set_dead X +set_alive X [Y] +set_pending X Y +set_queue X Y +set_rate X Y + Command mappings ---------------- @@ -37,64 +55,82 @@ Client Server ----------------------------------- quit quit ping ping -sponge X set-dead X -unsponge X set-alive X -clear ip X forget-ip X +sponge X set_dead X +unsponge X set_alive X +clear ip X clear_ip X +clear arp X clear_arp X clear log - -set ip X pending Y set-pending X Y -set ip X alive [Y] set-alive X Y -set ip X dead set-dead X -set ip X mac Y set-alive X Y -set ip X queue Y set-queue X Y -set ip X rate Y set-rate X Y +set ip X pending Y set_pending X Y +set ip X alive [Y] set_alive X Y +set ip X dead set_dead X +set ip X mac Y set_alive X Y +set ip X queue Y set_queue X Y +set ip X rate Y set_rate X Y show status status -show version status -show uptime status +show arp [X] get_arp [X] +show version get_status +show uptime get_status show log - -show ip X show-ip X -show ip X state show-ip X -show ip X mac show-ip X -show ip X queue show-ip X -show ip X rate show-ip X +show ip get_ip +show ip X get_ip X, get_arp X +show ip X state get_ip X +show ip X mac get_arp X +show ip X queue get_ip X +show ip X rate get_ip X Data Types ---------- +IP addresses are sent as hexadecimal strings. +MAC addresses are sent as hexadecimal strings. +Boolean values are sent as 0 or 1. +Time stamps are sent as seconds since epoch. + Server: - Time stamps are sent as seconds since epoch. - Mac addresses are sent/received as hex strings. - IP addresses are sent/received as hex strings. - Boolean values are sent as 0 or 1. - Output is sent as : + Output is sent as = , with + between records. + + ----------------------------- + + ip=1c201a6f + state=ALIVE + queue=0 + rate=0.0 + state_mtime=1301071508 + state_atime=1301071567 + + ip=1c201a70 + state=DEAD + queue=500 + rate=60.0 + state_mtime=1301071402 + state_atime=1301071663 - ip 1c201a6f - state ALIVE - queue 0 - rate 0.0 - mac 000cdbfd2300 - mac_changed 1301071000 - state_mtime 1301071508 - state_atime 1301071567 [OK] - id arpsponge - version 3.10-alpha2 - date 1301071803 - started 1300897051 - network 5bc81100/26 - interface eth0 - ip 5bc81128 - mac fe000096000a - max_queue 200 - max_rate 30.00 - flood_protection 5.00 - max_pending 10 - sweep_period 900 - sweep_age 3600 - proberate 100 - next_sweep 38 - learning 0 - dummy 1 + ----------------------------- + + id=arpsponge + pid=3456 + version=3.10-alpha2(110) + date=1301071803 + started=1300897051 + network=5bc81100 + prefixlen=26 + interface=eth0 + ip=5bc81128 + mac=fe000096000a + max_queue=200 + max_rate=30.00 + flood_protection=5.00 + max_pending=10 + sweep_period=900 + sweep_age=3600 + proberate=100 + next_sweep=38 + learning=0 + dummy=1 + [OK] diff --git a/lib/M6/ARP/Control.pm b/lib/M6/ARP/Control.pm index 933d56d..2a03ce7 100644 --- a/lib/M6/ARP/Control.pm +++ b/lib/M6/ARP/Control.pm @@ -14,10 +14,6 @@ package M6::ARP::Control; use strict; -use base qw( IO::Socket::UNIX ); - -use IO::Socket; -use Scalar::Util qw( blessed ); BEGIN { our $VERSION = '0.02'; @@ -35,302 +31,6 @@ sub _set_error { return; } -# $handle = $handle->_send_data("something\n", ...); -# -# Wrapper around "syswrite" on a socket handle. -# This catches SIGPIPE for when the remote end has disconnected. -# In case of a SIGPIPE or other error, this will return undef, -# otherwise it will return the object itself, allowing chaining: -# -# $handle->_send_data("hello world\n"); -# $handle->_send_data("hello", " world\n"); -# -# $handle->_send_data("hello")->_send_data(" world\n"); -# -sub _send_data { - my $self = shift; - my $data = join('', @_); - - local($::SIG{PIPE}) = 'IGNORE'; - my $nwritten = $self->syswrite($data); - if (!$nwritten && length($!)) { - return $self->_set_error($!); - } - return $self; -} - -# $data = $handle->_get_data($blocking); -# -# Wrapper around "sysread" on a socket handle. This normally -# implements a non-blocking read on a socket, regardless of -# what the current blocking mode on the socket is. Returns -# "undef" if there is no data. Tries to read no more than $BUFSIZ -# bytes, but may run over that if the last character is not a newline. -# -# $data = $handle->_get_data($blocking); -# -sub _get_data { - my $self = shift; - my $blocking = @_ ? int(shift) : 0; - - my $buf; - my $old_blocking = $self->blocking($blocking); - my $n = $self->sysread($buf, $BUFSIZ); - - if ($buf !~ /\n\Z/) { - my $char; - while ($self->sysread($char, 1)) { - $buf .= $char; - last if $char eq "\n"; - } - } - $self->blocking($old_blocking); - return $n ? $buf : undef; -} - -package M6::ARP::Control::Server; - -use POSIX qw( strftime ); - -use base qw( M6::ARP::Control ); - -use IO::Socket; - -sub create_server { - my $type = shift @_; - $type = ref $type || $type; - - my $socketname = shift; - my $maxclients = @_ ? shift : 5; - - print STDERR "M6::ARP::Control::create_server($socketname, $maxclients)\n"; - # Fill in some harmless defaults... - #my $self = $type->new( - my $self = IO::Socket::UNIX->new( - Local => $socketname, - Type => SOCK_STREAM, - Listen => $maxclients, - ) or return $type->_set_error($!); - - $self->blocking(0); # Make sure we never hang as a server. - bless $self, $type; -} - -sub new { - my $type = shift @_; - $type = ref $type || $type; - - print STDERR "M6::ARP::Control::Server::new($type, @_)\n"; - - my %args = @_; - my $self = IO::Socket::UNIX->new(%args) or return $type->_set_error($!); - - $self->blocking(0); # Make sure we never hang as a server. - bless $self, $type; -} - -sub accept { - my $self = shift; - - my $socket = $self->SUPER::accept() or return $self->_set_error($!); - - bless $socket, ref $self; - $socket->blocking(0); # Make sure we never hang as a server. - return $socket->_send_data("\014READY\n"); -} - -sub get_command { - my $self = shift; - return $self->_get_data(); -} - -sub send_response { - my $self = shift; - my $response = join('', @_); - $response .= "\n" if $response !~ /\n\Z/; - return $self->_send_data("$response\014READY\n"); -} - -sub send_log { - my $self = shift; - my $log = join('', @_); - chomp($log); - my $tstamp = strftime("%Y-%m-%d %H:%M:%S", localtime(time)); - my @log = map { "\014LOG\t$tstamp [$$] $_\n" } split(/\n/, $log); - return $self->_send_data(@log); -} - -package M6::ARP::Control::Client; - -use base qw( M6::ARP::Control ); - -use IO::Socket; - -# $ref = $handle->_log_buffer; -# $handle->_log_buffer($ref); -# -# Get/set the internal buffer of logging lines received from -# the server end. The log_buffer acts as a circular buffer of -# $MAXLOGLINES lines. -# -sub _log_buffer { - my $self = shift; - if (@_) { - ${*$self}{'m6_arp_control_client_log_buffer'} = shift; - return $self; - } - else { - return ${*$self}{'m6_arp_control_client_log_buffer'}; - } -} - -# $leftover = $handle->_parse_log_buffer($data [, \@logbuffer]); -# -# Remove the "\014LOG\t" log lines from $data, store them in the -# internal log buffer (or @logbuffer if given) and return the rest -# of $data. -# -sub _parse_log_buffer { - my $self = shift; - my $data = shift; - - my ($log, $maxloglines); - - if (@_) { - $log = shift; - $maxloglines = 0; - } - else { - $log = $self->_log_buffer; - $maxloglines = $MAXLOGLINES; - } - - while ($data =~ s/^\014LOG\t(.*?\n)//m) { - if ($maxloglines && @$log > $maxloglines) { - shift @$log; # Rotate log buffer if necessary. - } - push @$log, $1; - } - return $data; -} - - -# $data = $handle->get_log_buffer; -# -# Return the internal log buffer as a single string. Gather -# any other log information you can get if it is available. -# -sub get_log_buffer { - my $self = shift; - my %args = (-order => +1, @_); - - # Tease out log data from the socket. - my $buf = $self->_parse_log_buffer($self->_get_data(0)); - - # Anything else is weird. Tag it as such. - if (length $buf) { - $buf =~ s/^/UNEXPECTED: /gm; - } - - my $log = $self->_log_buffer; - - $buf = $buf . join('', $args{-order} < 0 ? reverse @$log : @$log); - - return length $buf ? $buf : undef; -} - -# $handle->clear_log_buffer; -# -# Clear the internal log buffer. -# -sub clear_log_buffer { - @{$_[0]->_log_buffer} = (); - return $_[0]; -} - - -# @lines = $handle->read_log_data( [ -blocking => {0|1} ] ); -# -# Read logging data from $handle. Default is to block for input, -# but can be overridden with "-blocking => 0". -# -sub read_log_data { - my $self = shift; - my %args = (-blocking => 1, @_); - - my $blocking = $args{-blocking}; - my @lines; - - # Tease out log data from the socket. - my $buf = $self->_parse_log_buffer($self->_get_data($blocking), \@lines); - - # Anything else is weird. Tag it as such. - if (length $buf) { - push @lines, map { "UNEXPECTED: $_\n" } split(/\n/, $buf); - } - return @lines; -} - -# $data = $handle->_get_response; -# -# Wrapper around "sysread" on a socket handle, reads data -# until it sees the "ready" prompt or an EOF. Strips the -# ready prompt. -# -# Returns undef on EOF or error, a string with the response -# otherwise. Note that the response string may be empty. -# -sub _get_response { - my $self = shift; - my $response = ''; - my $buf = ''; - my $ok = undef; - - while (my $n = $self->sysread($buf, $BUFSIZ)) { - $response .= $buf; - if ($response =~ s/^\014READY\n//m) { - $ok = 1; - last; - } - } - $response = $self->_parse_log_buffer($response); - return $ok ? $response : undef; -} - -sub create_client { - my ($type, $sockfile) = @_; - my $self = IO::Socket::Client->new( - Peer => $sockfile, - Type => SOCK_STREAM, - ) or return; - - return bless $self, $type; -} - -sub new { - my ($type, @args) = @_; - my $self = IO::Socket::UNIX->new(@args) or return $type->_set_error($!); - - bless $self, $type; - $self->_log_buffer([]); - return defined $self->_get_response ? $self : undef; -} - -# $reply = $handle->send_command($command); -# -# Send $command to the remote end and wait for the answer. -# Returns the answer (minus any LOG lines). Returns undef -# on error, in which case the connection is considered to -# be lost. -# -sub send_command { - my $self = shift; - my $command = join(' ', split(' ', join('', @_)))."\n"; - - $self->_send_data($command) || return; - return $self->_get_response; -} - 1; __END__ @@ -345,63 +45,44 @@ M6::ARP::Control - client/server implementation for arpsponge control use M6::ARP::Control; - $server = M6::ARP::Control::Server->create_server($socket_file); + M6::ARP::Control->_set_error("something scwewwy"); - # Alternative method (equivalent to above): - $server = M6::ARP::Control::Server->new( - Local => $socket_file, - Type => SOCK_STREAM, - Listen =>5 - ); + print M6::ARP::Control->error, "\n"; - $conn = $server->accept(); + $M6::ARP::Control::BUFSIZ = 8*1024; + $M6::ARP::Control::MAXLOGLINES = 1024; - $command = $conn->read_command(); - - if (!defined $command) { - print STDERR "Client disconnected\n"; - $conn->close; - } - - if (!$conn->send_reply('Ok')) { - print STDERR "Client disconnected\n"; - $conn->close; - } - - # --------------------------------------------- - - $client = M6::ARP::Control::Client->create_client($socket_file); - - # Alternative method (equivalent to above): - $client = M6::ARP::Control::Client->new( - Peer => $socket_file, - Type => SOCK_STREAM, - ); - - $reply = $client->send_command('something important'); - - if (!defined $reply) { - print STDERR "Server disconnected\n"; - $client->close; - } + # Modules that actually do some work: + use M6::ARP::Control::Base; + use M6::ARP::Control::Server; + use M6::ARP::Control::Client; =head1 DESCRIPTION -This module implements a simple client/server protocol for -controlling the ARP sponge using UNIX domain sockets. +The C modules implement a simple client/server +protocol for controlling the ARP sponge using UNIX domain sockets. -The L(8) uses a -L -object, the L(1) program uses -L. +The server (L) uses a +L +object, the client (L) uses +L. -It is a fairly thin wrapper around L(3p), -implementing some defaults and handling exceptions (most -notably the SIGPIPE when writing to a disconnected peer). +The implementation consists of a fairly thin wrapper around +L(3p), with sponge command handling in the +L +part. + +You will probably never have to deal with this module directly, +but rather use +L +or +L. =head1 PROTOCOL -The protocol implemented by this module is very simple: +=head2 General + +The basic protocol implemented by this module is very simple: =over @@ -423,65 +104,55 @@ Server handles command and sends a reply, followed by "\014READY\n". =back -=head1 M6::ARP::Control::Server +=head2 Logging -The -C -class is designed with single-threaded servers in mind that uses a -C loop to detect input on a socket. Hence, the default -I/O mode these objects is non-blocking. +The server may send unsollicited logging data to the client +which is prefixed by "\014LOG\t" and terminated with a newline. -=head2 Constructor +The client should be aware that these lines can show up where +normal command output is expected. + +The +L +object knows how to handle this and will store logging information +in an internal buffer. + +=head1 VARIABLES =over -=item XB ( I<%ARGS> ) +=item X<$M6::ARP::Control::Error>I<$M6::ARP::Control::Error> -Create a new object instance and return a reference to it. Because -this object inherits from L(3), we must keep the same -semantics for the arguments. +Global control socket error message. Use +L<_set_error|/_set_error> and L +to manipulate this variable. -The L method is preferred. +=item X<$M6::ARP::Control::BUFSIZ>I<$M6::ARP::Control::BUFSIZ> -=item XB ( I [, I ] ) +Maximum size of data chunk we try to read in at once. See also +L. -Create a new server instance, listening on I and returning -a reference to the client object. +=item X<$M6::ARP::Control::MAXLOGLINES>I<$M6::ARP::Control::MAXLOGLINES> -On error, returns C and sets the module's error field. - -=cut +Maximum number of log lines that a +L should buffer internally. =back -=head1 M6::ARP::Control::Client +=head1 CLASS METHODS -=head2 Constructor +The following must be called as B>I. =over -=item XB ( I<%ARGS> ) +=item XB -Create a new object instance and return a reference to it. Because -this object inherits from L(3), we must keep the same -semantics for the arguments. +Return latest error reported by any control socket connection. -The L method is preferred. +=item X<_set_error>B<_set_error> ( I ... ) -=item XB ( I ) - -Create a new client instance, connecting to I and return -a reference to the client object. - -On error, returns C and sets the module's error field. - -=cut - -=back - -=head2 Methods - -=over +Set the control socket error string. Should be called as a class +method. =back @@ -491,9 +162,12 @@ See the L section. =head1 SEE ALSO -L, L(8), -L(3), -L(3). +L, +L, +L, +L(3), +L(3), +L(8), L(1). =head1 AUTHORS diff --git a/lib/M6/ARP/Sponge.pm b/lib/M6/ARP/Sponge.pm index 6f73096..fcab0de 100644 --- a/lib/M6/ARP/Sponge.pm +++ b/lib/M6/ARP/Sponge.pm @@ -250,12 +250,18 @@ sub arp_table { return $self->{'arp_table'} if @_ == 0; - my $ip = shift; + my $ip = shift; + my $arp_table = $self->{'arp_table'}; if (@_) { my $mac = shift; my $time = @_ ? shift : time; - $self->{'arp_table'}->{$ip} = [ $mac, $time ]; + if (defined $mac) { + $self->{'arp_table'}->{$ip} = [ $mac, $time ]; + } + else { + delete $self->{'arp_table'}->{$ip}; + } } return $self->{'arp_table'}->{$ip} ? @{$self->{'arp_table'}->{$ip}} : (); } diff --git a/lib/M6/ARP/Util.pm b/lib/M6/ARP/Util.pm index cbe2206..c134597 100644 --- a/lib/M6/ARP/Util.pm +++ b/lib/M6/ARP/Util.pm @@ -183,12 +183,15 @@ sub mac2mac { ############################################################################### -=item XB ( I