#!/usr/bin/perl

# This is a program for manipulating Captrap's MAC address table.

# Copyright 2009 Corey Hickey


# This file is part of Captrap.
#
# Captrap is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Captrap is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Captrap.  If not, see <http://www.gnu.org/licenses/>.


use strict;
use warnings FATAL => 'all';

use DBI;
use Term::ReadLine; # apt-get install libterm-readline-perl-perl ??
use Switch;

# for development using a different Captrap module
# use lib "lib";
use Captrap qw(:misc :actions :config :db);


# -----------------------------------------------------------------------------
# printing
# -----------------------------------------------------------------------------

# print main help info
sub usage {
  my $common = shift; # unused
  my $actions = mk_actions();
  my $actions_text = describe_actions($actions);
  print "
This is a script for manipulating Captrap's MAC address table.

captrap_mac [ACTION] [[ACTION-PARAMETERS]] ...

ACTIONS

$actions_text

EXIT STATUS

0              Everything is ok.

1              No arguments given; usage information was shown.

2              Invalid argument.

3              There was a problem executing an action.


EXAMPLES

Print known and unknown MAC addresses:
captrap_mac -list-known -list-unknown

List known addresses, add a test MAC, list again, delete the test, and list:
captrap_mac -list-known -add-mac 00:de:ad:be:ef:01 bogey test \
    -list-known -del-mac 00:de:ad:be:ef:01 -list-known

Change the state and comment of a MAC address (comment is made blank):
captrap_mac -upd-mac 00:de:ad:be:ef:01 00:de:ad:be:ef:01 up \"\"

"
}


# wrapper for printing known MAC addresses
sub get_print_known_macs {
  my $common = shift;
  print_known_macs(get_known_macs($common));
  return 0; # ok
}


# wrapper for printing unknown MAC addresses
sub get_print_unknown_macs {
  my $common = shift;
  print_unknown_macs(get_unknown_macs($common));
  return 0; # ok
}


# print a labelled table of known MAC addresses
sub print_known_macs {
  my $macs = shift;
  my $labels = [ "MAC address", "state", "comment" ];
  print "[[[ known MAC addresses ]]]\n";
  print_table($macs, $labels);
}


# print a labelled table of unknown MAC addresses
sub print_unknown_macs {
  my $macs = shift;
  my $labels = [ "MAC address", "bytes", "first seen", "last seen" ];
  print "[[[ unknown MAC addresses ]]]\n";
  print_table($macs, $labels);
  return 0; # ok
}


# print a labelled table of states
sub print_valid_states {
  my $states = shift;
  my $labels = [ "state" ];
  print "[[[ valid states ]]]\n";
  print_table($states, $labels);
}


# print an indexed table
sub print_table {
  my $table = shift; # ref to array of array refs
  my $labels = shift; # array ref
  # first initialize column widths based on label widths
  my @widths;
  for (my $i = 0; $i < @$labels; ++$i) {
    $widths[$i] = length($labels->[$i]);
  }
  # now expand widths as necessary to fit data
  foreach my $line (@$table) {
    for (my $i = 0; $i < @$line; ++$i) {
      # if it's not defined, make room for '<NULL>'
      my $l = defined($line->[$i]) ? length($line->[$i]) : length('<NULL>');
      if (defined($widths[$i])) {
        $widths[$i] = $l if $l > $widths[$i];
      } else {
      # new entry
        $widths[$i] = $l;
      }
    }
  }
  # use widths to generate format string
  my @ph;
  my $len = 0; # length of a separator line (used later)
  foreach my $width (@widths) {
    push(@ph, "%-${width}s");
    $len += $width;
  }
  my $fmt = join("  ", @ph) . "\n";
  $len += 2 * $#widths; # to account for the double-spaces
  # now adjust to make room for indices
  my $digits = length($#{$table});
  my $ws = " " x ($digits + 2);
  printf("$ws$fmt", @$labels);
  $fmt = "%${digits}d  $fmt";
  # print a separator line
  print $ws, "-" x $len, "\n";
  # now finally print the lines
  for (my $i = 0; $i < @$table; ++$i) {
    # make undefs safe for printing
    my @row = map { defined($_) ? $_ : "<NULL>" } @{$table->[$i]};
    printf("$fmt", $i, @row);
  }
  print "\n";
}

# -----------------------------------------------------------------------------
# database
# -----------------------------------------------------------------------------

# get all known macs
sub get_known_macs {
  my $common = shift;
  my $macs = $common->{config}->{db_table_macs};
  my $sel = "
      SELECT MAC, STATE, COMMENT
      FROM $macs
      ORDER BY STATE, MAC
  ";
  my $sth = $common->{dbh}->prepare($sel);
  $sth->execute();
  my @macs;
  while (my @a = $sth->fetchrow_array()) {
    die if @a < 3; # shouldn't happen
    push(@macs, \@a);
  }
  return \@macs;
}


# add a mac address
sub add_mac {
  my $common = shift;
  my $mac = shift;
  my $state = shift;
  my $comment = shift;
  unless (tr_mac(\$mac)) {
    print STDERR "Error: supplied MAC address \"$mac\" is invalid\n";
    return 2;
  }
  if (mac_is_known($common, $mac)) {
    print STDERR "Error: supplied MAC address \"$mac\" is already in table\n";
    return 3;
  }
  unless (check_state($common, $state)) {
    print STDERR "Error: supplied state \"$state\" is invalid\n";
    return 2;
  }
  $comment = undef if safe_eq($comment, "");
  my $macs = $common->{config}->{db_table_macs};
  my $ins = "
      INSERT INTO $macs (mac, state, comment)
      VALUES (?, ?, ?)
  ";
  my $sth = $common->{dbh}->prepare($ins);
  $sth->execute($mac, $state, $comment);
  return 0;
}


# delete a mac address
sub del_mac {
  my $common = shift;
  my $mac = shift;
  unless (tr_mac(\$mac)) {
    print STDERR "Error: supplied MAC address \"$mac\" is invalid\n";
    return 2;
  }
  unless (mac_is_known($common, $mac)) {
    print STDERR "Error: supplied MAC address \"$mac\" is not in table\n";
    return 3;
  }
  my $macs = $common->{config}->{db_table_macs};
  my $del = "
      DELETE FROM $macs
      WHERE MAC = ? LIMIT 1
  ";
  my $sth = $common->{dbh}->prepare($del);
  $sth->execute($mac);
  return 0;
}


# update a mac address
sub upd_mac {
  my $common = shift;
  my $old_mac = shift;
  my $mac = shift;
  my $state = shift;
  my $comment = shift;
  unless (tr_mac(\$old_mac)) {
    print STDERR "Error: supplied MAC address \"$old_mac\" is invalid\n";
    return 2;
  }
  unless (mac_is_known($common, $old_mac)) {
    print STDERR "Error: supplied MAC address \"$old_mac\" is not in table\n";
    return 3;
  }
  unless (tr_mac(\$mac)) {
    print STDERR "Error: supplied MAC address \"$mac\" is invalid\n";
    return 2;
  }
  if ($mac ne $old_mac && mac_is_known($common, $mac)) {
    print STDERR "Error: supplied MAC address \"$mac\" is already in table\n";
    return 3;
  }
  unless (check_state($common, $state)) {
    print STDERR "Error: supplied state \"$state\" is invalid\n";
    return 2;
  }
  $comment = undef if safe_eq($comment, "");
  my $macs = $common->{config}->{db_table_macs};
  my $upd = "
      UPDATE $macs SET MAC = ?, STATE = ?, COMMENT = ?
      WHERE MAC = ? LIMIT 1
  ";
  my $sth = $common->{dbh}->prepare($upd);
  $sth->execute($mac, $state, $comment, $old_mac);
  return 0;
}

# -----------------------------------------------------------------------------
# misc utility
# -----------------------------------------------------------------------------

# translate a mac address to lowercase and check if it is valid
sub tr_mac {
  my $mac = shift; # scalar ref
  $$mac =~ tr/[A-Z]/[a-z]/;
  return $$mac =~ /^([0-9a-f]{2}:){5}[0-9a-f]{2}$/;
}


# check if a mac address is in the macs table
# mac should already be lowercase
sub mac_is_known {
  my $common = shift;
  my $mac = shift;
  my $known_macs = get_known_macs($common);
  foreach my $line (@$known_macs) {
    return 1 if $line->[0] eq $mac;
  }
  return 0; # uknown mac
}


# see if supplied state is valid
sub check_state {
  my $common = shift;
  my $state = shift;
  foreach my $valid (@{$common->{config}->{states}}) {
    return 1 if $state eq $valid;
  }
  return 0; # bad
}


# make a 1-column table out of the states list
sub get_valid_states {
  my $common = shift;
  my $states = $common->{config}->{states};
  return [ map { [ $_ ] } @$states ];
}


# strip leading and trailing whitespace
sub trim {
  my $line = shift;
  $line =~ /^\s*(.*\S)\s*$/;
  return $1;
}


# make a line out of MAC info
sub mac_line {
  my $mac = shift;
  my $state = shift;
  my $comment = shift;
  $comment = "<NULL>" unless defined($comment);
  return "$mac  $state  $comment\n";
}

# -----------------------------------------------------------------------------
# interactive terminal functions
# -----------------------------------------------------------------------------

# get a MAC address from the user, possibly using a table index
sub interact_get_mac {
  my $term = shift;
  my $macs = shift;
  my $lineref = shift; # may be undef
  my $enter = "Enter the index number from the table";
  my $prompt = "  index ";
  # print different stuff depending on whether the user can enter a MAC
  if (defined($lineref)) {
    $enter .= ".\n";
    $prompt .= "? ";
  } else {
    $enter .= ", or enter a valid MAC address.\n";
    $prompt .= "or MAC ? ";
  }
  my $to_quit = "To cancel and go back to the previous terminal, hit CTRL-D.\n";
  print $enter;
  print $to_quit;
  my $line;
  GET_MAC: while (defined($line = $term->readline($prompt))) {
    $line = trim($line);
    next GET_MAC unless defined($line);
    if ($line =~ '^\d+$') {
      # looks like an index number
      if ($line > $#{$macs}) {
        print "Error: index is too high. Highest index in table: $#{$macs}.\n";
        print "Look at the table of MACs and try again.\n";
        print $to_quit;
        next GET_MAC;
      }
      # ok, looks like a valid index
      $$lineref = $macs->[$line] if defined($lineref);
      return $macs->[$line]->[0];
    }
    # only accept a MAC if $lineref is undefined
    if (!defined($lineref) && tr_mac(\$line)) {
      # ok, looks like a valid MAC address
      return $line;
    }
    print "Error: unrecognized input: $line\n";
    print $enter;
    print $to_quit;
  }
  return undef; # user hit CTRL-D
}


# get a state from the user by using a table index
sub interact_get_state {
  my $term = shift;
  my $states = shift;
  print "What state is traffic received by this MAC?\n";
  print "Enter the index number from the table\n";
  my $to_quit = "To cancel and go back to the previous terminal, hit CTRL-D.\n";
  print $to_quit;
  my $prompt = "  state index ? ";
  my $line;
  GET_STATE: while (defined($line = $term->readline($prompt))) {
    $line = trim($line);
    next GET_STATE unless defined($line);
    if ($line =~ '^\d+$') {
      # looks like an index number
      if ($line > $#{$states}) {
        print "Error: index is too high. Highest index in table: $#{$states}.\n";
        print "Look at the table of states and try again.\n";
        print $to_quit;
        next GET_STATE;
      }
      # ok, looks like a valid index
      return $states->[$line]->[0];
    }
    print "Error: unrecognized input: $line\n";
    print "Enter an index number from the table.\n";
    print $to_quit;
  }
  return undef; # user hit CTRL-D
}


# get a comment from the user
sub interact_get_comment {
  my $term = shift;
  print "Would you like to enter a comment into the table?\n";
  print "If so, type the comment below. Otherwise, hit <ENTER>.\n";
  my $to_quit = "To cancel and go back to the previous terminal, hit CTRL-D.\n";
  print $to_quit;
  my $prompt = "  comment ? ";
  return $term->readline($prompt);
}


# print help information for how to change MAC info
sub interact_change_info_help {
  my $mac = shift;
  my $state = shift;
  my $comment = shift;
  $comment = "<NULL>" unless defined($comment);
  return "Here's the current set of information:\n" .
      mac_line($mac, $state, $comment) . "\n" .
      "Do you want to change anything?\n" .
      "Enter 'm' to change MAC, 's' for state, 'c' for comment.\n" .
      "Enter 'n' if you don't want to make any further changes.\n" .
      "Enter 'h' to print this help.\n" .
      "To quit back to the main interactive terminal, hit CTRL-D.\n";
}


# walk the user through changing mac/state/comment
# returns undef if the user cancelled, 1 if changes were made, or 0 otherwise
sub interact_change_info {
  my $common = shift;
  my $term = shift;
  my $unknown_macs = shift;
  my $states = shift;
  my $mac = shift;     # scalar ref
  my $state = shift;   # scalar ref
  my $comment = shift; # scalar ref
  print interact_change_info_help($$mac, $$state, $$comment);
  my $prompt = "  change info ? ";
  my $line;
  my ($old_mac, $old_state, $old_comment) = ($$mac, $$state, $$comment);
  CHANGE: while (defined($line = $term->readline($prompt))) {
    $line = trim($line);
    next CHANGE unless defined($line);
    my $cmd = (split(/\s/, $line))[0];
    my ($save, $ref); # in case the user cancels a change
    switch ($cmd) {
      case (/^(m|mac)$/i) {
        ($save, $ref) = ($$mac, $mac);
        print_unknown_macs($unknown_macs);
        $$mac = interact_get_mac($term, $unknown_macs);
      }
      case (/^(s|state)$/i) {
        ($save, $ref) = ($$state, $state);
        print_valid_states($states);
        $$state = interact_get_state($term, $states);
      }
      case (/^(c|comment)$/i) {
        ($save, $ref) = ($$comment, $comment);
        $$comment = interact_get_comment($term);
        if (safe_eq($$comment, "")) {
          $$comment = undef if safe_eq($$comment, "");
          # we need to trick the undef check below
          $ref = \"tricked";
        }
      }
      case (/^(h|help)$/i) {
        # interact_change_info_help is printed anyway
      }
      case (/^(n|no)$/i) {
        print "Finished making changes.\n";
        my $changed = 0;
        $changed |= $old_mac     ne $$mac;
        $changed |= $old_state   ne $$state;
        $changed |= ! safe_eq($old_comment, $$comment);
        return $changed;
      }
      else {
        print "unrecognised command \"$cmd\"; try \"help\"\n";
        next;
      }
    }
    unless (defined($$ref)) {
      # user hit CTRL-D while making an individual change
      print "\nchange cancelled\n";
      $$ref = $save;
    }
    print interact_change_info_help($$mac, $$state, $$comment);
  }
  # user hit CTRL-D, so revert changes
  print "\nCancelling changes to this MAC...\n";
  $$mac     = $old_mac;
  $$state   = $old_state;
  $$comment = $old_comment;
  return undef;
}


# query for confirmation
sub interact_confirm {
  my $term = shift;
  my $prompt = shift;
  print "Enter 'y' or 'n'.\n";
  my $line;
  CONFIRM: while (defined($line = $term->readline($prompt))) {
    $line = trim($line);
    next CONFIRM unless defined($line);
    my $cmd = (split(/\s/, $line))[0];
    switch ($cmd) {
      case (/^(y|yes)$/i) {
        return 1;
      }
      case (/^(n|no)$/i) {
        return 0;
      }
      else {
        print "Please enter 'y' or 'n'.\n";
      }
    }
  }
  # user hit CTRL-D
  print "\nchoice cancelled\n";
  return undef;
}


# interactive terminal for adding a MAC address
sub interact_add {
  my $common = shift;
  my $term = shift;
  my $unknown_macs = get_unknown_macs($common);
  print_unknown_macs($unknown_macs);
  print "What MAC address would you like to add?\n";
  my $mac = interact_get_mac($term, $unknown_macs);
  if (! defined($mac)) {
    print "\nCancelled\n";
    return 0;
  }
  print "You have chosen to add MAC address: $mac\n";
  my $states = get_valid_states($common);
  print_valid_states($states);
  my $state = interact_get_state($term, $states);
  if (! defined($state)) {
    print "\nCancelled\n";
    return 0;
  }
  print "You have chosen state: $state\n";
  my $comment = interact_get_comment($term);
  if (! defined($comment)) {
    print "\nCancelled\n";
    return 0;
  }
  # empty comment --> NULL
  if ($comment eq "") {
    print "You have chosen to leave comment NULL.\n";
    $comment = undef;
  } else {
    print "You have chosen comment: $comment\n";
  }
  my $changed = interact_change_info($common, $term, $unknown_macs, $states,
      \$mac, \$state, \$comment);
  if ($changed) {
    print "MAC info was changed to:\n";
  } else {
    print "MAC info remains set to:\n";
  }
  print mac_line($mac, $state, $comment);
  print "Do you want to add this MAC address to the database?\n";
  if (interact_confirm($term, "  confirm add ? ")) {
    if (! add_mac($common, $mac, $state, $comment)) {
      print "Successfully added $mac.\n";
    }
    print "Returning to main terminal.\n";
  } else {
    print "Add canceled. Returning to main terminal.\n";
  }
}


# interactive terminal for deleting a MAC address
sub interact_del {
  my $common = shift;
  my $term = shift;
  my $known_macs = get_known_macs($common);
  print_known_macs($known_macs);
  print "What MAC address would you like to delete?\n";
  my $mac = interact_get_mac($term, $known_macs);
  if (! defined($mac)) {
    print "\nCancelled\n";
    return 0;
  }
  print "You have chosen to delete MAC address: $mac\n";
  print "Do you want to delete this MAC address from the database?\n";
  if (interact_confirm($term, "  confirm delete ? ")) {
    if (! del_mac($common, $mac)) {
      print "Successfully deleted $mac.\n";
    }
    print "Returning to main terminal.\n";
  } else {
    print "Delete canceled. Returning to main terminal.\n";
  }
}


# interactive terminal for updating a MAC address
sub interact_upd {
  my $common = shift;
  my $term = shift;
  my $known_macs   = get_known_macs($common);
  my $unknown_macs = get_unknown_macs($common);
  print_known_macs($known_macs);
  print "What MAC address would you like to update?\n";
  my $lineref;
  my $old_mac = interact_get_mac($term, $known_macs, \$lineref);
  if (! defined($old_mac)) {
    print "\nCancelled\n";
    return 0;
  }
  print "You have chosen to update MAC address: $old_mac\n";
  my ($mac, $state, $comment) = @$lineref;
  my $states = get_valid_states($common);
  my $changed = interact_change_info($common, $term, $unknown_macs, $states,
      \$mac, \$state, \$comment);
  if (!$changed) {
    print "No changes made. Returning to main terminal.\n";
    return;
  }
  print "MAC info was changed to:\n";
  print mac_line($mac, $state, $comment);
  print "Do you want to update the MAC info in the database?\n";
  if (interact_confirm($term, "  confirm update ? ")) {
    if (! upd_mac($common, $old_mac, $mac, $state, $comment)) {
      print "Successfully updated $mac.\n";
    }
    print "Returning to main terminal.\n";
  } else {
    print "Update canceled. Returning to main terminal.\n";
  }
}


# main interactive terminal
sub interact {
  my $common = shift;
  my $term = Term::ReadLine->new("captrap_mac");
  my $prompt = "captrap_mac > ";
  print "\nWelcome! Enter 'h' for help or 'q' to quit.\n";
  my $line;
  TERM: while (defined($line = $term->readline($prompt))) {
    $line = trim($line);
    next TERM unless defined($line);
    my $cmd = (split(/\s/, $line))[0];
    switch ($cmd) {
      case (/^(a|add)$/i) {
        interact_add($common, $term);
      }
      case (/^(d|delete)$/i) {
        interact_del($common, $term);
      }
      case (/^(u|update)$/i) {
        interact_upd($common, $term);
      }
      case (/^(l|list)$/i) {
        get_print_known_macs($common);
        get_print_unknown_macs($common);
      }
      case (/^(h|help|\?)$/i) {
        interact_help();
      }
      case (/^(q|quit)$/i) {
        print "quitting interactive mode\n";
        last TERM;
      }
      else {
        print "unrecognised command \"$cmd\"; try \"help\"\n";
      }
    }
  }
  print "\n";
}


# print help for main interactive terminal
sub interact_help {
  print "
This is an interactive terminal for manipulating information in Captrap's MAC
table. You can add, delete, and update information about MAC addresses by using
the following commands. All commands have a short form--you can enter just the
first letter. For example, 'a' is equivalent to 'add'.

add      Add a MAC address to the table.

delete   Delete a MAC address from the table.

update   Update a MAC address or any of its information.

list     Print lists of known and unknown MAC addresses.

help     Print this information

quit     Quit the terminal.

The 'add', 'delete', and 'update' commands ask several questions. To cancel an
in-progress add, update, or delete, use CTRL-D to go back to the main terminal.
CTRL-D also exits from the main terminal.

If you ever get lost, it's always safe to kill this program (by using CTRL-C,
for example); changes to the MAC address table are only committed after a
confirmation prompt.
";
}


# -----------------------------------------------------------------------------
# automatic address adding
# -----------------------------------------------------------------------------

# run a command, search the output, and set corresponding parameters
sub grep_cmd {
  my $cmd = shift; # array ref
  my $re = shift; # regular expression
  local (*C_OUT, *P_IN);
  pipe(P_IN, C_OUT); # child --> parent
  my $childpid = fork;
  die "couldn't fork" unless defined($childpid);
  if (! $childpid) {
    # I am the child
    close(P_IN);
    open(STDOUT, ">&=C_OUT") or die "child: could not reopen STDOUT";
    # "perldoc -f exec" and "perldoc perlobj" to see why
    exec { $cmd->[0] } @$cmd or die "Can't exec ", $cmd->[0];
  }
  # I am the parent
  close(C_OUT);
  my $matches;
  while (<P_IN>) {
    if (/$re/) {
      # we got it!
      $matches = [ $1, $2, $3, $4, $5, $6, $7, $8, $9 ];
      1 while (<P_IN>); # discard the rest
      last;
    }
  }
  close(P_IN) or die "close P_IN failed for some reason";
  # don't want zombies
  waitpid($childpid, 0);
  my ($status, $signal) = ($? >> 8, $? & 127);
  if ($status) {
    die "child '$cmd->[0]' command failed. exit status $status, signal $signal";
  }
  return $matches; # is undef if $re not found
}


# automatically determine address to add
sub auto_add {
  my $common = shift;
  my $dry_run = shift; # may be undef
  my $iface = $common->{config}->{interface};
  my $matches;
  # get local MAC address
  $matches = grep_cmd([ "ifconfig" ],
      qr/^$iface\s+.*\s+HWaddr\s+([0-9a-f:]*)\s*$/);
  my $local_mac = $matches->[0];
  unless (defined($local_mac)) {
    print STDERR "unable to determine local MAC address";
    return 1;
  }
  print "local MAC address: $local_mac\n";
  # get remote gateway IP address
  $matches = grep_cmd([ qw(route -n) ], qr/^0\.0\.0\.0\s+([0-9.]*)\s+/);
  my $gw_ip = $matches->[0];
  unless (defined($gw_ip)) {
    print STDERR "unable to determine remote gateway IP address";
    return 1;
  }
  print "remote gateway IP address: $gw_ip\n";
  # ping the gateway
  print "pinging gateway $gw_ip ... ";
  $matches = grep_cmd([ qw(ping -n -c 1 -W 10), $gw_ip ],
      qr/^(\d+) bytes from $gw_ip:/);
  my $pinged = $matches->[0];
  unless (defined($pinged)) {
    print STDERR "failed\nunable to ping remote gateway $gw_ip";
    return 1;
  }
  print "ok\n";
  # check the arp cache for the gateway MAC
  my $gw_ip_esc = $gw_ip;
  $gw_ip_esc =~ s/\./\\\./g; # have to escape '.'
  $matches = grep_cmd([ qw(arp -an), $gw_ip ],
      qr/^\? \($gw_ip_esc\) at (([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}) /);
  my $gw_mac = $matches->[0];
  unless (defined($gw_mac)) {
    print STDERR "unable to determine gateway MAC address from ARP cache\n";
    return 1;
  }
  $gw_mac = lc($gw_mac);
  print "remote gateway MAC address: $gw_mac\n";
  # broadcast is broadcast
  my $bcast_mac = "ff:ff:ff:ff:ff:ff";
  # ok, now we know all we need, so check the database for existing MACs
  my $macs_found = [
    [ $local_mac, "down",  "automatically determined local address" ],
    [ $gw_mac,    "up",    "automatically determined gateway address" ],
    [ $bcast_mac, "bcast", "broadcast address" ],
  ];
  my $macs_add = [];
  foreach my $mac (@$macs_found) {
    if (mac_is_known($common, $mac->[0])) {
      print "already in database: $mac->[0]\n";
    } else {
      push(@$macs_add, $mac);
    }
  }
  unless (@$macs_add) {
    print "no remaining MAC addresses to add\n";
    return 0;
  }
  my $labels = [ "MAC address", "state", "comment" ];
  print "\n[[[ MAC addresses to add ]]]\n";
  print_table($macs_add, $labels);
  return 0 if ($dry_run);
  # now add them
  foreach my $mac (@$macs_add) {
    print "adding: $mac->[0]...\n";
    add_mac($common, @$mac);
  }
  print "all done\n";
  return 0;
}


# wrapper for printing only
sub auto_print {
  my $common = shift;
  return auto_add($common, 1);
}

# -----------------------------------------------------------------------------
# actions info
# -----------------------------------------------------------------------------

# return a hash of action info
sub mk_actions {
  my $actions = mk_ixhash();
  %$actions = (
    "-help" => {
      func => \&usage,
      args => [],
      desc => "
          Print this usage text.
      ",
    },
    "-inter" => {
      func => \&interact,
      args => [],
      desc => "
          Start an interactive terminal session. All functions available in the
          command-line interface are available in the terminal. For quick
          edits, the terminal may be more convenient; all operations are
          guided.
      ",
    },
    "-list-known" => {
      func => \&get_print_known_macs,
      args => [],
      desc => "
          Print a table of known MAC addresses.
      ",
    },
    "-list-unknown" => {
      func => \&get_print_unknown_macs,
      args =>[],
      desc => "
          Print a table of unknown MAC addresses.
      ",
    },
    "-add-mac" => {
      func => \&add_mac,
      args => [ qw(MAC STATE COMMENT) ],
      desc => "
          Add a MAC address. This action takes three parameters: (1) MAC
          address in the usual colon-separated hexadecimal form, (2) traffic
          state, and (3) comment. The MAC address may be upper- or lowercase;
          it will be translated to lowercase automatically. The traffic state
          must be one of the states listed in the config file. The comment is
          mandatory, but it may be empty; use \"\" to supply an empty comment.
      ",
    },
    "-del-mac" => {
      func => \&del_mac,
      args => [ "MAC" ],
      desc => "
          Delete a MAC address. This action takes one parameter: a MAC address.
          See \"-add-mac\" for details.
      ",
    },
    "-upd-mac" => {
      func => \&upd_mac,
      args => [ qw(OLD_MAC NEW_MAC STATE COMMENT) ],
      desc => "
          Update a MAC address, state, and comment. This action takes four
          parameters: a MAC address to change, and the new MAC address, state,
          and comment. See \"-add-mac\" for details.
      ",
    },
    "-auto-add" => {
      func => \&auto_add,
      args => [],
      desc => "
          Attempt to determine the local and remote MAC addresses by examining
          the output of 'ifconfig', 'route', and 'arp', and then add them to
          the database. This also adds the broadcast address.
      ",
    },
    "-auto-print" => {
      func => \&auto_print,
      args => [],
      desc => "
          Like \"-auto-add\", but just print what would be added.
      ",
    },
  );
  return $actions;
}


# -----------------------------------------------------------------------------


# parse the arguments and take actions
if (! @ARGV) {
  usage();
  exit(1);
}
my $actions = mk_actions();
check_args(\@ARGV, $actions);

my $config = parse_config();
my $common = {
  config => $config,
  dbh => mk_dbh($config, 1),
};

do_args($common, \@ARGV, $actions);

$common->{dbh}->disconnect();
exit(0);
