Copyright © 2008, 2009 Thomas M. Eastep
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation; with no Invariant Sections, with no Front-Cover, and with no Back-Cover Texts. A copy of the license is included in the section entitled “GNU Free Documentation License”.
2010/01/03
Table of Contents
For Perl programmers, manual chains provide an alternative to Actions with extension scripts. Manual chains are chains which you create and populate yourself using the low-level functions in Shorewall::Chains.
Manual chains work in conjunction with the compile extension script and Embedded PERL scripts. The general idea is like this:
In the compile extension script, you define functions that you can call later using Embedded PERL. These functions create a manual chain using Shorewall::Chains::new_manual_chain() and populate it with rules using Shorewall::Chains::add_rule().
The functions also call Shorewall::Config::shorewall() to create and pass a rule to Shorewall. The TARGET in that rule is the name of the chain just created.
The functions defined in the compile script are called by embedded PERL statements. The arguments to those calls define the contents of the manual chains and the rule(s) passed back to Shorewall for normal processing.
As shown in the following example, manual chains are created using a call to &Shorewall::Chains::new_manual_chain. That function returns a reference to the newly-created chain.
By default, chains are subject to optimize 4 (see OPTIMIZE in shorewall.conf (5)). You can exempt your chain from that optimization by calling one of two functions:
&Shorewall::Chains::dont_delete - exempt the chain from all optimizations.
&Shorewall::Chains::dont_optimize - exempt the chain from all optimizations except that the chain will be omitted from the configuration if there are no branches to the chain.
Both functions accept the name of the chain or a reference to the chain as a single argument and both return a reference to the chain (to the chain's table entry).
This example provides an alternative to the Port Knocking example.
In this example, a Knock.pm module is created and placed in /etc/shorewall:
package Knock; use strict; use warnings; use base qw{Exporter}; use Carp; use Shorewall::Chains; use Scalar::Util qw{reftype}; use Shorewall::Config qw{shorewall}; our @EXPORT = qw{Knock}; my %recent_names; my %chains_created; sub scalar_or_array { my $arg = shift; my $name = shift; return () unless defined $arg; return ($arg) unless reftype($arg); return @$arg if reftype($arg) eq 'ARRAY'; croak "Expecting argument '$name' to be scalar or array ref"; } sub Knock { my $src = shift; my $dest = shift; my $args = shift; my $proto = $args->{proto} || 'tcp'; my $seconds = $args->{seconds} || 60; my $original_dest = $args->{original_dest} || '-'; my @target = scalar_or_array($args->{target}, 'target'); my @knocker_ports = scalar_or_array($args->{knocker}, 'knocker'); my @trap_ports = scalar_or_array($args->{trap}, 'trap'); if (not defined $args->{name}) { # If you don't supply a name, then this must be the single-call # variant, so you have to specify all the arguments unless (scalar @target) { croak "No 'target' ports specified"; } unless (scalar @knocker_ports) { croak "No 'knock' ports specified"; } } # We'll need a unique name for the recent match list. Construct one # from the port and a serial number, if the user didn't supply one. my $name = $args->{name} || ($target[0] . '_' . ++$recent_names{$target[0]}); $name = 'Knock' . $name; # We want one chain for all Knock rules that share a 'name' field my $chainref = $chains_created{$name}; unless (defined $chainref) { $chainref = $chains_created{$name} = new_manual_chain($name); } # Logging if ($args->{log_level}) { foreach my $port (@target) { log_rule_limit($args->{log_level}, $chainref, 'Knock', 'ACCEPT', '', $args->{log_tag} || '', 'add', "-p $proto --dport $port -m recent --rcheck --name $name" ); log_rule_limit($args->{log_level}, $chainref, 'Knock', 'DROP', '', $args->{log_tag} || '', 'add', "-p $proto --dport ! $port" ); } } # Add the recent match rules to the manual chain foreach my $knock (@knocker_ports) { add_rule($chainref, "-p $proto --dport $knock -m recent --name $name --set -j DROP"); } foreach my $trap (@trap_ports) { add_rule($chainref, "-p $proto --dport $trap -m recent --name $name --remove -j DROP"); } foreach my $port (@target) { add_rule($chainref, "-p $proto --dport $port -m recent --rcheck --seconds $seconds --name $name -j ACCEPT"); } # And add a rule to the main chain(s) to jump into the manual chain at the appropriate points my $all_dest_ports = join(',', @target, @knocker_ports, @trap_ports); shorewall "$chainref->{name} $src $dest $proto $all_dest_ports - $original_dest"; return 1; } 1;
This simplifies /etc/shorewall/compile:
use Knock; 1;
The rule from the Port Knocking article:
#ACTION SOURCE DEST PROTO DEST PORT(S) SSHKnock net $FW tcp 22,1599,1600,1601
becomes:
PERL Knock 'net', 'loc:192.168.1.5', {target => 22, knocker => 1600, trap => [1599, 1601]};
Similarly
#ACTION SOURCE DEST PROTO DEST PORT(S) SOURCE ORIGINAL # PORT(S) DEST DNAT- net loc:192.168.1.5 tcp 22 - 206.124.146.178 SSHKnock net $FW tcp 1599,1600,1601 SSHKnock net loc:192.168.1.5 tcp 22 - 206.124.146.178
becomes:
#ACTION SOURCE DEST PROTO DEST PORT(S) SOURCE ORIGINAL # PORT(S) DEST DNAT- net loc:192.168.1.5 tcp 22 - 206.124.146.178 PERL Knock 'net', '$FW', {name => 'SSH', knocker => 1600, trap => [1599, 1601]}; PERL Knock 'net', 'loc:192.168.1.5', {name => 'SSH', target => 22, original_dest => '206.124.136.178'};