mirror of
https://gitlab.com/shorewall/code.git
synced 2024-12-04 21:41:15 +01:00
74a839b12e
Signed-off-by: Tom Eastep <teastep@shorewall.net>
665 lines
23 KiB
XML
665 lines
23 KiB
XML
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE article PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN"
|
|
"http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
|
|
<article>
|
|
<!--$Id$-->
|
|
|
|
<articleinfo>
|
|
<title>Extension Scripts (User Exits)</title>
|
|
|
|
<authorgroup>
|
|
<author>
|
|
<firstname>Tom</firstname>
|
|
|
|
<surname>Eastep</surname>
|
|
</author>
|
|
</authorgroup>
|
|
|
|
<pubdate><?dbtimestamp format="Y/m/d"?></pubdate>
|
|
|
|
<copyright>
|
|
<year>2001-2010</year>
|
|
|
|
<holder>Thomas M. Eastep</holder>
|
|
</copyright>
|
|
|
|
<legalnotice>
|
|
<para>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
|
|
<quote><ulink url="GnuCopyright.htm">GNU Free Documentation
|
|
License</ulink></quote>.</para>
|
|
</legalnotice>
|
|
</articleinfo>
|
|
|
|
<caution>
|
|
<para><emphasis role="bold">This article applies to Shorewall 4.3 and
|
|
later. If you are running a version of Shorewall earlier than Shorewall
|
|
4.3.5 then please see the documentation for that
|
|
release.</emphasis></para>
|
|
</caution>
|
|
|
|
<section id="Scripts">
|
|
<title>Extension Scripts</title>
|
|
|
|
<para>Extension scripts are user-provided scripts that are invoked at
|
|
various points during firewall start, restart, stop and clear. For each
|
|
script, the Shorewall compiler creates a Bourne Shell function with the
|
|
extension script as its body and calls the function at runtime.</para>
|
|
|
|
<caution>
|
|
<orderedlist>
|
|
<listitem>
|
|
<para>Be sure that you actually need to use an extension script to
|
|
do what you want. Shorewall has a wide range of features that cover
|
|
most requirements.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para>DO NOT SIMPLY COPY RULES THAT YOU FIND ON THE NET INTO AN
|
|
EXTENSION SCRIPT AND EXPECT THEM TO WORK AND TO NOT BREAK SHOREWALL.
|
|
TO USE SHOREWALL EXTENSION SCRIPTS YOU MUST KNOW WHAT YOU ARE DOING
|
|
WITH RESPECT TO iptables/Netfilter AND SHOREWALL.</para>
|
|
</listitem>
|
|
</orderedlist>
|
|
</caution>
|
|
|
|
<para>The following scripts can be supplied:</para>
|
|
|
|
<itemizedlist>
|
|
<listitem>
|
|
<para><filename>lib.private</filename> -- Intended to contain
|
|
declarations of shell functions to be called by other run-time
|
|
extension scripts. See<ulink url="MultiISP.html#lsm"> this
|
|
article</ulink> for an example of its use.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><filename>compile</filename> -- Invoked by the rules compiler
|
|
early in the compilation process. Must be written in Perl.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><filename>init</filename> -- invoked early in <quote>shorewall
|
|
start</quote> and <quote>shorewall restart</quote></para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><filename>initdone</filename> -- invoked after Shorewall has
|
|
flushed all existing rules but before any rules have been added to the
|
|
builtin chains.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><filename>start</filename> -- invoked after the firewall has
|
|
been started or restarted.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><filename>started</filename> -- invoked after the firewall has
|
|
been marked as 'running'.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><filename>stop</filename> -- invoked as a first step when the
|
|
firewall is being stopped.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><filename>stopped</filename> -- invoked after the firewall has
|
|
been stopped.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><filename>clear</filename> -- invoked after the firewall has
|
|
been cleared.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><filename>tcclear</filename> -- invoked to clear traffic shaping
|
|
when CLEAR_TC=Yes in <ulink
|
|
url="manpages/shorewall.conf.html">shorewall.conf</ulink>.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><filename>refresh</filename> -- called in place of
|
|
<filename>init</filename> when the firewall is being refreshed rather
|
|
than started or restarted.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><filename>refreshed</filename> -- invoked after the firewall has
|
|
been refreshed.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><filename>maclog</filename> -- invoked while mac filtering rules
|
|
are being created. It is invoked once for each interface having
|
|
'maclist' specified and it is invoked just before the logging rule is
|
|
added to the current chain (the name of that chain will be in
|
|
$CHAIN).</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><filename>isusable</filename> -- invoked when Shorewall is
|
|
trying to determine the usability of the network interface associated
|
|
with an optional entry in
|
|
<filename>/etc/shorewall/providers</filename>. $1 is the name of the
|
|
interface which will have been determined to be up and configured
|
|
before the script is invoked. The return value from the script
|
|
indicates whether or not the interface is usable (0 = usable, other =
|
|
unusable).</para>
|
|
|
|
<para>Example:<programlisting># Ping a gateway through the passed interface
|
|
case $1 in
|
|
eth0)
|
|
ping -c 4 -t 1 -I eth0 206.124.146.254 > /dev/null 2>&1
|
|
return
|
|
;;
|
|
eth1)
|
|
ping -c 4 -t 1 -I eth1 192.168.12.254 > /dev/null 2>&1
|
|
return
|
|
;;
|
|
*)
|
|
# No additional testing of other interfaces
|
|
return 0
|
|
;;
|
|
esac</programlisting><caution>
|
|
<para>We recommend that this script only be used with
|
|
ADMINISABSENTMINDED=Yes.</para>
|
|
|
|
<para>The firewall state when this script is invoked is
|
|
indeterminate. So if you have ADMINISABSENTMINDED=No in <ulink
|
|
url="manpages/shorewall.conf.html">shorewall.conf</ulink>(8) and
|
|
output on an interface is not allowed by <ulink
|
|
url="manpages/shorewall-stoppedrules.html">stoppedrules</ulink>(8)
|
|
then the isuasable script must blow it's own holes in the firewall
|
|
before probing.</para>
|
|
</caution></para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><filename>save</filename> -- This script is invoked during
|
|
execution of the <command>shorewall save</command> and
|
|
<command>shorewall-lite save</command> commands.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><filename>restored</filename> -- This script is invoked at the
|
|
completion of a successful <command>shorewall restore</command> and
|
|
<command>shorewall-lite restore</command>.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para>findgw -- This script is invoked when Shorewall is attempting to
|
|
discover the gateway through a dynamic interface. The script is most
|
|
often used when the interface is managed by dhclient which has no
|
|
standardized location/name for its lease database. Scripts for use
|
|
with dhclient on several distributions are available at <ulink
|
|
url="http://www.shorewall.net/pub/shorewall/contrib/findgw/">http://www.shorewall.net/pub/shorewall/contrib/findgw/</ulink></para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><filename>scfilter</filename> -- Added in Shorewall 4.4.14.
|
|
Unlike the other scripts, this script is executed by the command-line
|
|
tools (<filename>/sbin/shorewall</filename>,
|
|
<filename>/sbin/shorewall6</filename>, etc) and can be used to
|
|
reformat the output of the <command>show connections</command>
|
|
command. The connection information is piped through this script so
|
|
that the script can drop information, add information or alter the
|
|
format of the information. When using Shorewall Lite or Shorewall6
|
|
Lite, the script is encapsulated in a function that is copied into the
|
|
generated auxillary configuration file. That function is invoked by
|
|
the 'show connections' command.</para>
|
|
|
|
<para>The default script is as follows and simply pipes the output
|
|
through unaltered.</para>
|
|
|
|
<programlisting>#! /bin/sh
|
|
cat -</programlisting>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><filename>postcompile</filename> -- Added in Shorewall 4.5.8.
|
|
This shell script is invoked by<emphasis role="bold">
|
|
/sbin/shorewall</emphasis> after a script has been compiled. $1 is the
|
|
path name of the compiled script.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><filename>lib.cli-user</filename> -- Added in Shorewall 5.0.2.
|
|
This is actually a shell library (set of function declarations) that
|
|
can be used to augment or replace functions in the standard CLI
|
|
libraries.</para>
|
|
</listitem>
|
|
</itemizedlist>
|
|
|
|
<para><emphasis role="bold">If your version of Shorewall doesn't have the
|
|
file that you want to use from the above list, you can simply create the
|
|
file yourself.</emphasis> You can also supply a script with the same name
|
|
as any of the filter chains in the firewall and the script will be invoked
|
|
after the /etc/shorewall/rules file has been processed but before the
|
|
/etc/shorewall/policy file has been processed.</para>
|
|
|
|
<para>The following table indicate which commands invoke the various
|
|
scripts.</para>
|
|
|
|
<informaltable frame="none" rowheader="firstcol">
|
|
<tgroup cols="2">
|
|
<tbody>
|
|
<row>
|
|
<entry><emphasis role="bold">script</emphasis></entry>
|
|
|
|
<entry><emphasis role="bold">Commands</emphasis></entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>clear</entry>
|
|
|
|
<entry>clear</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>compile</entry>
|
|
|
|
<entry>check, compile, export, load, refresh, reload, restart,
|
|
restore,start</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>continue</entry>
|
|
|
|
<entry/>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>init</entry>
|
|
|
|
<entry>load, refresh, reload, restart restore, start</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>initdone</entry>
|
|
|
|
<entry>check, compile, export, refresh, restart, start</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>isusable</entry>
|
|
|
|
<entry>refresh, restart, restore, start</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>maclog</entry>
|
|
|
|
<entry>check, compile, export, refresh, restart, start</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>postcompile</entry>
|
|
|
|
<entry>compile, export, load, refresh, reload, restart, restore,
|
|
start</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>refresh</entry>
|
|
|
|
<entry>refresh</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>refreshed</entry>
|
|
|
|
<entry>refresh</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>restored</entry>
|
|
|
|
<entry>restore</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>save</entry>
|
|
|
|
<entry>save</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>scfilter</entry>
|
|
|
|
<entry>show connections</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>start</entry>
|
|
|
|
<entry>load, reload, restart, start</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>started</entry>
|
|
|
|
<entry>load, reload, restart, start</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>stop</entry>
|
|
|
|
<entry>stop, clear</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>stopped</entry>
|
|
|
|
<entry>stop, clear</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>tcclear</entry>
|
|
|
|
<entry>load, reload, restart, restore, start</entry>
|
|
</row>
|
|
</tbody>
|
|
</tgroup>
|
|
</informaltable>
|
|
|
|
<para>There are a couple of special considerations for commands in
|
|
extension scripts:</para>
|
|
|
|
<itemizedlist>
|
|
<listitem>
|
|
<para>When you want to run <command>iptables</command>, use the
|
|
command <command>run_iptables</command> instead.
|
|
<command>run_iptables</command> will run the iptables utility passing
|
|
the arguments to <command>run_iptables</command> and if the command
|
|
fails, the firewall will be stopped (or restored from the last
|
|
<command>save</command> command, if any).
|
|
<command>run_iptables</command> should not be called from the
|
|
<filename>started</filename> or <filename>restored</filename>
|
|
scripts.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para>If you wish to generate a log message, use <emphasis
|
|
role="bold">log_rule_limit</emphasis>. Parameters are:</para>
|
|
|
|
<itemizedlist>
|
|
<listitem>
|
|
<para>Log Level</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para>Chain to insert the rule into</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para>Chain name to display in the message (this can be different
|
|
from the preceding argument — see the <ulink
|
|
url="PortKnocking.html">Port Knocking article</ulink> for an
|
|
example of how to use this).</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para>Disposition to report in the message (ACCEPT, DROP,
|
|
etc)</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para>Rate Limit (if passed as "" then $LOGLIMIT is assumed — see
|
|
the LOGLIMIT option in <ulink
|
|
url="Documentation.htm#Conf">/etc/shorewall/shorewall.conf</ulink>)</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para>Log Tag ("" if none)</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para>Command (-A or -I for append or insert).</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para>The remaining arguments are passed "as is" to
|
|
iptables</para>
|
|
</listitem>
|
|
</itemizedlist>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para>Many of the extension scripts get executed for both the
|
|
shorewall start and shorewall restart commands. You can determine
|
|
which command is being executed using the contents of $COMMAND.</para>
|
|
|
|
<programlisting>if [ $COMMAND = start ]; then
|
|
...</programlisting>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para>In addition to COMMAND, Shorewall defines three other variables
|
|
that may be used for locating Shorewall files:</para>
|
|
|
|
<itemizedlist>
|
|
<listitem>
|
|
<para>CONFDIR - The configuration directory. Will be <filename
|
|
class="directory">/etc/shorewall</filename>, <filename
|
|
class="directory">/etc/shorewall6/</filename>, <filename
|
|
class="directory">/etc/shorewall-lite</filename>, or <filename
|
|
class="directory">/etc/shorewall6-lite</filename> depending on
|
|
which product is running.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para>SHAREDIR - The product shared directory. Will be <filename
|
|
class="directory">/usr/share/shorewall</filename>, <filename
|
|
class="directory">/usr/share/shorewall6/</filename>, <filename
|
|
class="directory">/usr/share/shorewall-lite</filename>, or
|
|
<filename class="directory">/usr/share/shorewall6-lite</filename>
|
|
depending on which product is running.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para>VARDIR - The product state directory. Defaults <filename
|
|
class="directory">/var/lib/shorewall</filename>, <filename
|
|
class="directory">/var/lib/shorewall6/</filename>, <filename
|
|
class="directory">/var/lib/shorewall-lite</filename>, or <filename
|
|
class="directory">/var/lib/shorewall6-lite</filename> depending on
|
|
which product is running, but may be overridden by an entry in
|
|
${CONFDIR}/vardir.</para>
|
|
</listitem>
|
|
</itemizedlist>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para>Shell variables used in extension scripts must follow the same
|
|
rules as those in<filename> /etc/shorewall/params</filename>. See
|
|
<ulink url="???">this article</ulink>.</para>
|
|
</listitem>
|
|
</itemizedlist>
|
|
|
|
<para/>
|
|
|
|
<section id="Perl">
|
|
<title>Compile-time vs Run-time Scripts</title>
|
|
|
|
<para>Shorewall runs some extension scripts at compile-time rather than
|
|
at run-time.</para>
|
|
|
|
<para>The following table summarizes when the various extension scripts
|
|
are run:<informaltable frame="all">
|
|
<tgroup cols="2">
|
|
<tbody>
|
|
<row>
|
|
<entry><emphasis role="bold">Compile-time</emphasis></entry>
|
|
|
|
<entry><emphasis role="bold">Run-time</emphasis></entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>compile</entry>
|
|
|
|
<entry>clear</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>initdone</entry>
|
|
|
|
<entry>init</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>maclog</entry>
|
|
|
|
<entry>isusable</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>Per-chain (including those associated with
|
|
actions)</entry>
|
|
|
|
<entry>start</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry>postcompile</entry>
|
|
|
|
<entry>started</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry/>
|
|
|
|
<entry>stop</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry/>
|
|
|
|
<entry>stopped</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry/>
|
|
|
|
<entry>tcclear</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry/>
|
|
|
|
<entry>refresh</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry/>
|
|
|
|
<entry>refreshed</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry/>
|
|
|
|
<entry>restored</entry>
|
|
</row>
|
|
|
|
<row>
|
|
<entry/>
|
|
|
|
<entry>scfilter</entry>
|
|
</row>
|
|
</tbody>
|
|
</tgroup>
|
|
</informaltable></para>
|
|
|
|
<para>With the exception of postcompile, compile-time extension scripts
|
|
are executed using the Perl 'eval `cat
|
|
<<emphasis>file</emphasis>>`' mechanism. Be sure that each script
|
|
returns a 'true' value; otherwise, the compiler will assume that the
|
|
script failed and will abort the compilation.</para>
|
|
|
|
<para>Each compile-time script is implicitly prefaced with:</para>
|
|
|
|
<programlisting>package Shorewall::User;</programlisting>
|
|
|
|
<para>Most scripts will need to begin with the following
|
|
line:<programlisting>use Shorewall::Chains;</programlisting>For more
|
|
complex scripts, you may need to 'use' other Shorewall Perl modules --
|
|
browse <filename
|
|
class="directory">/usr/share/shorewall/Shorewall/</filename> to see
|
|
what's available.</para>
|
|
|
|
<para>When a script is invoked, the <emphasis
|
|
role="bold">$chainref</emphasis> scalar variable will hold a reference
|
|
to a chain table entry.<simplelist>
|
|
<member><emphasis role="bold">$chainref->{name}</emphasis>
|
|
contains the name of the chain</member>
|
|
|
|
<member><emphasis role="bold">$chainref->{table}</emphasis> holds
|
|
the table name</member>
|
|
</simplelist></para>
|
|
|
|
<para>To add a rule to the chain:<programlisting>add_rule( $chainref, <<emphasis>the rule</emphasis>> [ , <<emphasis>break lists</emphasis>> ] );</programlisting>Where<simplelist>
|
|
<member><<emphasis>the rule</emphasis>> is a scalar argument
|
|
holding the rule text. Do not include "-A <<emphasis>chain
|
|
name</emphasis>>"</member>
|
|
</simplelist>Example:<programlisting>add_rule( $chainref, '-j ACCEPT' );</programlisting></para>
|
|
|
|
<para>The add_rule() function accepts an optional third argument; If
|
|
that argument evaluates to true and the passed rule contains a <emphasis
|
|
role="bold">--dports</emphasis> or <emphasis
|
|
role="bold">--sports</emphasis> list with more than 15 ports (a port
|
|
range counts as two ports), the rule will be split into multiple rules
|
|
where each resulting rule has 15 or fewer ports in its <emphasis
|
|
role="bold">--dports</emphasis> and <emphasis
|
|
role="bold">--sports</emphasis> lists.</para>
|
|
|
|
<para>To insert a rule into the chain:<programlisting> insert_rule( $chainref, <<emphasis>rulenum</emphasis>>, <<emphasis>the rule</emphasis>> );</programlisting>The
|
|
<emphasis role="bold">log_rule_limit()</emphasis> function works like it
|
|
did in the shell compiler with three exceptions:<itemizedlist>
|
|
<listitem>
|
|
<para>You pass the chain reference rather than the name of the
|
|
chain.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para>The commands are 'add' and 'insert' rather than '-A' and
|
|
'-I'.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para>There is only a single "pass as-is to iptables" argument (so
|
|
you must quote that part).</para>
|
|
</listitem>
|
|
</itemizedlist>Example:<programlisting>log_rule_limit(
|
|
'info' , #Log Level
|
|
$chainref , #Chain to add the rule to
|
|
$chainref->{name}, #Name of the chain as it will appear in the log prefix
|
|
'DROP' , #Disposition of the packet
|
|
'', #Limit
|
|
'' , #Log tag
|
|
'add', #Command
|
|
'-p tcp' #Added to the rule as-is
|
|
);</programlisting>Note that in the 'initdone' script, there is
|
|
no default chain (<emphasis role="bold">$chainref</emphasis>). You can
|
|
obtain a reference to a standard chain by:<programlisting>my $chainref = $chain_table{<<emphasis>table</emphasis>>}{<<emphasis>chain name</emphasis>>};</programlisting>Example:<programlisting>my $chainref = $chain_table{filter}{INPUT};</programlisting></para>
|
|
|
|
<para>You can also use the hash references <emphasis
|
|
role="bold">$filter_table</emphasis>, <emphasis
|
|
role="bold">$mangle_table</emphasis> and <emphasis
|
|
role="bold">$nat_table</emphasis> to access chain references in the
|
|
three main tables.</para>
|
|
|
|
<para>Example:</para>
|
|
|
|
<programlisting>my $chainref = $filter_table->{INPUT}; #Same as above with a few less keystrokes; runs faster too</programlisting>
|
|
|
|
<para>For imformation about the 'compile' extension script, see the
|
|
<ulink url="ManualChains.html">Manual Chains article</ulink>.</para>
|
|
</section>
|
|
</section>
|
|
</article>
|